1、 SpringMVC 自动配置的场景概述
在进行项目编写前,我们还需要知道一个东西,就是SpringBoot对我们的SpringMVC还做了哪些配置,包括如何扩展,如何定制。
只有把这些都搞清楚了,我们在之后使用才会更加得心应手。途径一:源码分析;途径二:官方文档!
官网阅读
官方文档内容概述如下:
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多场景我们都无需自定义配置)
The auto-configuration adds the following features on top of Spring’s defaults:
-
Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
内容协商视图解析器和BeanName视图解析器 -
Support for serving static resources, including support for WebJars 。
静态资源(包括webjars) -
Automatic registration of Converter, GenericConverter, and Formatter beans.
自动注册了转换器(这就是我们网页提交数据到后台自动封装成为对象的东西,比如把"1"字符串自动转换为int类型),格式化器(比如页面给我们了一个2019-8-10,它会给我们自动格式化为Date对象)】 -
Support for HttpMessageConverters (covered later in this document).
支持HttpMessageConverters( SpringMVC用来转换Http请求和响应的的,比如我们要把一个User对象转换为JSON字符串,可以去看官网文档解释;) -
Automatic registration of MessageCodesResolver (covered later in this document).
定义错误代码生成规则的 -
Static index.html support.
首页定制 -
Custom Favicon support (covered later in this document).
图标定制 -
Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).
初始化数据绑定器:帮我们把请求数据绑定到JavaBean中! -
If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.
不用@EnableWebMvc注解。使用 @Configuration + WebMvcConfigurer 自定义规则 -
If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.
如果希望提供RequestMappingHandlerMapping、RequestMappingHandlerAdapter或ExceptionHandler、ExceptionResolver的自定义实例,则可以声明WebMVCregistrationAdapter实例来提供此类组件。 -
If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.
使用 @EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC
2、Web开发静态资源【使用方法】
静态资源目录
只要静态资源放在下面几种路径下就可以正常访问:
- /static
- /public
- /resources
- /META-INF/resources
访问路径:当前项目根路径/ + 静态资源名
原理: 静态映射/**。
- 请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面。
可以改变默认的静态资源路径,如下:
resources:
static-locations: [classpath:/haha/]
- 此时我们在location上加了一个大括号,表示该localtions为数组,也就是说还可以在后面加其他路径。
- 注意:改变后
/static,/public,/resources, /META-INF/resources
等静态资源路径会失效
welcome与favicon功能
官方文档
欢迎页支持
-
静态资源路径下 index.html 为默认欢迎页。
-
可以通过配置静态资源路径改变目录,但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问
spring: # mvc: # static-path-pattern: /res/** 这个会导致welcome page功能失效 resources: static-locations: [classpath:/haha/]
-
controller也能处理/index。
自定义Favicon
- 指网页标签上的小图标。
- favicon.ico 放在静态资源目录下即可。
3、Web开发静态资源【源码分析】
使用SpringBoot的步骤:
- SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类)
- SpringMVC功能的自动配置类WebMvcAutoConfiguration
写请求非常简单,那我们要引入我们前端资源,我们项目中有许多的静态资源,比如css,js等文件,这个SpringBoot怎么处理呢?
方法1:导入已有资源
SpringBoot 中,SpringMVC 的 web 配置都在 WebMvcAutoConfiguration
这个配置类里面;
我们可以去看看 WebMvcAutoConfigurationAdapter
中有很多配置方法;
有一个方法:addResourceHandlers 添加资源处理,如下:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
// 已禁用默认资源处理
logger.debug("Default resource handling disabled");
return;
}
// 缓存控制
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
// webjars 配置
if (!registry.hasMappingForPattern("/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));
}
}
读一下源代码可以得到下面两个信息
-
如果将
spring:resources:add-mappings:
置为 false 那么就能够进制掉后面的静态资源规则:spring: resources: add-mappings: false #禁用所有静态资源规则
-
所有的 /webjars/** , 都需要去 classpath:/META-INF/resources/webjars/ 找对应的资源;
什么是webjars 呢?
Webjars本质就是以jar包的方式引入我们的静态资源 , 我们以前要导入一个静态资源文件,直接导入即可。现在使用 springboot时,我们需要使用Webjars 来导入资源。可以点击这里 访问 webjar的官方网站,如下:
可以看到webjar中保存了大部分常用的 静态资源的jar 包。我们只需要使用 maven 直接引入即可:
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.4.1</version>
</dependency>
导入完毕,查看 webjars 目录结构,并访问 Jquery.js 文件!
访问:只要是静态资源,SpringBoot就会去对应的路径寻找资源,我们这里访问:http://localhost:8080/webjars/jquery/3.4.1/jquery.js
方法2:导入自定义资源
还是从 addResourceHandlers 中查看,可以发现还有一种 getstaticPathPattern 的方法,进行静态资源的导入。
点开这个方法,代码如下:
// 进入方法
public String[] getStaticLocations() {
return this.staticLocations;
}
// 找到对应的值
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
// 找到路径
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/META-INF/resources/",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
};
所以得出结论,以下四个目录存放的静态资源可以被我们识别:
"classpath:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"
我们可以在resources根目录下新建对应的文件夹,都可以存放我们的静态文件。这些文件夹的存在的优先级如下:
说明优先级: resources>static>public
自定义静态资源路径: 当然处理上面四种路径,还可以通过配置文件来指定一下,哪些文件夹是需要我们放静态资源文件的,在application.properties中配置;
spring.resources.static-locations=classpath:/coding/,classpath:/kuang/
静态资源访问前缀: 如果需要给所有静态访问添加前缀:
spring:
mvc:
static-path-pattern: /res/**
默认首页路径的处理规则
静态资源文件夹说完后,继续向下看源码!可以看到一个欢迎页的映射,就是我们的首页
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService,
ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(), // getWelcomePage 获得欢迎页
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
return welcomePageHandlerMapping;
}
点进去继续看
private Optional<Resource> getWelcomePage() {
String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
// ::是java8 中新引入的运算符
// Class::function的时候function是属于Class的,应该是静态方法。
// this::function的funtion是属于这个对象的。
// 简而言之,就是一种语法糖而已,是一种简写
return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
}
// 欢迎页就是一个location下的的 index.html 而已
private Resource getIndexHtml(String location) {
return this.resourceLoader.getResource(location + "index.html");
}
欢迎页,静态资源文件夹下的所有 index.html 页面;被 /** 映射。
比如我访问 http://localhost:8080/ ,就会找静态资源文件夹下的 index.html
4、Request 映射-【使用方法】
使用
- 注解如下 的格式为
@xxxMapping;
- @GetMapping
- @PostMapping
- @PutMapping
- @DeleteMapping
- Rest的 HTTP 请求方式
-
以前:
- /getUser 获取用户
- /deleteUser 删除用户
- /editUser 修改用户
- /saveUser保存用户
-
现在: /user
- GET-获取用户
- DELETE-删除用户
- PUT-修改用户
- POST-保存用户
-
- 核心Filter;HiddenHttpMethodFilter
具体实例:
-
开启页面表单的Rest功能
spring: mvc: hiddenmethod: filter: enabled: true #开启页面表单的Rest功能
-
页面 form的属性method=post,隐藏域 _method=put、delete等(如果直接get或post,无需隐藏域)
<form action="/user" method="get"> <input value="REST-GET提交" type="submit" /> </form> <form action="/user" method="post"> <input value="REST-POST提交" type="submit" /> </form> <form action="/user" method="post"> <input name="_method" type="hidden" value="DELETE"/> <input value="REST-DELETE 提交" type="submit"/> </form> <form action="/user" method="post"> <input name="_method" type="hidden" value="PUT" /> <input value="REST-PUT提交"type="submit" /> <form>
@GetMapping("/user") //@RequestMapping(value = "/user",method = RequestMethod.GET) public String getUser(){ return "GET-张三"; } @PostMapping("/user") //@RequestMapping(value = "/user",method = RequestMethod.POST) public String saveUser(){ return "POST-张三"; } @PutMapping("/user") //@RequestMapping(value = "/user",method = RequestMethod.PUT) public String putUser(){ return "PUT-张三"; } @DeleteMapping("/user") //@RequestMapping(value = "/user",method = RequestMethod.DELETE) public String deleteUser(){ return "DELETE-张三"; }
原理
Rest原理: 表单提交要使用REST的时候,请求过来被 HiddenHttpMethodFilter
拦截,拦截器将把提交方式赋值给 _method
参数。
public class HiddenHttpMethodFilter extends OncePerRequestFilter {
private static final List<String> ALLOWED_METHODS =
Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),
HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
/** Default method parameter: {@code _method}. */
public static final String DEFAULT_METHOD_PARAM = "_method";
private String methodParam = DEFAULT_METHOD_PARAM;
/**
* Set the parameter name to look for HTTP methods.
* @see #DEFAULT_METHOD_PARAM
*/
public void setMethodParam(String methodParam) {
Assert.hasText(methodParam, "'methodParam' must not be empty");
this.methodParam = methodParam;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//1、拿到请求
HttpServletRequest requestToUse = request;
//2、判断表单是否为 post,说明只有post才能用REST风格
if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
//3、获取 _method 的值
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
//4、所有的请求字符变为大写(所以小写也可以)
String method = paramValue.toUpperCase(Locale.ENGLISH);
//判断允许的请求方式中是否包含method 请求
//点进去可以看到,兼容:PUT.DELETE.PATCH
if (ALLOWED_METHODS.contains(method)) {
requestToUse = new HttpMethodRequestWrapper(request, method);
}
}
}
filterChain.doFilter(requestToUse, response);
}
/**
* Simple {@link HttpServletRequest} wrapper that returns the supplied method for
* {@link HttpServletRequest#getMethod()}.
*/
//原生 request 使用的为 post,这里使用包装模式,重写了getMethod方法,返回传入的值。
//过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。
private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
private final String method;
public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
super(request);
this.method = method;
}
@Override
public String getMethod() {
return this.method;
}
}
}
因此,在前端提交REST格式的请求还可以用post方法,并且传递参数 _method
。如下:
原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值。
过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。
改变默认的_method
-
首先查看
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 }) public class WebMvcAutoConfiguration { ... @Bean @ConditionalOnMissingBean(HiddenHttpMethodFilter.class) @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false) public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { return new OrderedHiddenHttpMethodFilter(); } ... }
-
其中有一句
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
表示没有HiddenHttpMethodFilter时,才执行hiddenHttpMethodFilter()
。因此,我们可以自定义filter,改变默认的_method
。 -
举例,此时我们将默认的 “_method” 属性改为"_m",如下:
@Configuration(proxyBeanMethods = false) public class WebConfig{ //自定义filter @Bean public HiddenHttpMethodFilter hiddenHttpMethodFilter(){ HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter(); methodFilter.setMethodParam("_m"); return methodFilter; } }
<form action="/user" method="post"> <input name="_m" type="hidden" value="DELETE"/> <input value="REST-DELETE 提交" type="submit"/> </form>
总结
在面对Rest格式的请求时的具体步骤:
- 首先 Rest 请求的基本原理肯定是存在一个过滤器,对发送过来的请求进程处理。而这个过滤器默认关闭,只有通过
spring:mvc: hiddenmethod: filter:enabled: true
来开启页面表单的Rest功能 - 浏览器发送一个请求,到达过滤器后,过滤器会判断该请求是否为 POST,浏览器只会对POST请求的request进行包装。
- 如果是POST请求,就需要查看,其中的 “_method” 参数,该参数决定了 是PUT,DELETE 还是 PATCH。
- 在得到“_method” 后会进行统一的大写转换。
- 然后利用包装模式,加入对应的请求格式。
5、Request 映射【源码分析】
接下来我们探索一下,发送一个请求,如何映射到指定的controller位置的。
-
Servlet 中的 doGet 的调用顺序如下:
-
换句话说,doGet 最终调用的是 doDispatch() 。SpringMVC功能分析都从
org.springframework.web.servlet.DispatcherServlet -> doDispatch()
开始:protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // 找到当前请求使用哪个Handler(Controller的方法)处理 mappedHandler = getHandler(processedRequest); //HandlerMapping:处理器映射。/xxx->>xxxx ... }
-
getHandler()方法如下:
@Nullable protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { if (this.handlerMappings != null) { for (HandlerMapping mapping : this.handlerMappings) { HandlerExecutionChain handler = mapping.getHandler(request); if (handler != null) { return handler; } } } return null; }
-
this.handlerMappings
在Debug模式下展现的内容:
-
其中的
RequestMappingHandlerMapping
保存了所有@RequestMapping 和handler的映射规则。
总结:
- 在发送一个请求时,请求会通过Servlet进行接收。Servlet会调用其子类
spring.web.servlet.Dispatcher
的doDispatch()
方法。 - 在这个方法中,会根据请求寻找相应的handler(即controller)
- 而这个controller存储在一个叫做 RequestMappingHandlerMapping 的map中。该map以请求为key,以controller为value。只要直接 get 就能够获得。
IDEA快捷键:查看继承类,查看层次结构
- Ctrl + Alt + U : 以UML的类图展现类有哪些继承类,派生类以及实现哪些接口。
- Crtl + Alt + Shift + U : 同上,区别在于上条快捷键结果在新页展现,而本条快捷键结果在弹窗展现。
- Ctrl + H : 以树形方式展现类层次结构图。
6、常用参数注解使用
- @PathVariable 路径变量
- @RequestHeader 获取请求头
- @RequestParam 获取请求参数(指问号后的参数,url?a=1&b=2)
- @CookieValue 获取Cookie值
- @RequestAttribute 获取request域属性
- @RequestBody 获取请求体[POST]
- @MatrixVariable 矩阵变量
例子1:获取变量,请求参数,请求头,cookie
@RestController
public class ParameterTestController {
// car/2/owner/zhangsan
@GetMapping("/car/{id}/owner/{username}")
public Map<String,Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String name,
@PathVariable Map<String,String> pv,
@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String,String> header,
@RequestParam("age") Integer age,
@RequestParam("inters") List<String> inters,
@RequestParam Map<String,String> params,
@CookieValue("_ga") String _ga,
@CookieValue("_ga") Cookie cookie){
Map<String,Object> map = new HashMap<>();
// map.put("id",id);
// map.put("name",name);
// map.put("pv",pv);
// map.put("userAgent",userAgent);
// map.put("headers",header);
map.put("age",age);
map.put("inters",inters);
map.put("params",params);
map.put("_ga",_ga);
System.out.println(cookie.getName()+"===>"+cookie.getValue());
return map;
}
@PostMapping("/save")
public Map postMethod(@RequestBody String content){
Map<String,Object> map = new HashMap<>();
map.put("content",content);
return map;
}
}
例子2:获取 request 域,请求体
@Controller
public class RequestController {
@GetMapping("/goto")
public String goToPage(HttpServletRequest request){
request.setAttribute("msg","成功了...");
request.setAttribute("code",200);
return "forward:/success"; //转发到 /success请求
}
@GetMapping("/params")
public String testParam(Map<String,Object> map,
Model model,
HttpServletRequest request,
HttpServletResponse response){
map.put("hello","world666");
model.addAttribute("world","hello666");
request.setAttribute("message","HelloWorld");
Cookie cookie = new Cookie("c1","v1");
response.addCookie(cookie);
return "forward:/success";
}
///<-----------------主角@RequestAttribute在这个方法
@ResponseBody
@GetMapping("/success")
public Map success(@RequestAttribute(value = "msg",required = false) String msg,
@RequestAttribute(value = "code",required = false)Integer code,
HttpServletRequest request){
Object msg1 = request.getAttribute("msg");
Map<String,Object> map = new HashMap<>();
Object hello = request.getAttribute("hello");
Object world = request.getAttribute("world");
Object message = request.getAttribute("message");
map.put("reqMethod_msg",msg1);
map.put("annotation_msg",msg);
map.put("hello",hello);
map.put("world",world);
map.put("message",message);
return map;
}
}
例子3:获取矩阵变量
-
矩阵变量: 在 url中用
;
分隔的这些变量的集合。如:/cars/sell;low=34;brand=byd,audi,yd
。当cookie 被禁用时,由于session的sessionid存储在cookie中,因此无法进行获取。这是就可以将sessionID用矩阵变量的形式,利用url进行传递。 -
具体语法:
/cars/sell;low=34;brand=byd,audi,yd
。
其中 sell 表示该矩阵变量。后面的每个等于好表示一个key和一个value。一个key 可以对应一个value数组。 -
SpringBoot默认是禁用了矩阵变量的功能。基本原理是所有url的处理其实都是交给
UrlPathHelper
进行处理的。UrlPathHelper 其中一个方法为removeSemicolonContent
,该方法翻译过来是是否删除分号后面的所有字符,默认为 True。也就是不支持 矩阵变量。 -
由于默认的MVC配置使用的默认的 UrlPathHelper,因此为了能够让其支持矩阵变量,我能就必须自定义一个MVC配置。这里就需要用到 定制SpringMVC配置文件了。自定义规则的方式,官方文档给了三种:
-
手动开启矩阵变量方法1: 实现WebMvcConfigurer接口,利用
proxyBeanMethods = false
关闭默认的MVC自动配置器。然后重写里面的configurePathMatch方法,来自定义url解析方式。@Configuration(proxyBeanMethods = false) public class WebConfig implements WebMvcConfigurer { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); // 不移除;后面的内容。矩阵变量功能就可以生效 urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } }
-
手动开启矩阵变量方法2: 直接创建一个 bean:WebMvcConfigurer。
@Configuration(proxyBeanMethods = false) public class WebConfig{ @Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); // 不移除;后面的内容。矩阵变量功能就可以生效 urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } } } }
-
此时测试 @MatrixVariable
@RestController public class ParameterTestController { ///cars/sell;low=34;brand=byd,audi,yd @GetMapping("/cars/{path}") public Map carsSell(@MatrixVariable("low") Integer low, @MatrixVariable("brand") List<String> brand, @PathVariable("path") String path){ Map<String,Object> map = new HashMap<>(); map.put("low",low); map.put("brand",brand); map.put("path",path); return map; } // /boss/1;age=20/2;age=10 @GetMapping("/boss/{bossId}/{empId}") public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge, @MatrixVariable(value = "age",pathVar = "empId") Integer empAge){ Map<String,Object> map = new HashMap<>(); map.put("bossAge",bossAge); map.put("empAge",empAge); return map; } }
7、参数解析的过程【源码解析】
①、参数解析的流程
所有的请求从 DispatcherServlet
开始:
public class DispatcherServlet extends FrameworkServlet {
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
// 为当前 Handler 找一个适配器 `HandlerAdapter`,用的最多的是RequestMappingHandlerAdapter。
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
...
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
方法处理总结:
-
1、HandlerMapping 中找到能处理请求的
Handler(Controller.method())
。 -
2、为当前 Handler 找一个适配器
HandlerAdapter
,用的最多的是RequestMappingHandlerAdapter。 -
3、拿 RequestMappingHandlerAdapter 适配器举例,如果此时为 RequestMappingHandlerAdapter 后,会调用该适配器的 handle 方法。
-
4、handle 方法会调用
invokeHandlerMethod
-
5、
invokeHandlerMethod
首先会将 26 个参数解析器,14个返回值处理器都会被封装到 ServletInvocableHandlerMethod 中。
-
6、封装好后。
invokeHandlerMethod
方法会调用invokeForRequest
方法。该方法才是真正执行对应 controller 代码的目標方法。
-
7、由于需要真正执行目标方法,那么肯定需要开始解析参数。我们点入
invokeForRequest
方法。
在invokeForRequest
中主要有两个方法。- getMethodArgument 用于获取方法的参数值。该方法后,arg 结果如下:
- doInvoke 是真正使用反射来运行目标方法。
- getMethodArgument 用于获取方法的参数值。该方法后,arg 结果如下:
-
8、getMethodArgumentValues 方法确定了具体的每个参数的值。getMethodArgumentValues 的具体代码如下:
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { ... @Nullable//InvocableHandlerMethod类的,ServletInvocableHandlerMethod类继承InvocableHandlerMethod类 public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { 获取方法的参数值 Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); ... return doInvoke(args); } //本节重点,获取方法的参数值 protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { MethodParameter[] parameters = getMethodParameters(); if (ObjectUtils.isEmpty(parameters)) { return EMPTY_ARGS; } 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) { continue; } //查看resolvers是否有支持 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 ex) { .... } } return args; } }
可以看到,getMethodArgumentValues 遍历每一参数,判断当前参数是否存在对应的解析器(support 方法),如果存在则进行解析(resolveArgument方法)。
-
this.resolvers. 就是参数解析器(该解析类型为
HandlerMethodArgumentResolverComposite
),后面的参数解析器章节会进行详解。
②、参数解析器
invokeHandlerMethod 方法中存在下面两个判断,分别为设置参数解析器和设置返回值处理器。
参数解析器概述
参数解析器 一共有26个,用来确定目标方法的参数的具体值是什么。
参数解析器的接口为 HandleMethodArgumentResolver ,该接口主要有下面两个方法:
- support:首先该参数解析器会判断该解析器是否支持该参数的解析。
- resolve:如果支持,就会利用该函数进行解析
源码
-
this.resolvers 的具体类别为实例 resolver 接口的HandlerMethodArgumentResolverComposite类。通过
invokeHandlerMethod
中的invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
进行设置。 -
可以看到 solver 就是一个list,所有的参数解析器都会被加入到这个solvers里面
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { @Nullable private HandlerMethodArgumentResolverComposite argumentResolvers; @Override public void afterPropertiesSet() { ... if (this.argumentResolvers == null) {//初始化argumentResolvers List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers(); this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); } ... } //初始化了一堆的实现HandlerMethodArgumentResolver接口的 private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() { List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(30); // Annotation-based argument resolution resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false)); resolvers.add(new RequestParamMapMethodArgumentResolver()); resolvers.add(new PathVariableMethodArgumentResolver()); resolvers.add(new PathVariableMapMethodArgumentResolver()); resolvers.add(new MatrixVariableMethodArgumentResolver()); resolvers.add(new MatrixVariableMapMethodArgumentResolver()); resolvers.add(new ServletModelAttributeMethodProcessor(false)); resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory())); resolvers.add(new RequestHeaderMapMethodArgumentResolver()); resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory())); resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory())); resolvers.add(new SessionAttributeMethodArgumentResolver()); resolvers.add(new RequestAttributeMethodArgumentResolver()); // Type-based argument resolution resolvers.add(new ServletRequestMethodArgumentResolver()); resolvers.add(new ServletResponseMethodArgumentResolver()); resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RedirectAttributesMethodArgumentResolver()); resolvers.add(new ModelMethodProcessor()); resolvers.add(new MapMethodProcessor()); resolvers.add(new ErrorsMethodArgumentResolver()); resolvers.add(new SessionStatusMethodArgumentResolver()); resolvers.add(new UriComponentsBuilderMethodArgumentResolver()); if (KotlinDetector.isKotlinPresent()) { resolvers.add(new ContinuationHandlerMethodArgumentResolver()); } // Custom arguments if (getCustomArgumentResolvers() != null) { resolvers.addAll(getCustomArgumentResolvers()); } // Catch-all resolvers.add(new PrincipalMethodArgumentResolver()); resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true)); resolvers.add(new ServletModelAttributeMethodProcessor(true)); return resolvers; } }
-
HandlerMethodArgumentResolverComposite类如下:(众多参数解析器argumentResolvers的包装类)。
public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver { private final List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>(); ... public HandlerMethodArgumentResolverComposite addResolvers( @Nullable HandlerMethodArgumentResolver... resolvers) { if (resolvers != null) { Collections.addAll(this.argumentResolvers, resolvers); } return this; } ... }
-
我们看看HandlerMethodArgumentResolver的源码:
public interface HandlerMethodArgumentResolver { //当前解析器是否支持解析这种参数 boolean supportsParameter(MethodParameter parameter); @Nullable//如果支持,就调用 resolveArgument Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; }
③、返回值处理器
返回值处理器概述
返回值处理器:一共14中返回值处理器。
this.argumentResolvers
在 afterPropertiesSet()方法内初始化
源码
代码:和解析器的添加方法类似
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
@Nullable
private HandlerMethodReturnValueHandlerComposite returnValueHandlers;
@Override
public void afterPropertiesSet() {
...
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}
//初始化了一堆的实现HandlerMethodReturnValueHandler接口的
private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(20);
// Single-purpose return value types
handlers.add(new ModelAndViewMethodReturnValueHandler());
handlers.add(new ModelMethodProcessor());
handlers.add(new ViewMethodReturnValueHandler());
handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters(),
this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager));
handlers.add(new StreamingResponseBodyReturnValueHandler());
handlers.add(new HttpEntityMethodProcessor(getMessageConverters(),
this.contentNegotiationManager, this.requestResponseBodyAdvice));
handlers.add(new HttpHeadersReturnValueHandler());
handlers.add(new CallableMethodReturnValueHandler());
handlers.add(new DeferredResultMethodReturnValueHandler());
handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));
// Annotation-based return value types
handlers.add(new ServletModelAttributeMethodProcessor(false));
handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),
this.contentNegotiationManager, this.requestResponseBodyAdvice));
// Multi-purpose return value types
handlers.add(new ViewNameMethodReturnValueHandler());
handlers.add(new MapMethodProcessor());
// Custom return value types
if (getCustomReturnValueHandlers() != null) {
handlers.addAll(getCustomReturnValueHandlers());
}
// Catch-all
if (!CollectionUtils.isEmpty(getModelAndViewResolvers())) {
handlers.add(new ModelAndViewResolverMethodReturnValueHandler(getModelAndViewResolvers()));
}
else {
handlers.add(new ServletModelAttributeMethodProcessor(true));
}
return handlers;
}
}
8、请求处理- Servlet参数解析器【案例1】
servlet的解析器为:ServletRequestMethodArgumentResolver
该解析器可以下面的类:WebRequest
- ServletRequest
- MultipartRequest
- HttpSession
- javax.servlet.http.PushBuilder
- Principal
- InputStream
- Reader
- HttpMethod
- Locale
- TimeZone
- ZoneId
解析器的具体函数如下:
public class ServletRequestMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Nullable
private static Class<?> pushBuilder;
static {
try {
pushBuilder = ClassUtils.forName("javax.servlet.http.PushBuilder",
ServletRequestMethodArgumentResolver.class.getClassLoader());
}
catch (ClassNotFoundException ex) {
// Servlet 4.0 PushBuilder not found - not supported for injection
pushBuilder = null;
}
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return (WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
(Principal.class.isAssignableFrom(paramType) && !parameter.hasParameterAnnotations()) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Class<?> paramType = parameter.getParameterType();
// WebRequest / NativeWebRequest / ServletWebRequest
if (WebRequest.class.isAssignableFrom(paramType)) {
if (!paramType.isInstance(webRequest)) {
throw new IllegalStateException(
"Current request is not of type [" + paramType.getName() + "]: " + webRequest);
}
return webRequest;
}
// ServletRequest / HttpServletRequest / MultipartRequest / MultipartHttpServletRequest
if (ServletRequest.class.isAssignableFrom(paramType) || MultipartRequest.class.isAssignableFrom(paramType)) {
return resolveNativeRequest(webRequest, paramType);
}
// HttpServletRequest required for all further argument types
return resolveArgument(paramType, resolveNativeRequest(webRequest, HttpServletRequest.class));
}
private <T> T resolveNativeRequest(NativeWebRequest webRequest, Class<T> requiredType) {
T nativeRequest = webRequest.getNativeRequest(requiredType);
if (nativeRequest == null) {
throw new IllegalStateException(
"Current request is not of type [" + requiredType.getName() + "]: " + webRequest);
}
return nativeRequest;
}
@Nullable
private Object resolveArgument(Class<?> paramType, HttpServletRequest request) throws IOException {
if (HttpSession.class.isAssignableFrom(paramType)) {
HttpSession session = request.getSession();
if (session != null && !paramType.isInstance(session)) {
throw new IllegalStateException(
"Current session is not of type [" + paramType.getName() + "]: " + session);
}
return session;
}
else if (pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) {
return PushBuilderDelegate.resolvePushBuilder(request, paramType);
}
else if (InputStream.class.isAssignableFrom(paramType)) {
InputStream inputStream = request.getInputStream();
if (inputStream != null && !paramType.isInstance(inputStream)) {
throw new IllegalStateException(
"Request input stream is not of type [" + paramType.getName() + "]: " + inputStream);
}
return inputStream;
}
else if (Reader.class.isAssignableFrom(paramType)) {
Reader reader = request.getReader();
if (reader != null && !paramType.isInstance(reader)) {
throw new IllegalStateException(
"Request body reader is not of type [" + paramType.getName() + "]: " + reader);
}
return reader;
}
else if (Principal.class.isAssignableFrom(paramType)) {
Principal userPrincipal = request.getUserPrincipal();
if (userPrincipal != null && !paramType.isInstance(userPrincipal)) {
throw new IllegalStateException(
"Current user principal is not of type [" + paramType.getName() + "]: " + userPrincipal);
}
return userPrincipal;
}
else if (HttpMethod.class == paramType) {
return HttpMethod.resolve(request.getMethod());
}
else if (Locale.class == paramType) {
return RequestContextUtils.getLocale(request);
}
else if (TimeZone.class == paramType) {
TimeZone timeZone = RequestContextUtils.getTimeZone(request);
return (timeZone != null ? timeZone : TimeZone.getDefault());
}
else if (ZoneId.class == paramType) {
TimeZone timeZone = RequestContextUtils.getTimeZone(request);
return (timeZone != null ? timeZone.toZoneId() : ZoneId.systemDefault());
}
// Should never happen...
throw new UnsupportedOperationException("Unknown parameter type: " + paramType.getName());
}
/**
* Inner class to avoid a hard dependency on Servlet API 4.0 at runtime.
*/
private static class PushBuilderDelegate {
@Nullable
public static Object resolvePushBuilder(HttpServletRequest request, Class<?> paramType) {
PushBuilder pushBuilder = request.newPushBuilder();
if (pushBuilder != null && !paramType.isInstance(pushBuilder)) {
throw new IllegalStateException(
"Current push builder is not of type [" + paramType.getName() + "]: " + pushBuilder);
}
return pushBuilder;
}
}
}
9、请求处理- Model、Map原理【案例2】
对于一些复杂的参数:
-
Map
-
Model(map、model里面的数据会被放在request的请求域 request.setAttribute)
-
Errors/BindingResult
-
RedirectAttributes( 重定向携带数据)
-
ServletResponse(response)
-
SessionStatus
-
UriComponentsBuilder
-
ServletUriComponentsBuilder
-
无论是 model 还是 map 都使用的是 名为 modelMethodProcessor 解析器作为参数解析器。换句话说,只有这个解析器支持解析他们。
-
这个解析器底层使用的也就是 setAttribute方法。如下:
10、响应处理-ReturnValueHandler-【案例1】
对应的返回处理器的寻找方式和参数解析器类似
-
在
invokeAndHandle
函数中,在运行完所有controller后,数据会被返回到一个 Object 对象中 ,如下:public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); ... try { //看下块代码 //这段代码就是用于将返回的 Object 包装到对应的返回类型解析器中 this.returnValueHandlers.handleReturnValue( returnValue, getReturnValueType(returnValue), mavContainer, webRequest); } catch (Exception ex) { ... } }
以 ResponseBody 注解为例
- 该注解使用的返回值解析器为
RequestResponseBodyMethodProcessor
,它实现HandlerMethodReturnValueHandler接口
。 - 代码如下:
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { ... @Override public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { mavContainer.setRequestHandled(true); ServletServerHttpRequest inputMessage = createInputMessage(webRequest); ServletServerHttpResponse outputMessage = createOutputMessage(webRequest); // 使用消息转换器进行写出操作,本方法下一章节介绍: // Try even with null return value. ResponseBodyAdvice could get involved. writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage); } }
11、响应处理-视图解析器-【案例2】
ContentNegotiatingViewResolver
视图解析器为 ContentNegotiatingViewResolver 该解析器可以根据方法的返回值取得视图对象(View),然后由视图对象决定如何渲染(转发,重定向)。
-
我们去看看这里的源码:我们找到 WebMvcAutoConfiguration , 然后搜索ContentNegotiatingViewResolver。找到如下方法:
@Bean @ConditionalOnBean(ViewResolver.class) @ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class) public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) { ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver(); resolver.setContentNegotiationManager(beanFactory.getBean(ContentNegotiationManager.class)); // ContentNegotiatingViewResolver使用所有其他视图解析器来定位视图,因此它应该具有较高的优先级 resolver.setOrder(Ordered.HIGHEST_PRECEDENCE); return resolver; }
-
进入
ContentNegotiatingViewResolver
类,我们可以看到一个视图解析的方法(该方法的目的是希望通过viewName 和location 找到对应的view):@Nullable // 注解说明:@Nullable 即参数可为null public View resolveViewName(String viewName, Locale locale) throws Exception { RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes"); List<MediaType> requestedMediaTypes = this.getMediaTypes(((ServletRequestAttributes)attrs).getRequest()); if (requestedMediaTypes != null) { // 获取候选的视图对象 List<View> candidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes); // 选择一个最适合的视图对象,然后把这个对象返回 View bestView = this.getBestView(candidateViews, requestedMediaTypes, attrs); if (bestView != null) { return bestView; } } // ..... }
-
我们点击
getCandidateViews
,观察他是怎么获得候选的视图的呢?
getCandidateViews中看到他得到了存储所有视图解析器的集合的迭代器,然后进行while循环,挨个解析!
所以得出结论:ContentNegotiatingViewResolver 这个视图解析器(viewResolvers)就是用来组合所有的视图解析器的
-
我们再去研究下他的组合逻辑,看到有个属性viewResolvers,看看它是在哪里进行赋值的!
protected void initServletContext(ServletContext servletContext) { // 这里它是从beanFactory工具中获取容器中的所有视图解析器 // ViewRescolver.class 把所有的视图解析器来组合的 Collection<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors( this.obtainApplicationContext(),ViewResolver.class). values(); ViewResolver viewResolver; if (this.viewResolvers == null) { this.viewResolvers = new ArrayList(matchingBeans.size()); Iterator var3 = matchingBeans.iterator(); while(var3.hasNext()) { viewResolver = (ViewResolver)var3.next(); if (this != viewResolver) { this.viewResolvers.add(viewResolver); } } } else { for(int i = 0; i < this.viewResolvers.size(); ++i) { viewResolver = (ViewResolver)this.viewResolvers.get(i); if (!matchingBeans.contains(viewResolver)) { String name = viewResolver.getClass().getName() + i; this.obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(viewResolver, name); } } } AnnotationAwareOrderComparator.sort(this.viewResolvers); this.cnmFactoryBean.setServletContext(servletContext); }
可以看到,viewResolvers 初始化的逻辑为:
- 从 BeanFactoryUtils.beansOfTypeIncludingAncestors 方法中获取ViewResolver 列表,该方法是遍历了整个spring容器,来获得了所有的
ViewResolver.class
类文件 - 来获取ViewResolver列表后,赋值给viewResolvers;
- 从 BeanFactoryUtils.beansOfTypeIncludingAncestors 方法中获取ViewResolver 列表,该方法是遍历了整个spring容器,来获得了所有的
通过上面的溯源,我们可以发现 ContentNegotiatingViewResolver会遍历整个spring容器,获得所有解析器。然后从这些解析器中返回需要的。因此我们可以自定义解析器,只要继承了 viewResolve 就会被自动配置。
自定义 视图解析器
-
1、我们在我们的主程序中去写一个视图解析器来试试;
// 如果 想DIY一些定制化的功能,只要写这个组件,然后将它交给springboot,springboot就会帮我们自动装配 //扩展springmvc @Configuration public class MyMvcConfig implements WebMvcConfigurer { //ViewResolver 实现类视图解析器接口的类,我们就可以把它看做视图解析器 @Bean public ViewResolver myViewResolver(){ return new MyViewResolver(); } //我们写一个静态内部类,视图解析器就需要实现ViewResolver接口 public static class MyViewResolver implements ViewResolver { @Override public View resolveViewName(String s, Locale locale) throws Exception { return null; } } }
-
2、怎么看我们自己写的视图解析器有没有起作用呢?
我们给 DispatcherServlet 中的 doDispatch方法 加个断点进行调试一下,因为所有的请求都会走到这个方法中
-
3、我们启动我们的项目,然后随便访问一个页面,看一下Debug信息;找到this
-
4、找到视图解析器,我们看到我们自己定义的就在这里了;
12、更改转换器和格式化器
在springboot项目里面,我们导入web的依赖,就有了springmvc,里面有人家已经封装的springmvc的配置,现在我们想要自定义自己的配置,比如格式化转换器。
日期的格式,比如2020-2-2 和 2020/2/2
我们使用哪个?源码里面默认的是哪个?
- 既然要看源码,那么找spring.factoryies 文件,找到关于springmvc的类:
- 可以看到 其中有一个 mvcProperties 的变量。找到定义位置:
- 点击进去
- 这样就看到关于springmvc里面的配置了。我们在这个里面找和格式化相关的配置
可以看到默认就是 dd/MM/yyyy - 我们可以直接通过yml 重新配置date格式如下:
- 同样,我们也可以使用 properties 来完成这件事:
添加额外的MVC配置
这么多的自动配置,原理都是一样的,通过这个 WebMVC 的自动配置原理分析,我们要学会一种学习方式,通过源码探究,得出结论;这个结论一定是属于自己的,而且一通百通。
SpringBoot的底层,大量用到了这些设计细节思想,所以,没事需要多阅读源码!得出结论;
- SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置的(如果用户自己配置@bean),如果有就用用户配置的,如果没有就用自动配置的;
- 如果有些组件可以存在多个,比如我们的视图解析器,就将用户配置的和自己默认的组合起来!
官方文档如下:
If you want to keep Spring Boot MVC features and you want to add additional MVC configuration (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.
我们要做的就是编写一个@Configuration注解类,并且 类型要为WebMvcConfigurer,还不能标注@EnableWebMvc注解; 我们去自己写一个;我们新建一个包叫config,写一个类MyMvcConfig;
//应为类型要求为WebMvcConfigurer,所以我们实现其接口
//可以使用自定义类扩展MVC的功能
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 浏览器发送/test , 就会跳转到test页面;
registry.addViewController("/test").setViewName("test");
}
}
我们去浏览器访问一下:
我们可以去分析一下原理:
-
1、WebMvcAutoConfiguration 是 SpringMVC的自动配置类,里面有一个类WebMvcAutoConfigurationAdapter
-
2、这个类上有一个注解,在做其他自动配置时会导入:@Import(EnableWebMvcConfiguration.class)
-
3、我们点进EnableWebMvcConfiguration这个类看一下,它继承了一个父类:DelegatingWebMvcConfiguration
这个父类中有这样一段代码:public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport { private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite(); // 从容器中获取所有的webmvcConfigurer @Autowired(required = false) public void setConfigurers(List<WebMvcConfigurer> configurers) { if (!CollectionUtils.isEmpty(configurers)) { this.configurers.addWebMvcConfigurers(configurers); } } }
-
4、我们可以在这个类中去寻找一个我们刚才设置的viewController 当做参考,发现它调用了一个
protected void addViewControllers(ViewControllerRegistry registry) { this.configurers.addViewControllers(registry); }
-
5、我们点进去看一下
public void addViewControllers(ViewControllerRegistry registry) { Iterator var2 = this.delegates.iterator(); while(var2.hasNext()) { // 将所有的WebMvcConfigurer相关配置来一起调用!包括我们自己配置的和Spring给我们配置的 WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next(); delegate.addViewControllers(registry); } }
所以得出结论: 所有的 WebMvcConfiguration 都会被作用,不止Spring自己的配置类,我们自己的配置类当然也会被调用;
13、 全面接管SpringMVC
官方文档:
If you want to take complete control of Spring MVCyou can add your own @Configuration annotated with @EnableWebMvc.
全面接管即:SpringBoot对SpringMVC的自动配置不需要了,所有都是我们自己去配置!
只需在我们的配置类中要加一个@EnableWebMvc。
- 我们看下如果我们全面接管了SpringMVC了,我们之前SpringBoot给我们配置的静态资源映射一定会无效,我们可以去测试一下;
不加注解之前,访问首页:
给配置类加上注解:@EnableWebMvc
我们发现所有的SpringMVC自动配置都失效了!回归到了最初的样子;
源码分析:
1、这里发现它是导入了一个类,我们可以继续进去看
@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}
2、它继承了一个父类 WebMvcConfigurationSupport
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
// ......
}
3、我们来回顾一下Webmvc自动配置类
@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 })
public class WebMvcAutoConfiguration {
}
总结: 由于 自动配置类中,有个条件注解,只有没有bean 时才会注入 WebMvcConfigurationSupport。但是如果此时,添加了注解 @EnableWebMvc 相当于增加了 bean。那么自动配置里面就不会去加载那些扩展类。
关于网站图标说明:
与其他静态资源一样,Spring Boot在配置的静态内容位置中查找 favicon.ico。如果存在这样的文件,它将自动用作应用程序的favicon。
-
1、关闭SpringBoot默认图标
#关闭默认图标 spring.mvc.favicon.enabled=false
-
2、自己放一个图标在静态资源目录下,我放在 public 目录下
-
3、清除浏览器缓存!刷新网页,发现图标已经变成自己的了!