spring boot 对spring mvc 几乎全部都做了自动配置,所以基本不用使用者进行配置,当然也可以自己配置。例如:静态资源、内容协商、试图解析器、网站图标、数据绑定器、欢迎页等等都会自动注册。
一、静态资源和访问
目录
类路径下 :/static
(or /public
or /resources
or /META-INF/resources
),只要静态资源放入这些路径下,那么直接在浏览器就可以访问了(根路径)。
静态资源映射的路径是/**。那么当存在动态资源和静态资源请求路径相同的时候,容器会先处理动态资源,如果动态资源不存在,则交给静态资源处理。
静态资源访问前缀(默认无)
spring.mvc.static-path-pattern=/resources/**
写了这个配置,那么就会静态资源加前缀。
修改静态资源的文件夹
spring.web.resources.static-locations
写了这个配置,就可以修改默认的静态资源路径,可以接受数组。
同时支持webjars,也就是把一些css、js打包成一个jar包,然后直接在pom中引入,直接可以使用。访问路径是 /webjars/**
欢迎页和图标
静态页有2中处理方式,第一种是直接在静态文件夹下放入index.html,另一种是增加一个controller处理欢迎页。
index.html 配置欢迎页的时候,不能配置访问前缀,否则会欢迎页会失效。
图标也是直接命名放入静态资源文件夹,同时配置前缀也会导致无法使用网站图标。
在测试的时候,会发现经常出现缓存问题。
二、静态资源访问的底层原理
- spring boot 启动的时候,会加载相关加载类
- spring mvc 启动自动配置类大部分都在 WebMvcAutoConfiguration
@Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class })
- 容器中配置一个webmvc的配置类,其中属性依赖于WebMvcProperties.class,ResourceProperties.class
@Configuration(proxyBeanMethods = false) @Import(EnableWebMvcConfiguration.class) @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
- WebMvcProperties == spring mvc
- ResourceProperties == spring resource
- 配置类只有一个有参构造器,那么所有参数都在容器中
- 资源处理的默认规则
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { //判断静态资源是否开启 add-mappings if (!this.resourceProperties.isAddMappings()) { logger.debug("Default resource handling disabled"); return; } //静态资源,缓存的时间 Duration cachePeriod = this.resourceProperties.getCache().getPeriod(); CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl(); if (!registry.hasMappingForPattern("/webjars/**")) { //webjars 路径处理,同时会缓存一段时间 customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/") .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)); } //静态资源访问规则处理 String staticPathPattern = this.mvcProperties.getStaticPathPattern(); if (!registry.hasMappingForPattern(staticPathPattern)) { //在配置的位置,寻找静态资源 customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern) .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations())) .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)); } }
- 欢迎页源码
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Optional<Resource> welcomePage, String staticPathPattern) { if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) { //欢迎页存在 && /** 直接进入到欢迎页 logger.info("Adding welcome page: " + welcomePage.get()); setRootViewName("forward:index.html"); } else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) { //否则进入一个controller logger.info("Adding welcome page template: index"); setRootViewName("index"); } }
//HandlerMapping:处理器映射,保存了每个handler能处理那些请求 //下面就是放入一个欢迎页处理的handlerMapping @Bean public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) { WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping( new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(), this.mvcProperties.getStaticPathPattern()); welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider)); welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations()); return welcomePageHandlerMapping; }
三、请求处理
请求映射就不去说了,相抵比较简单。
简单说一下 rest风格的请求,现在开发中期望使用http的方法动词进行操作的区分,同样针对user的数据处理,都是/user,GET-获取用户、DELETE-删除用户、PUT-修改用户、POST-保存用户
核心就是配置HiddenHttpMethodFilter的核心filter,只要带一个“_method”的隐藏域就可以实现rest请求了。需要在配置文件开启 spring.mvc.hiddenmethod.filter = true
表单提交的rest风格原理,因为表单只有post和get,所以需要处理,其他可以提交Rest风格提交的时候,不需要处理。
- 需要_method参数
- 被HiddenHttpMethodFilter拦截
- 必须是post方式,才能使用rest风格的提交
- 获取到_method的值
- 根据判断,使用wapper包装模式,重写一下请求
- 放行的时候,使用wapper进行处理请求
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.web.filter;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
public class HiddenHttpMethodFilter extends OncePerRequestFilter {
private static final List<String> ALLOWED_METHODS;
public static final String DEFAULT_METHOD_PARAM = "_method";
private String methodParam = "_method";
public HiddenHttpMethodFilter() {
}
public void setMethodParam(String methodParam) {
Assert.hasText(methodParam, "'methodParam' must not be empty");
this.methodParam = methodParam;
}
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest requestToUse = request;
if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ENGLISH);
if (ALLOWED_METHODS.contains(method)) {
requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
}
}
}
filterChain.doFilter((ServletRequest)requestToUse, response);
}
static {
ALLOWED_METHODS = Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
}
private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
private final String method;
public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
super(request);
this.method = method;
}
public String getMethod() {
return this.method;
}
}
}
请求映射原理
所有对spring mvc的分析,都是从DispatcherServlet开始的,查看它的继承结构,可以看到本身就是一个 servlet,查找它的doGet\doPost方法,调用顺序是:
doGet\doPost(FrameworkServlet)->processRequest(FrameworkServlet)->doService(DispatcherServlet)->doDispatch(DispatcherServlet) 。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Object dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
//查看那个handler处理该请求
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
所有的handler 都保存在 handlerMappings中。
其中 RequestMappingHandlerMapping 保存,每一个类处理哪个请求:
所有的请求映射都保存在handlerMapping中,
- spring boot 自动配置了欢迎页的 handlerMapping 自动配置进去
- 请求进来后会尝试所有的HandlerMapping是否处理请求,直到找到能处理请求的
- spring boot 自动配置的handlerMapping,一共5个
- 使用者也可以自己配置handlerMapping,例如api/v1 和 api/v2 调用不同的包的业务实现
四、参数处理
spring boot 对于参数的处理沿用了 spring mvc 方式,主要有一下几种:
- 在路径中使用通配符/car/{id}/owner/{username},那么在参数中可以使用@PathVariable(“参数名”)获取,可以直接用map接受全部参数
- 获取请求头的方式,使用@RequestHeader,也可以使用map接收全部请求头
- 最常用的方式就是使用@RequestParam 接收 ?x1=a1&x2=a2_1&x2=a2_2,也可以使用map接收全部参数
- 使用@CookieValue 注解获取Cookie中的值,也可以使用map接收全部Cookie的值
- 使用@RequestBody 获取Post请求体的全部内容,一般使用一个bean接收全部值
- 使用@RequestAttribute 获取请求域中的属性值
- 使用@MatrixVariable获取矩阵变量,矩阵变量就是请求中使用;进行分割的参数,例如:/car;jsession=abc,可以使它处理cookie被禁用而无法使用session的问题,但是在spring boot中默认禁用矩阵变量的,对于路径的处理都是使用UrlPathHepler,其中有一个属性removeSemicolonContent默认移除;号后的内容,设置为true即可
各种参数解析原理
- HandlerMapping中找到能够处理请求的Handler(其实就是Controller的某个方法)
- 为当前的Handler找到一个适配器HandlerAdapter->RequestMappingHandlerAdapter
四种HandlerAdapter:
- 0-支持RequestMapping 注解的
- 1-支持函数式编程的
之后就会执行目标方法:
26个参数解析器,其实也就是接收参数有多少种写法:
15中返回值处理器,也就是能够写多少种返回值方式:
真正的执行目标方法:
确定目标方法每一个参数值:
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
//获取参数详细信息
MethodParameter[] parameters = this.getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
} else {
//返回值
Object[] args = new Object[parameters.length];
//遍历各个参数
for(int i = 0; i < parameters.length; ++i) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] == null) {
//解析器是否支持该参数,遍历26个解析器哪个一个能支持传递过来的参数
//就是查看参数上标注的哪个注解,例如标注了@ReqeustParam
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
//给参数赋值,拿到当前参数的参数解析器
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
} catch (Exception var10) {
if (this.logger.isDebugEnabled()) {
String exMsg = var10.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
this.logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw var10;
}
}
}
return args;
}
}
五、Servlet Api 参数
spring mvc 也是可以传入 servlet api的参数,例如 request、session、header等等。
跟参数解析一样,也是在26个解析中查询到符合哪个解析器,例如request就是由解析器ServletRequestMethodArgumentResolver进行解析。
六、复杂参数
map和model
相当于在request的请求与中放入数据。map、model类型的参数,最终会返 BingingAwareModelMap,是map也会model,继承了linkHashMap。
最终map和model对象都解析成一个对象:
当目标方法执行完毕后, 将所有的数据放在ModelAndViewContainer,包含需要去的页面地址和相关的数据
model中的所有数据,遍历放入请求域中。
RedirectAttribute
重定向携带的数据
ServletResponse
原生的响应体所带数据
七、自定义对象参数
spring mvc 可以将页面提交的数据,直接封装成对象,下面看看数据绑定的原理。
查找支持的解析器,发现是 ServletModelAttributeMethodProcessor
是否标注注解了或者不是简单类型:
发现支持之后,就准备进行封装:
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
String name = ModelFactory.getNameForParameter(parameter);
ModelAttribute ann = (ModelAttribute)parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
}
Object attribute = null;
BindingResult bindingResult = null;
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
} else {
try {
//创建一个空的pojo对象
attribute = this.createAttribute(name, parameter, binderFactory, webRequest);
} catch (BindException var10) {
if (this.isBindExceptionRequired(parameter)) {
throw var10;
}
if (parameter.getParameterType() == Optional.class) {
attribute = Optional.empty();
}
bindingResult = var10.getBindingResult();
}
}
if (bindingResult == null) {
//web数据绑定器,将请求参数的值绑定到制定的bean中(attribute)
//利用类型转换器,把数据转换成java中的类型
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
this.bindRequestParameters(binder, webRequest);
}
this.validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
return attribute;
}
绑定器中的转换服务:
利用反射机制,进行数据封装。GenericConversionService(每个属性的值,也是在124个转换器进行找到可以转换的转换器,进行转换)
使用者也可以在webDataBind放入自己的转换器。