第十篇:基于注解配置 SpringMVC
第十篇:基于注解配置 SpringMVC
参考文档
实践
配置方式,看《SpringMVC- 第一篇:搭建简单的 SpringMVC 应用.md》中 基于注解配置
这一部分即可。
配置方式
主要是两种配置 SpringMVC 的方式,或者说两个方面,一种是注册 bean,一种是自定义 SpringMVC 的结构。
其实这两种方式在 xml 的配置方式中也有体现,注册 bean 的方式就是添加 <bean>
标签,另一种方式是添加 mvc:
开头的标签即 mvc 命名空间下的标签,比如 <mvc:annotation-driven>
、<mvc:interceptors>
。
这两种方式在用 Java 配置 SpringMVC 的时候也有体现,@Bean
注解方法,和重写 WebMvcConfigurer
中的方法。
基于 xml 的配置的时候,mvc 命名空间下的标签通常对应着一个 org.springframework.web.servlet.config
包下的 Parser
类,而基于 Java 配置的时候,则经常使用 org.springframework.web.servlet.config
包下的 Configurer 类或 Registry 类。顾名思义,Parser
表示的是从 xml 配置中的解析,将配置信息字符串中转化到 Java 对象中。一般都是转化成 BeanDefinition
。Configurer
类通常用于汇总对某一个类或者某一种功能或者某一种业务的配置的管理 (修改和查找),说白了就是一个配置集中管理类。Registry
类则用于管理(注册、修改、删除、查找)一类组件,当组件类型较多时使用注册中心统一管理是一种非常有效的手段。
常见的 Parser
类:
-
AnnotationDrivenBeanDefinitionParser
-
CorsBeanDefinitionParser
-
DefaultServletHandlerBeanDefinitionParser
-
FreeMarkerConfigurerBeanDefinitionParser
-
GroovyMarkupConfigurerBeanDefinitionParser
-
InterceptorsBeanDefinitionParser
-
ResourcesBeanDefinitionParser
-
ScriptTemplateConfigurerBeanDefinitionParser
-
TilesConfigurerBeanDefinitionParser
-
ViewControllerBeanDefinitionParser
-
ViewResolversBeanDefinitionParser

每个 Parser 的具体的功能见《SpringMVC- 第九篇:基于 XML 配置 SpringMVC》
常见的 Configurer 类:
-
AsyncSupportConfigurer
异步请求处理的配置器 -
ContentNegotiationConfigurer
内容协商管理器ContentNegotiationManager
的配置器 -
DefaultServletHandlerConfigurer
Tomcat 默认的 servlet 请求处理的配置器 -
PathMatchConfigurer
集中路径匹配相关的配置,还没深入学习,TODO

常见的 Registry 类
-
CorsRegistry
管理CorsConfiguration
,跨域配置 -
InterceptorRegistry
管理HandlerInterceptor
,请求拦截器 -
ResourceHandlerRegistry
管理ResourceHttpRequestHandler
,处理对静态资源请求 -
ViewControllerRegistry
管理ParameterizableViewController
,当 url 对应的控制器方法无业务逻辑的时候,可直接在此添加 url 和视图的映射- 对应 xml 配置中的
<view-controller>
、<redirect-view-controller>
、<status-controller>
- 对应 xml 配置中的
-
ViewResolverRegistry
管理ViewResolver
比如ThymeleafViewResolver
SpringMVC 常用配置项
这里的每个方法,都应该看看背后的调用链,在具体使用方面,应该写点示例代码。
configureDefaultServletHandling
xml 的配置,回看《SpringMVC- 第二篇:控制器方法(handler)映射》的
默认请求处理器
调用栈
WebMvcConfigurationSupport#defaultServletHandlerMapping
是一个 @Bean
方法,创建的 Bean 的类型是 HandlerMapping
,Bean 的名称是 defaultServletHandlerMapping
具体细节,看后面的小节
WebMvcConfigurationSupport
中查看具体细节
@Bean
@Nullable
public HandlerMapping defaultServletHandlerMapping() {
Assert.state(this.servletContext != null, "No ServletContext set");
// 手动创建 DefaultServletHandlerConfigurer
DefaultServletHandlerConfigurer configurer = new DefaultServletHandlerConfigurer(this.servletContext);
// 调用所有Web配置类中的 configureDefaultServletHandling 方法,并传入同一个 configurer 对象
// 也就是说,在 所有Web配置类中的 configureDefaultServletHandling 方法中,configurer 参数具有传递性
configureDefaultServletHandling(configurer);
// 返回 HandlerMapping ,一般为 SimpleUrlHandlerMapping
return configurer.buildHandlerMapping();
}
方法体中通过 WebMvcConfigurationSupport#configureDefaultServletHandling
方法来进行自定义,这个方法是空的,意思是留给子类重写,WebMvcConfigurationSupport
只有两个子类,一个是 DelegatingWebMvcConfiguration
,我们直接看 DelegatingWebMvcConfiguration#configureDefaultServletHandling
。
另一个是
StandaloneConfiguration
,是StandaloneMockMvcBuilder
的内部类(从类名可以推断出是用于测试的 Mock 类),直接忽略
DelegatingWebMvcConfiguration#configureDefaultServletHandling
实际上委托给 DelegatingWebMvcConfiguration#configurers
,类型为 WebMvcConfigurerComposite
,包含了所有的 Web 配置类中的信息。
具体细节,看后面的小节
DelegatingWebMvcConfiguration
中查看具体细节
经过了所有 Web 配置类中的 configureDefaultServletHandling
方法的修改,最终调用 DefaultServletHandlerConfigurer#buildHandlerMapping
来返回处理器映射
DefaultServletHandlerConfigurer
DefaultServletHandlerConfigurer
源码分析
// 只有一个作用,为 handler 属性赋值
public void enable(@Nullable String defaultServletName) {
this.handler = new DefaultServletHttpRequestHandler();
if (defaultServletName != null) {
this.handler.setDefaultServletName(defaultServletName);
}
this.handler.setServletContext(this.servletContext);
}
// 此方法唯一的调用场景为 WebMvcConfigurationSupport 中的 defaultServletHandlerMapping 方法,
// defaultServletHandlerMapping方法为 @Bean 修饰的方法,将 buildHandlerMapping 的返回值当成一个 类型为 HandlerMapping 名为 defaultServletHandlerMapping 的 bean
@Nullable
protected SimpleUrlHandlerMapping buildHandlerMapping() {
// 调用了 enable() 方法,handler才不为空,否则,handler 就是 null ,为空
if (this.handler == null) {
return null;
}
// handler 不为空,才会创建 SimpleUrlHandlerMapping ,才会在处理器映射列表的最末尾添加上处理所有请求(映射路径为/**)的处理器映射
return new SimpleUrlHandlerMapping(Collections.singletonMap("/**", this.handler),
Ordered.LOWEST_PRECEDENCE);
}
DefaultServletHttpRequestHandler
实现了 HttpRequestHandler
接口,我们主要看 handleRequest
方法的实现,
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Assert.state(this.servletContext != null, "No ServletContext set");
RequestDispatcher rd = this.servletContext.getNamedDispatcher(this.defaultServletName);
if (rd == null) {
throw new IllegalStateException("A RequestDispatcher could not be located for the default servlet '" +
this.defaultServletName + "'");
}
// 逻辑很简单,直接转发到 默认的Servlet
rd.forward(request, response);
}
实践
//使用默认的servlet处理静态资源
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
addInterceptors
调用栈
WebMvcConfigurationSupport#getInterceptors
的作用是获取所有的拦截器,这个方法很简单
protected final Object[] getInterceptors( FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
if (this.interceptors == null) {
InterceptorRegistry registry = new InterceptorRegistry();
// 调用所有的 Web配置类(一般都实现了WebMvcConfigurer接口)中的 addInterceptors 方法
addInterceptors(registry);
// 添加默认的 Interceptor ,ConversionServiceExposingInterceptor 和 ResourceUrlProviderExposingInterceptor
// 默认添加的 ConversionServiceExposingInterceptor 和 ResourceUrlProviderExposingInterceptor 的 includePatterns 和 excludePatterns 都是空,而且order 为0 ,
// ConversionServiceExposingInterceptor 的功能主要是将 WebMvcConfigurationSupport#mvcConversionService 方法配置的 ConversionService 实例Bean添加到请求域的属性中,属性名为 ConversionService.class.getName() 具体名称为 org.springframework.core.convert.ConversionService
// ResourceUrlProviderExposingInterceptor 的功能就是将 WebMvcConfigurationSupport#mvcResourceUrlProvider 方法配置的 ResourceUrlProvider 实例ban添加到请求域属性中,属性名为 ResourceUrlProvider.class.getName(), 具体名称为 org.springframework.web.servlet.resource.ResourceUrlProvider
// 为了不跟默认的 HandlerInterceptor 在顺序上起冲突,所以 Web配置类在重写 addInterceptors 方法添加自定义的 Interceptor 的时候 order 最好是 从 1 开始
registry.addInterceptor(new ConversionServiceExposingInterceptor(mvcConversionService));
registry.addInterceptor(new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider));
this.interceptors = registry.getInterceptors();
}
return this.interceptors.toArray();
}
我们细看 WebMvcConfigurationSupport#addInterceptors
方法,可以注意到,在 WebMvcConfigurationSupport
中,这个方法是空的,意思是留给子类重写,WebMvcConfigurationSupport
只有两个子类,一个是 DelegatingWebMvcConfiguration
,我们直接看 DelegatingWebMvcConfiguration#addInterceptors
。
另一个是
StandaloneConfiguration
,是StandaloneMockMvcBuilder
的内部类(从类名可以推断出是用于测试的 Mock 类),直接忽略
DelegatingWebMvcConfiguration#addInterceptors
实际上委托给 DelegatingWebMvcConfiguration#configurers
,类型为 WebMvcConfigurerComposite
,包含了所有的 Web 配置类中的信息。
具体细节,看后面的小节
DelegatingWebMvcConfiguration
中查看具体细节
经过了所有 Web 配置类中的 addInterceptors
方法的修改,最终,在 WebMvcConfigurationSupport#getInterceptors
中调用 InterceptorRegistry#getInterceptors
来返回所有的拦截器。
WebMvcConfigurationSupport#getInterceptors
方法,在 WebMvcConfigurationSupport
方法中被多次调用,基本上只要是 WebMvcConfigurationSupport
中声明的 HandlerMapping
类型的 Bean,都需要配置配置 HandlerInterceptor
,都会调用 WebMvcConfigurationSupport#getInterceptors
方法,比如
-
WebMvcConfigurationSupport#requestMappingHandlerMapping
返回RequestMappingHandlerMapping
-
WebMvcConfigurationSupport#viewControllerHandlerMapping
返回HandlerMapping
-
WebMvcConfigurationSupport#beanNameHandlerMapping
返回BeanNameUrlHandlerMapping
-
WebMvcConfigurationSupport#routerFunctionMapping
返回RouterFunctionMapping
-
WebMvcConfigurationSupport#resourceHandlerMapping
返回HandlerMapping
只要是
HandlerMapping
类型的 Bean,都需要配置配置HandlerInterceptor
,这在《SpringMVC 控制器方法(handler)的映射 - HandlerMapping》中有过分析具体细节,看后面的小节
WebMvcConfigurationSupport
中查看具体细节
InterceptorRegistry
看完了调用栈,我们接下来分析 InterceptorRegistry
InterceptorRegistry
只有一个属性:List<InterceptorRegistration> registrations
,所有添加的 HandlerInterceptor
或者 WebRequestInterceptor
都会被封装为 InterceptorRegistration
,最终通过调用 InterceptorRegistry#getInterceptors
遍历所有的 InterceptorRegistration
按照顺序返回按需包装后的 HandlerInterceptor
和 WebRequestInterceptor
。
InterceptorRegistration
InterceptorRegistration
的功能是对 HandlerInterceptor
或者 WebRequestInterceptor
根据实际情况进行包装,主要看 InterceptorRegistration#getInterceptors
方法,这个方法只被 InterceptorRegistry#getInterceptors
调用。
InterceptorRegistration#getInterceptors
方法中并不是所有添加的拦截器都会包装成 MappedInterceptor
,如果没有设置匹配的路径模式(includePatterns 字段)或者排除的路径模式(excludePatterns 字段),那么说明就不需要进行映射路径的判断,就不需要包装成 MappedInterceptor
,直接返回即可。
比如 WebMvcConfigurationSupport#getInterceptors
中默认添加的 ConversionServiceExposingInterceptor
和 ResourceUrlProviderExposingInterceptor
的 includePatterns
字段和 excludePatterns
字段都是空,所以不会包装成 MappedInterceptor
,而且 order
为 0,会放在所有自定义 HandlerInterceptor
的前面,这需要所有的自定义的 HandlerInterceptor
的 order 都从 1 开始,所以,自定义的 HandlerInterceptor
的 Order 最好是从 1 开始。
public class InterceptorRegistration {
private final HandlerInterceptor interceptor;
// 映射的路径模式
@Nullable
private List<String> includePatterns;
// 排除的路径模式
@Nullable
private List<String> excludePatterns;
// 路径匹配器
@Nullable
private PathMatcher pathMatcher;
// 拦截器的顺序
private int order = 0;
// 构造器 属性的get、set方法
......
// 获取拦截器
protected Object getInterceptor() {
// 这个判断很妙,如果没有设置 匹配的路径模式或者排除的路径模式,那么说明就不需要进行映射路径的判断, 就不需要包装成 MappedInterceptor,直接返回即可
// 因此并不是所有添加的拦截器都会包装成 MappedInterceptor
// 默认添加的 ConversionServiceExposingInterceptor 和 ResourceUrlProviderExposingInterceptor 的 includePatterns 和 excludePatterns 都是空,而且order 为0 ,
// 所以不会包装成 MappedInterceptor,且放在所有自定义 HandlerInterceptor 的前面(这需要所有的自定义的HandlerInterceptor的order都从1开始)
if (this.includePatterns == null && this.excludePatterns == null) {
return this.interceptor;
}
MappedInterceptor mappedInterceptor = new MappedInterceptor(
StringUtils.toStringArray(this.includePatterns),
StringUtils.toStringArray(this.excludePatterns),
this.interceptor);
if (this.pathMatcher != null) {
mappedInterceptor.setPathMatcher(this.pathMatcher);
}
return mappedInterceptor;
}
}
实践
@Override
public void addInterceptors(InterceptorRegistry registry) {
MyInterceptor0 interceptor0 = new MyInterceptor0();
MyInterceptor1 interceptor1 = new MyInterceptor1();
MyInterceptor2 interceptor2 = new MyInterceptor2();
// 为了不跟默认的 HandlerInterceptor 在顺序上起冲突,所以 order 最好是 从 1 开始
registry.addInterceptor(interceptor0).addPathPatterns("/HandlerInterceptor/**").excludePathPatterns("/").order(1);
registry.addInterceptor(interceptor1).addPathPatterns("/HandlerInterceptor/**").excludePathPatterns("/").order(2);
registry.addInterceptor(interceptor2).addPathPatterns("/HandlerInterceptor/**").excludePathPatterns("/").order(3);
}
更多实践直接回去看《SpringMVC- 第二篇:控制器方法(handler)映射》的 HandlerInterceptor 章节,配置后使用的效果都是一样的
configurePathMatch
这个很核心。是个很重要的知识点,之前在学习 XML 配置的时候,没有接触过
因为其他的处理器映射(控制器方法映射)都需要用到这里的配置,这里的配置相当于是其他功能模块的底层,所以跟路径解析相关的配置工作都可以在这里做
按照惯例,先了解一下此方法大致的架构。
调用栈
WebMvcConfigurationSupport#getPathMatchConfigurer
方法,将配置工作委托给 configurePathMatch
方法,可以注意到,在 WebMvcConfigurationSupport
中,这个方法是空的,意思是留给子类重写。剩下的调用栈跟配置方法的调用栈大同小异,这里就省略了。
具体细节,看后面的小节
WebMvcConfigurationSupport
小节和DelegatingWebMvcConfiguration
小节中查看具体细节
经过了所有 Web 配置类中的 configurePathMatch
方法的修改,最终,在 WebMvcConfigurationSupport#getPathMatchConfigurer
中返回最终的 PathMatchConfigurer
对象。
WebMvcConfigurationSupport#getPathMatchConfigurer
方法,在 WebMvcConfigurationSupport
方法中被多次调用:
-
WebMvcConfigurationSupport#requestMappingHandlerMapping
,构造RequestMappingHandlerMapping
类型的 Bean -
WebMvcConfigurationSupport#mvcUrlPathHelper
,直接返回PathMatchConfigurer
中的urlPathHelper
,构造UrlPathHelper
类型的 Bean -
WebMvcConfigurationSupport#mvcPathMatcher
,直接返回PathMatchConfigurer
中的pathMatcher
,构造PathMatcher
类型的 Bean -
WebMvcConfigurationSupport#viewControllerHandlerMapping
构造HandlerMapping
类型的 Bean -
WebMvcConfigurationSupport#routerFunctionMapping
构造RouterFunctionMapping
类型的 Bean -
WebMvcConfigurationSupport#resourceHandlerMapping
构造HandlerMapping
类型的 Bean -
WebMvcConfigurationSupport#mvcResourceUrlProvider
构造ResourceUrlProvider
类型的 Bean
基础知识
查看《Web 应用服务器中对路径的处理.md》
PathMatchConfigurer - 路径配置信息的汇总
trailingSlashMatch 字段 - 弃用
看了 PathPatternParser
你就知道为什么要弃用了。
pathPrefixes 字段
仅限用于 HandlerMethod
类型的 handler 的路径匹配
配置路径前缀以用于匹配控制器方法。配置路径前缀用于丰富那些控制器类型与相应的 Predicate 相匹配的控制器方法的映射方式,第一个 Predicate 匹配的前缀会被使用。所有前缀添加的顺序很重要
HandlerTypePredicate
Predicate 类型变量推荐使用 HandlerTypePredicate,非常好用。
HandlerTypePredicate
实现函数式接口 Predicate
,从三个方面匹配 handler(注意,HandlerTypePredicate
不局限于 HandlerMethod
,也可以是其他类型的 handler)
-
Base packages -- 包匹配,根据 handler 所在的包名来匹配,以特定的包名开头的包才会匹配
-
Assignable types -- 类型匹配,为特定类型或者特定类型的子类型的 handler 才会匹配
-
Annotations -- 注解匹配,带有特定注解的 handler 才会匹配
这三种方式也是非常常见的匹配 bean 的方式。
HandlerTypePredicate
的构造函数
-
HandlerTypePredicate#forAnyHandlerType
任意类型的 handler 都匹配 -
HandlerTypePredicate#forBasePackage
在指定包名下的包或者子包下的控制器类型可以匹配 -
HandlerTypePredicate#forBasePackageClass
指定类所在的包和其子包下的控制器类型可以匹配(HandlerTypePredicate#forBasePackage
的类型安全的替代) -
HandlerTypePredicate#forAssignableType
指定类型的控制器或者指定类型的子类控制器可以匹配 -
HandlerTypePredicate#forAnnotation
带有执行注解的控制器类型可以匹配
Predicate
接口自带的组合函数
-
and
与 -
negate
非 -
or
或
HandlerTypePredicate
也可以利用这些方法进行组合,比如
Predicate<Class<?>> predicate = HandlerTypePredicate.forAnnotation(RestController.class).and(HandlerTypePredicate.forBasePackage("org.example"));
功能
将处于某一个范围内(最小粒度是同一个控制器内)控制器方法的公共路径或者公共字符提取出来,放到外部进行配置。
在进行请求路径和控制器方法的映射路径进行匹配之前,准确的说是在控制器方法的映射信息最开始初始化的时候,对控制器方法进行前缀的添加,这个前缀一般就是一层或者多层路径,
应用场景有很多,比如版本信息,当系统中需要同时保存多个版本的控制器方法的时候,可以将不同的版本放到不同的包里面,然后通过 pathPrefixes 为不同的包下的的控制器方法添加不同的前缀,比如 /v1
、/v2
,这样,当用户访问带有不同的版本数字的路径就能访问不同的版本的 API。
这应该是路径版本管理的最佳实践了。
简单实践
两个版本的 API
版本 1:
@RestController
@RequestMapping("/PathPrefix")
public class PathPrefixV1TestController {
@RequestMapping("/aaa")
public String getPathTest0(){
return "version 1 aaa";
}
@RequestMapping("/bbb")
public String getPathTest1(){
return "version 1 bbb";
}
}
版本 2:
@RestController
@RequestMapping("/PathPrefix")
public class PathPrefixV2TestController {
@RequestMapping("/aaa")
public String getPathTest0(){
return "version 2 aaa";
}
@RequestMapping("/bbb")
public String getPathTest1(){
return "version 2 bbb";
}
}
Web 配置中的 pathPrefixes 配置
@Configuration
//扫描组件
@ComponentScan("xyz.xiashuo.springmvcannotationconfig")
//开启MVC注解驱动
@EnableWebMvc
@Order(1)
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
HandlerTypePredicate predicateV1 = HandlerTypePredicate.forAssignableType(PathPrefixV1TestController.class);
HandlerTypePredicate predicateV2 = HandlerTypePredicate.forAssignableType(PathPrefixV2TestController.class);
// 就算前缀写的是 v1,最终效果也是跟前缀为 /v1 一样
configurer.addPathPrefix("/v1", predicateV1);
configurer.addPathPrefix("/v2", predicateV2);
WebMvcConfigurer.super.configurePathMatch(configurer);
}
}
简单的源码分析
PathMatchConfigurer
的 pathPrefixes
属性只在一个地方用到了,就是 WebMvcConfigurationSupport#requestMappingHandlerMapping
,这个方法的作用是向 IOC 容器中注入一个 RequestMappingHandlerMapping
类型的 Bean,PathMatchConfigurer
的 pathPrefixes
属性直接同步到了 RequestMappingHandlerMapping
的 pathPrefixes
属性中,(好家伙不仅类型一样,连字段名都一样),RequestMappingHandlerMapping
的作用,就是处理请求跟 HandlerMethod
类型的 handler 的映射。这也跟前面提到的 pathPrefixes
仅限用于 HandlerMethod
类型的 handler 的路径匹配对应上了。
RequestMappingHandlerMapping
的 pathPrefixes
属性在 getPathPrefix
方法中得到应用,getPathPrefix
的作用是,传入控制器的类型,返回控制器内的控制器方法对应的需要添加的前缀。
注意,哪个前缀对应的 Predicate 匹配成功,后面前缀对应的 Predicate 就不会再尝试匹配,所以前缀添加的顺序很重要。
@Nullable
String getPathPrefix(Class<?> handlerType) {
// 遍历 pathPrefixes
for (Map.Entry<String, Predicate<Class<?>>> entry : this.pathPrefixes.entrySet()) {
// 获取 pathPrefixes 的 Predicate 来测试 当前处理的控制器方法所在的控制器类型
if (entry.getValue().test(handlerType)) {
String prefix = entry.getKey();
// 如果可以解析,就进行解析
if (this.embeddedValueResolver != null) {
prefix = this.embeddedValueResolver.resolveStringValue(prefix);
}
// 如果当前的控制器方法所在的控制器类型匹配上了 Predicate ,就返回前缀,不考虑后续的 Predicate 了。
// 这种设计模式在SpringMVC中随处可见,比如内容协商中,多个ContentNegotiationStrategy一起解析请求的MediaType
return prefix;
}
}
return null;
}
调用栈
RequestMappingHandlerMapping
的父类是 AbstractHandlerMethodMapping
,AbstractHandlerMethodMapping
实现了 InitializingBean
,在 AbstractHandlerMethodMapping#afterPropertiesSet
中调用 AbstractHandlerMethodMapping#initHandlerMethods
初始化 IOC 容器中所有的 HandlerMethod
,遍历 IOC 容器中所有的 Bean 的名字,然后委托给 AbstractHandlerMethodMapping#processCandidateBean
来获取 Bean 实例,筛选出控制器类,并尝试从控制器类中找到控制器方法,在控制器类中找控制器方法的工作,委托给 AbstractHandlerMethodMapping#detectHandlerMethods
,在 AbstractHandlerMethodMapping#detectHandlerMethods
方法中,
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = (handler instanceof String ? obtainApplicationContext().getType((String) handler) : handler.getClass());
if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
// method -> { try { return getMappingForMethod(method, userType); } catch (Throwable ex) { throw new IllegalStateException("Invalid mapping on handler class [" + userType.getName() + "]: " + method, ex); }
// 是一个lambda表达式,强制转化为 MethodIntrospector.MetadataLookup 的接口实现,作为参数,传递给 MethodIntrospector的selectMethods方法
// 同时,getMappingForMethod方法返回的,也就是inspect方法返回的,是 RequestMappingInfo 类型,即 此函数中的泛型类型 T 为 RequestMappingInfo
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
});
if (logger.isTraceEnabled()) {
logger.trace(formatMappings(userType, methods));
}
methods.forEach((method, mapping) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
将函数式接口 MetadataLookup 的 inspect 方法用一个 lambda 表达式实现,lambda 表达式调用的是 AbstractHandlerMethodMapping#getMappingForMethod
,这个方法是空的,由子类 RequestMappingHandlerMapping
重写,
@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
info = typeInfo.combine(info);
}
// 查看有没有配置前缀
String prefix = getPathPrefix(handlerType);
if (prefix != null) {
// 如果有,在 info 的最前面添加上
info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
}
}
return info;
}
@Nullable
String getPathPrefix(Class<?> handlerType) {
for (Map.Entry<String, Predicate<Class<?>>> entry : this.pathPrefixes.entrySet()) {
if (entry.getValue().test(handlerType)) {
String prefix = entry.getKey();
if (this.embeddedValueResolver != null) {
prefix = this.embeddedValueResolver.resolveStringValue(prefix);
}
return prefix;
}
}
return null;
}
调用栈如下:忽略中间 lambda 表达式的调用栈。

关于
RequestMappingHandlerMapping
完整的初始化流程,请看《SpringMVC-RequestMappingHandlerMapping 源码解析.md》。
suffixPatternMatch 字段 - 弃用
registeredSuffixPatternMatch - 弃用
patternParser 字段 - 非默认路径映射处理类
和 PathPatternParser 相关的类有三个,简单介绍一下
PathContainer
或者其子类RequestPath
:分解请求路径PathPatternParser
:按照配置分解路径模板PatternParser
:将分解后的请求路径和分解后的路径模板进行匹配
其中,只有 PathMatchConfigurer
暴露给了用户,用户需要关心的,也只有保存有配置信息的 PathPatternParser
,PathPatternParser
中有哪些可配置的信息呢:
-
matchOptionalTrailingSeparator
:是否忽略请求路径最后的反斜杠/
的匹配,默认值为 true -
caseSensitive
:是否大小写敏感,默认值为 true -
pathOptions
,类型为PathContainer.Options
,PathContainer.Options
原本的功能是指定将请求路径转化为PathContainer
的时候的配置,这里用于配置将路径模板字符串分割为单个路径元素的时候的配置,默认值为PathContainer.Options.HTTP_PATH
,即分隔符为/
,且各个层级的片段都要解码。这个配置一般也不用动,除非你想要换分隔符
具体请看《PathContainer+PathPatternParser+PatternParser 系列接口简单解析》
实际设置:
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
PathPatternParser patternParser = new PathPatternParser();
// patternParser.setPathOptions();
// patternParser.setCaseSensitive();
// patternParser.setMatchOptionalTrailingSeparator();
configurer.setPatternParser(patternParser);
WebMvcConfigurer.super.configurePathMatch(configurer);
}
urlPathHelper 和 defaultUrlPathHelper && pathMatcher 和 defaultPathMatcher - 默认路径映射处理类
urlPathHelper 的作用简而言之,就是获取路径中的用于进行 handler 映射的片段。
pathMatcher 的作用是匹配,将请求路径中的用于进行 handler 映射的片段跟 handler 声明的映射路径进行匹配
重点关注 AbstractHandlerMapping 的 initLookupPath 方法中,对其 urlPathHelper 字段的使用。
参考《UrlPathHelper+PathMatcher 简单解析.md》
定制 urlPathHelper
总结一下,有哪些条件可以定制?后面实践的时候去试试,TODO
-
alwaysUseFullPath
:是否使用全路径进行 handler 映射路径匹配 -
urlDecode
和UrlPathHelper#defaultEncoding
:设置是否解码和默认的解码格式,默认的解码格式是ISO-8859-1
,在路径 URL 路径中使用中文的时候需要自定义解码格式。 -
removeSemicolonContent
是否去掉 URL 中由/
分割的每一层路径中的第一个;
后面的内容,默认为 true。举个例子访问/SpringMVC_AnnotationConfig/HandlerInterceptor;456/simple;1231
,如果UrlPathHelper#removeSemicolonContent
为 true,此路径会被解析为/SpringMVC_AnnotationConfig/HandlerInterceptor/simple
,/HandlerInterceptor
和/simple
分号后面的内容都被去掉了。有时候我们会在 URL 中携带参数,参数中也尽量不要包含;
,不然会被截断,如果万不得已,必须包含,那就得设置这个属性为 false,不让其被截断。
参考《UrlPathHelper+PathMatcher 简单解析.md》中的
UrlPathHelper简单解析
实际设置:
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper helper = new UrlPathHelper();
//helper.setDefaultEncoding("UTF-8");
//helper.setRemoveSemicolonContent();
//helper.setAlwaysUseFullPath();
//helper.setUrlDecode();
configurer.setUrlPathHelper(helper);
WebMvcConfigurer.super.configurePathMatch(configurer);
}
定制 pathMatcher
PathMatcher 只有一个实现类是 AntPathMatcher,支持 ant 样式的模式语法。除了更换 PathMatcher 的默认实现(默认为 AntPathMatcher),一般都是没有必要在 configurePathMatch 中自定义 pathMatcher 属性的。
到底用哪一套逻辑呢?
其实,我们可以很明显的感觉到,PathContainer+PathPatternParser+PatternParser
和 UrlPathHelper+PathMatcher
做的其实是同一个工作,即解析请求 URI,然后与 handler 的映射路径匹配,进而获取处理请求的 handler,那实际情况中用哪一套逻辑呢?我们简单的分情况分析一下
DispatcherServlet#doDispatch
中的 mappedHandler = getHandler(processedRequest);
最终调用的是 HandlerMapping#getHandler
,而 HandlerMapping#getHandler
的抽象实现类 AbstractHandlerMapping
在实现此方法的时候,将其委托给 AbstractHandlerMapping#getHandlerInternal
,AbstractHandlerMapping#getHandlerInternal
是抽象方法,需要子类重写。其中两个子类 AbstractHandlerMethodMapping
和 AbstractUrlHandlerMapping
都对其进行了重写。
AbstractHandlerMethodMapping
处理控制器方法映射 AbstractHandlerMethodMapping#getHandlerInternal
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
// 初始化查找路径
String lookupPath = initLookupPath(request);
// 映射注册锁,我猜意思是在查找的时候不允许注册,
this.mappingRegistry.acquireReadLock();
try {
// 查找控制器方法
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
// 释放锁
this.mappingRegistry.releaseReadLock();
}
}
接下来查看 AbstractHandlerMapping#initLookupPath
,注意,这个方法放到了 AbstractHandlerMapping
中,因为在 AbstractUrlHandlerMapping#getHandlerInternal
实现中也使用了这个方法,SpringMVC 的代码真的很整洁,能提取到父类中的一定提取到父类。
protected String initLookupPath(HttpServletRequest request) {
// 当前 AbstractHandlerMapping 的 patternParser 为空,表示没有配置 patternParser ,返回false
// 不为空,表示配置了 patternParser 即要使用 patternParser 返回true
if (usesPathPatterns()) {
// 如果决定要使用 patternParser
request.removeAttribute(UrlPathHelper.PATH_ATTRIBUTE);
RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(request);
String lookupPath = requestPath.pathWithinApplication().value();
// 很奇妙,通过requestPath.pathWithinApplication获取了查找路径之后,还通过UrlPathHelper.removeSemicolonContent去掉一下分号
return UrlPathHelper.defaultInstance.removeSemicolonContent(lookupPath);
}
else {
// resolveAndCacheLookupPath 实际上就是 getLookupPathForRequest 方法包了一层,
// 同时将查出来的数据放到请求域属性中,属性名为 org.springframework.web.util.UrlPathHelper.path
return getUrlPathHelper().resolveAndCacheLookupPath(request);
}
}
然后开始看 AbstractHandlerMethodMapping#lookupHandlerMethod
方法,会根据是否配置了 patternParser,调用不同的匹配方法。具体我们就不去深究了
@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<>();
List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
if (directPathMatches != null) {
// 如果配置了 patternParser , 最终会调用 PathPattern的matches方法
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
}
if (!matches.isEmpty()) {
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
bestMatch = matches.get(0);
if (logger.isTraceEnabled()) {
logger.trace(matches.size() + " matching mappings: " + matches);
}
if (CorsUtils.isPreFlightRequest(request)) {
return PREFLIGHT_AMBIGUOUS_MATCH;
}
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
String uri = request.getRequestURI();
throw new IllegalStateException(
"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
}
}
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
// 如果没有配置 patternParser,会在这里, 最终会调用 AntPathMatcher的doMatch方法
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}
else {
return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);
}
}
详细的过程,请查看《SpringMVC-RequestMappingHandlerMapping 源码解析.md》
AbstractUrlHandlerMapping
处理非控制器方法映射
查看 AbstractHandlerMethodMapping#getHandlerInternal
@Override
@Nullable
protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = initLookupPath(request);
Object handler;
// 很明显是否配置了 patternParser 进入不同的查找handler的方法,
if (usesPathPatterns()) {
RequestPath path = ServletRequestPathUtils.getParsedRequestPath(request);
handler = lookupHandler(path, lookupPath, request);
}
else {
handler = lookupHandler(lookupPath, request);
}
if (handler == null) {
// We need to care for the default handler directly, since we need to
// expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well.
Object rawHandler = null;
if (StringUtils.matchesCharacter(lookupPath, '/')) {
rawHandler = getRootHandler();
}
if (rawHandler == null) {
rawHandler = getDefaultHandler();
}
if (rawHandler != null) {
// Bean name or resolved handler?
if (rawHandler instanceof String) {
String handlerName = (String) rawHandler;
rawHandler = obtainApplicationContext().getBean(handlerName);
}
validateHandler(rawHandler, request);
handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null);
}
}
return handler;
}
如果配置了 patternParser,则进入 lookupHandler,使用 pathPatternHandlerMap 进行查找
如果没有,则进入 lookupHandler,通过 pathMatcher 进行查找,
逻辑很简单,这里就不细看了。
区别
这两个都是非常常用的路径模板工具,平常自己私底下也可以使用。非常方便。
此外,在处理大量请求的时候,速度上应该是:PathContainer+PathPatternParser+PatternParser
> UrlPathHelper+PathMatcher
,我猜的。
多个路径模板匹配上了同一个请求 URL
当多个路径模板匹配一个 URL 时,必须选择最佳匹配。匹配方式有两种,根据是否配置了 PathPattern
选择一种
注意,这个时候,跟 HandlerMapping
的匹配不是一个环节。
addViewControllers
此方法本质上也是用于构造一个 HandlerMapping
类型的 bean,实际类型为一个自定义的 SimpleUrlHandlerMapping
。直接将 URL 映射到 ParameterizableViewController
进行视图解析。
这种直接添加一个 HandlerMapping
来实现特定 URL 的处理的的实现方式跟 DefaultServletHandlerConfigurer
的实现方式很像,
在《SpringMVC- 第五篇:视图》的
<mvc:view-controller>
相关小节中已经学习过了
调用栈
WebMvcConfigurationSupport#viewControllerHandlerMapping
方法,这是一个@Bean 方法,创建的 Bean 的类型是 HandlerMapping
,Bean 的名称是 viewControllerHandlerMapping
,剩下的调用栈跟配置方法的调用栈大同小异,这里就省略了。
具体细节,看后面的小节
WebMvcConfigurationSupport
小节和DelegatingWebMvcConfiguration
小节中查看具体细节
@Bean
@Nullable
public HandlerMapping viewControllerHandlerMapping(
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {
// 手动创建 ViewControllerRegistry
ViewControllerRegistry registry = new ViewControllerRegistry(this.applicationContext);
// 调用各个Web配置类的 addViewControllers 方法
addViewControllers(registry);
// 获取原始的 HandlerMapping 类型为 SimpleUrlHandlerMapping
AbstractHandlerMapping handlerMapping = registry.buildHandlerMapping();
if (handlerMapping == null) {
return null;
}
// 填充 handlerMapping 的各项属性
PathMatchConfigurer pathConfig = getPathMatchConfigurer();
if (pathConfig.getPatternParser() != null) {
handlerMapping.setPatternParser(pathConfig.getPatternParser());
}
else {
handlerMapping.setUrlPathHelper(pathConfig.getUrlPathHelperOrDefault());
handlerMapping.setPathMatcher(pathConfig.getPathMatcherOrDefault());
}
handlerMapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
handlerMapping.setCorsConfigurations(getCorsConfigurations());
return handlerMapping;
}
经过了所有 Web 配置类中的 addViewControllers
方法的修改,最终,在 WebMvcConfigurationSupport#viewControllerHandlerMapping
中调用 ViewControllerRegistry#buildHandlerMapping
来返回最终的 HandlerMapping
对象。
ViewControllerRegistry
核心属性是 List<ViewControllerRegistration> registrations
和 List<RedirectViewControllerRegistration> redirectRegistrations
,所有添加 URL 字符串(即注册信息)都会封装为的 ViewControllerRegistration
或者 RedirectViewControllerRegistration
,最终通过调用 ViewControllerRegistry#buildHandlerMapping
根据 registrations
和 redirectRegistrations
构建 SimpleUrlHandlerMapping
实例并返回。SimpleUrlHandlerMapping
其实很简单,只用指定这个 HandlerMapping
需要处理的所有 URL 的匹配模式和单个 URL 匹配模式对应的处理器即可,用 urlMap 存起来,初始化的时候传入即可。
结构跟
InterceptorRegistry
有点像。
addViewControllers
方法只有一个参数,就是ViewControllerRegistry
类型的参数,ViewControllerRegistry
类型的参数下有两种注册信息封装类,一个是ViewControllerRegistration
,一个是RedirectViewControllerRegistration
,但是他们都用于配置同一种 handler,即ParameterizableViewController
public class ViewControllerRegistry {
@Nullable
private ApplicationContext applicationContext;
private final List<ViewControllerRegistration> registrations = new ArrayList<>(4);
private final List<RedirectViewControllerRegistration> redirectRegistrations = new ArrayList<>(10);
private int order = 1;
// 构造函数,初始化 applicationContext 字段
......
// addViewController 添加视图控制器到 registrations 字段中,
......
// addRedirectViewController 添加重定向视图控制器到 redirectRegistrations 字段中,
......
// addStatusController 添加一个视图控制器,但是只设置响应的状态码,不返回视图,不返回的意思就是,控制器方法返回的ModelAndView是null,这里不深入研究
......
// 设置返回的 SimpleUrlHandlerMapping 的 order属性
......
// 根据 registrations 和 redirectRegistrations 构建 SimpleUrlHandlerMapping
@Nullable
protected SimpleUrlHandlerMapping buildHandlerMapping() {
if (this.registrations.isEmpty() && this.redirectRegistrations.isEmpty()) {
return null;
}
// 这个 Map其实很简单 key 为URL的匹配模式,value为 handler 即处理器
// 将这个 HandlerMapping 需要处理的所有URL的匹配模式和对应的处理器用urlMap存起来,用于初始化 HandlerMapping 实现类实例
Map<String, Object> urlMap = new LinkedHashMap<>();
for (ViewControllerRegistration registration : this.registrations) {
urlMap.put(registration.getUrlPath(), registration.getViewController());
}
for (RedirectViewControllerRegistration registration : this.redirectRegistrations) {
urlMap.put(registration.getUrlPath(), registration.getViewController());
}
// 初始化 SimpleUrlHandlerMapping 实例的时候,要注意带上 order
return new SimpleUrlHandlerMapping(urlMap, this.order);
}
}
ViewControllerRegistration
ViewControllerRegistration
的源码很简单,懒得贴代码了,
属性只有两个,一个 URL 路径模式字符串(String urlPath
)和一个此 URL 对应的 handler(ParameterizableViewController controller
),类型为 ParameterizableViewController
,注意,虽然概念上是 handler,但是名字叫 controller,这一点有点容易让人迷惑。
构造方法只有以 urlPath
为参数的构造函数,除此之外,有 ViewControllerRegistration#setStatusCode
、ViewControllerRegistration#setViewName
、ViewControllerRegistration#setApplicationContext
方法用于设置 controller
实例的想用属性,
注意,ViewControllerRegistration#setStatusCode
会返回当前实例,这是为了实现类似 builder 一样的构造器的链式使用,但是很奇怪 ViewControllerRegistration#setViewName
、ViewControllerRegistration#setApplicationContext
都返回 void,不支持链式调用。在后面的操作小节中可以看到链式调用的例子。
ViewControllerRegistration#getViewController
用于返回配置好的 controller
实例,即 handler。
RedirectViewControllerRegistration
和 ViewControllerRegistration
基本上一模一样,跟 ViewControllerRegistration
相比,RedirectViewControllerRegistration
实际上就是将 handler 返回的视图从一个视图名称换成了一个类似于 redirect:
开头的重定向视图名,而且这个重定向的视图名需要你在构造 ViewControllerRegistration
的时候就指定。
ViewControllerRegistration#setViewName
方法中调用 controller.setView
方法
public void setViewName(String viewName) {
this.controller.setViewName(viewName);
}
RedirectViewControllerRegistration
的构造器函数中调用 controller.setView
方法
public RedirectViewControllerRegistration(String urlPath, String redirectUrl) {
Assert.notNull(urlPath, "'urlPath' is required.");
Assert.notNull(redirectUrl, "'redirectUrl' is required.");
this.urlPath = urlPath;
this.redirectView = new RedirectView(redirectUrl);
this.redirectView.setContextRelative(true);
this.controller.setView(this.redirectView);
}
一目了然。
ParameterizableViewController
ViewControllerRegistration
和 RedirectViewControllerRegistration
中,实际指定的 handler 类型都是 ParameterizableViewController
,这两个类的作用本质上就是包装 ParameterizableViewController
类,接下来我们就来会一会这个 ParameterizableViewController
。
Controller 接口
ParameterizableViewController
实现 Controller
接口,注意是 org.springframework.web.servlet.mvc
下的,不是 @Controller
注解,@Controller
注解所在包名为 org.springframework.stereotype
。Controller
接口的实现类都是 handler,即处理器,Controller
接口是一个函数式接口只有一个方法 handleRequest
,用于处理请求,说实话作为一个 handler,这一个方法就足够了,跟 @Controller
注解修饰的控制器中的控制器方法的作用是一样的。
@FunctionalInterface
public interface Controller {
@Nullable
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}
Controller
接口的使用场景是什么,什么场景下才要用到实现Controller
接口的 handler,而不使用@Controller
注解修饰的类中的控制器方法呢?简单一想,能想到的场景:运行时动态注册,就像我们使用ParameterizableViewController
的方式一样。
Controller
接口跟@Controller
注解的关系以及生效的原理,请看《SpringMVC 控制器方法(handler)适配器 - HandlerAdapter》
继承树:

只看 AbstractController
,实现 handleRequest
方法并委托给 AbstractController#handleRequestInternal
方法,AbstractController#handleRequestInternal
方法是空的,留给子类实现,ParameterizableViewController
继承了 AbstractController
,重写了 handleRequestInternal
,主要是对返回的 ModelAndView
进行处理,这里就不细看了,以后再研究
实践
在 WebConfig
中继承 WebMvcConfigurer
之后重写 addViewControllers
方法,这里省略了其他方法的重写,只看 addViewControllers
方法
@Configuration
//扫描组件
@ComponentScan("xyz.xiashuo.springmvcannotationconfig")
//开启MVC注解驱动
@EnableWebMvc
@Order(1)
public class WebConfig implements WebMvcConfigurer {
// 其他重写 WebMvcConfigurer 的方法
......
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 这个功能还是很有帮助的,比如,有的功能还没有做完,我们可以把链接转向一个视图,页面上写着此功能还在开发
// 又或者用户没有登陆,某个功能已经下架,我们也可以配置这个功能模块的所有路径都指向一个页面,页面上写着此功能已下架,
// 反正只要是一系列网页都指向同一个页面,同时这个页面不需要后台交互的,都可以用View Controller实现,简单又方便。
registry.addViewController("/aaa/**").setStatusCode(HttpStatus.OK).setViewName("/aHtml");
// 配置一个重定向视图
// 注意,此时 setStatusCode() 方法只能传入 Series.REDIRECTION 系列的 code
registry.addRedirectViewController("/bbb","/aaa").setStatusCode(HttpStatus.TEMPORARY_REDIRECT);
}
}
测试链接
<a th:href="@{/aaa}">测试ViewController</a>
<br/>
<a th:href="@{/bbb}">测试ViewController-转发</a>
<br/>
目标视图
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>测试页面</title>
</head>
<body>
<h1>"/aaa"的测试页面</h1>
<h2>此模块功能还未上线,敬请期待</h2>
</body>
</html>
有两点需要注意:
-
url 参数,可以使用通配符,匹配一系列 url,而不是某个特定的 URL
-
配置重定向视图的手,
setStatusCode()
方法只能传入Series.REDIRECTION
系列的 HTTP 状态码
使用场景
这个功能还是很有帮助的,比如,有的功能还没有做完,我们可以把链接转向一个视图,页面上写着此功能还在开发,又或者用户没有登陆,某个功能已经下架,我们也可以配置这个功能模块的所有路径都指向一个页面,页面上写着此功能已下架。
反正只要是一系列网页都指向同一个页面,同时这个页面不需要后台交互的,都可以用 View Controller 实现,简单又方便。
configureContentNegotiation
调用栈
WebMvcConfigurationSupport#mvcContentNegotiationManage
方法,是一个 @Bean
方法,用于注册一个 id 为 mvcContentNegotiationManager
的 ContentNegotiationManage
类型的 bean,在配置的过程中,将一部分配置工作委托给 configureContentNegotiation
方法,可以注意到,在 WebMvcConfigurationSupport
中,这个方法是空的,意思是留给子类重写。剩下的调用栈跟配置方法的调用栈大同小异,这里就省略了。
具体细节,看后面的小节
WebMvcConfigurationSupport
小节和DelegatingWebMvcConfiguration
小节中查看具体细节
经过了所有 Web 配置类中的 configureContentNegotiation
方法的修改,最终,在 WebMvcConfigurationSupport#mvcContentNegotiationManage
中返回最终的 ContentNegotiationManage
对象。
在 WebMvcConfigurationSupport
中,在多个地方都用到了 id 为 mvcContentNegotiationManager
的 ContentNegotiationManage
类型的 bean,这些也是内容协商的使用场景
-
WebMvcConfigurationSupport#handlerExceptionResolver
-
WebMvcConfigurationSupport#mvcViewResolver
-
WebMvcConfigurationSupport#requestMappingHandlerAdapter
-
WebMvcConfigurationSupport#requestMappingHandlerMapping
-
WebMvcConfigurationSupport#resourceHandlerMapping
现在看一下 WebMvcConfigurationSupport#mvcContentNegotiationManage
方法:
@Bean
public ContentNegotiationManager mvcContentNegotiationManager() {
if (this.contentNegotiationManager == null) {
ContentNegotiationConfigurer configurer = new ContentNegotiationConfigurer(this.servletContext);
// 调用getDefaultMediaTypes()注册默认的文件拓展名和MediaType的映射关系
// 主要用于 `PathExtensionContentNegotiationStrategy`或者`ServletPathExtensionContentNegotiationStrategy`,和`ParameterContentNegotiationStrategy`
configurer.mediaTypes(getDefaultMediaTypes());
// 依次调用Web配置中的 configureContentNegotiation 方法
configureContentNegotiation(configurer);
// 构建 ContentNegotiationManager
this.contentNegotiationManager = configurer.buildContentNegotiationManager();
}
return this.contentNegotiationManager;
}
其中会通过 getDefaultMediaTypes()
获取默认的文件拓展名和 MediaType 的映射关系,注册到 ContentNegotiationConfigurer
中,最终会用于注册 PathExtensionContentNegotiationStrategy
或者 ServletPathExtensionContentNegotiationStrategy
,和 ParameterContentNegotiationStrategy
。
getDefaultMediaTypes()
方法的具体内容需要根据全局变量来,注意,常用的文件拓展名只包括 xml 和 json,不包括 txt
等,如果想要添加,则需要我们在 Web 配置类中重写 configureContentNegotiation
方法来添加。
具体实践,请看《SpringMVC-ContentNegotiation 内容协商.md》的
通过Java配置
小节
protected Map<String, MediaType> getDefaultMediaTypes() {
Map<String, MediaType> map = new HashMap<>(4);
if (romePresent) {
map.put("atom", MediaType.APPLICATION_ATOM_XML);
map.put("rss", MediaType.APPLICATION_RSS_XML);
}
if (!shouldIgnoreXml && (jaxb2Present || jackson2XmlPresent)) {
map.put("xml", MediaType.APPLICATION_XML);
}
if (jackson2Present || gsonPresent || jsonbPresent) {
map.put("json", MediaType.APPLICATION_JSON);
}
if (jackson2SmilePresent) {
map.put("smile", MediaType.valueOf("application/x-jackson-smile"));
}
if (jackson2CborPresent) {
map.put("cbor", MediaType.APPLICATION_CBOR);
}
return map;
}
在静态代码块中根据第三方库是否存在来确定全局变量。
参考
WebMvcConfigurationSupport
小节中的分析
ContentNegotiationConfigurer
ContentNegotiationConfigurer 中的所有配置项还有具体的源码分析,请看《SpringMVC-ContentNegotiation 内容协商.md》的 通过Java配置
小节
实践
具体实践,请看《SpringMVC-ContentNegotiation 内容协商.md》的 通过Java配置
小节
configureMessageConverters & extendMessageConverters
调用栈
配置 HttpMessageConverter,跟 ContentNegotiation 联系紧密。
WebMvcConfigurationSupport#getMessageConverters
的作用是获取 SpringMVC 中共享所有的 HttpMessageConverter
,此方法会被其他组件调用,我们后面再看。先看调用栈
protected final List<HttpMessageConverter<?>> getMessageConverters() {
if (this.messageConverters == null) {
this.messageConverters = new ArrayList<>();
// 先调用所有Web配置中的 configureMessageConverters 方法
configureMessageConverters(this.messageConverters);
if (this.messageConverters.isEmpty()) {
// 如果 Web配置没有配置 messageConverters ,那么就加载默认的 messageConverters
// 一般我们都不会在Web配置中的 configureMessageConverters 方法中添加 messageConverters ,所以一般都会加载默认的 messageConverters
addDefaultHttpMessageConverters(this.messageConverters);
}
// 继续拓展 messageConverters,一般都是在默认的 messageConverters 的基础上拓展
extendMessageConverters(this.messageConverters);
}
return this.messageConverters;
}
方法内调用了 configureMessageConverters
、addDefaultHttpMessageConverters
和 extendMessageConverters
,我们先看 configureMessageConverters
和 extendMessageConverters
。
我们细看 WebMvcConfigurationSupport#configureMessageConverters
和 WebMvcConfigurationSupport#extendMessageConverters
方法,可以注意到,在 WebMvcConfigurationSupport
中,这两个方法是空的,意思是留给子类重写,剩下的调用栈跟配置方法的调用栈大同小异,这里就省略了。
具体细节,看后面的小节
WebMvcConfigurationSupport
小节和DelegatingWebMvcConfiguration
小节中查看具体细节
addDefaultHttpMessageConverters
方法不是空的,用于获取默认的 HttpMessageConverter
,这个我们后文详细解析。
所以这个方法大概的流程就是,先经过所有 Web 配置类中的 configureMessageConverters
方法配置 HttpMessageConverter
列表,如果列表仍为空列表,就调用 addDefaultHttpMessageConverters
加载默认的 HttpMessageConverter
,最后,调用所有 Web 配置类中的 extendMessageConverters
方法对 HttpMessageConverter
列表进行最后的拓展,最后返回这个 HttpMessageConverter
列表。
基本上,通过引入第三方库,即可自动注入相应的 HttpMessageConverter,很少需要自己在 Web 配置类重写 configureMessageConverters
或者 extendMessageConverters
来手动配置 HttpMessageConverter
,除非自己实现了 HttpMessageConverter
,可以通过重写 extendMessageConverters
方法于默认添加的 HttpMessageConverter
一起配合使用。或者需要对默认添加的 HttpMessageConverter
进行属性的修改
基于 xml 配置 SpringMVC 的时候,也会注入这几个
HttpMessageConverter
,请参考《SpringMVC- 第九篇:基于 XML 配置 SpringMVC.md》
仔细分析 addDefaultHttpMessageConverters
方法,默认添加 7 个 HttpMessageConverter
,
注意 HttpMessageConverter
实现类的注册的顺序很重要,因为前面的 HttpMessageConverter 解析成功,就不会再尝试后面的 HttpMessageConverter。
-
ByteArrayHttpMessageConverter
-
StringHttpMessageConverter
-
ResourceHttpMessageConverter
-
ResourceRegionHttpMessageConverter
-
SourceHttpMessageConverter
-
AllEncompassingFormHttpMessageConverter
-
Jaxb2RootElementHttpMessageConverter
关于具体的
HttpMessageConverter
实现类的分析,看《SpringMVC-ContentNegotiation 内容协商.md》的HttpMessageConverter
小节
protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
// 这四个 HttpMessageConverter 必添加
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(new StringHttpMessageConverter());
messageConverters.add(new ResourceHttpMessageConverter());
messageConverters.add(new ResourceRegionHttpMessageConverter());
// shouldIgnoreXml 默认为false ,所以默认也会添加 SourceHttpMessageConverter
if (!shouldIgnoreXml) {
try {
messageConverters.add(new SourceHttpMessageConverter<>());
}
catch (Throwable ex) {
// Ignore when no TransformerFactory implementation is available...
}
}
// 默认添加 AllEncompassingFormHttpMessageConverter
messageConverters.add(new AllEncompassingFormHttpMessageConverter());
if (romePresent) {
messageConverters.add(new AtomFeedHttpMessageConverter());
messageConverters.add(new RssChannelHttpMessageConverter());
}
// shouldIgnoreXml 默认为false,jaxb2Present 默认为true,所以默认添加 Jaxb2RootElementHttpMessageConverter
// 至此,默认添加 7个 HttpMessageConverter
// 其他的 HttpMessageConverter 的引入都需要引入第三方库
if (!shouldIgnoreXml) {
// 注意,如果 jackson2XmlPresent 为true,则不会再添加 Jaxb2RootElementHttpMessageConverter
if (jackson2XmlPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
}
else if (jaxb2Present) {
messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
}
}
if (jackson2Present) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));
}
else if (gsonPresent) {
messageConverters.add(new GsonHttpMessageConverter());
}
else if (jsonbPresent) {
messageConverters.add(new JsonbHttpMessageConverter());
}
else if (kotlinSerializationJsonPresent) {
messageConverters.add(new KotlinSerializationJsonHttpMessageConverter());
}
if (jackson2SmilePresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build()));
}
if (jackson2CborPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.cbor();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build()));
}
}
在静态代码块中根据第三方库是否存在来确定全局变量。
参考
WebMvcConfigurationSupport
小节中的分析
WebMvcConfigurationSupport#getMessageConverters
方法,在 WebMvcConfigurationSupport
方法中被多次调用
-
WebMvcConfigurationSupport#requestMappingHandlerAdapter
返回RequestMappingHandlerAdapter
类型的 Bean -
WebMvcConfigurationSupport#routerFunctionMapping
返回RouterFunctionMapping
类型的 Bean -
WebMvcConfigurationSupport#addDefaultHandlerExceptionResolvers
,用于添加默认的HandlerExceptionResolver
实践
略。
configureAsyncSupport
TODO
addFormatters
类型转换,数据绑定,正在学习
addResourceHandlers
此方法本质上也是用于构造一个 HandlerMapping
类型的 bean,实际类型为一个自定义的 SimpleUrlHandlerMapping
。直接将 URL 映射到 ResourceHttpRequestHandler
进行解析。
这种直接添加一个 HandlerMapping
来实现特定 URL 的处理的的实现方式跟 addViewControllers
的实现方式很像,
xml 相关的配置回看《SpringMVC- 第二篇:控制器方法(handler)映射》中的
静态资源映射
小节
调用链
WebMvcConfigurationSupport#resourceHandlerMapping
方法,这是一个 @Bean
方法,创建的 Bean 的类型是 HandlerMapping
,Bean 的名称是 resourceHandlerMapping
,剩下的调用栈跟配置方法的调用栈大同小异,这里就省略了。
具体细节,看后面的小节
WebMvcConfigurationSupport
小节和DelegatingWebMvcConfiguration
小节中查看具体细节
@Bean
@Nullable
public HandlerMapping resourceHandlerMapping(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {
Assert.state(this.applicationContext != null, "No ApplicationContext set");
Assert.state(this.servletContext != null, "No ServletContext set");
// 获取路径解析的相关配置 参考 configurePathMatch
PathMatchConfigurer pathConfig = getPathMatchConfigurer();
ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext,
this.servletContext, contentNegotiationManager, pathConfig.getUrlPathHelper());
// 依次调用所有Web配置中的 addResourceHandlers 方法
addResourceHandlers(registry);
// 获取最终的 AbstractHandlerMapping 对象,其实就是一个 SimpleUrlHandlerMapping
AbstractHandlerMapping handlerMapping = registry.getHandlerMapping();
if (handlerMapping == null) {
return null;
}
// 如果设置了 PatternParser ,就用 PatternParser 拿一套逻辑,如果没有就用 UrlPathHelper + PathMatcher
if (pathConfig.getPatternParser() != null) {
handlerMapping.setPatternParser(pathConfig.getPatternParser());
}
else {
handlerMapping.setUrlPathHelper(pathConfig.getUrlPathHelperOrDefault());
handlerMapping.setPathMatcher(pathConfig.getPathMatcherOrDefault());
}
// ResourceUrlProvider 主要是用来获取 拦截器的
handlerMapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
// 设置跨域配置,静态资源的访问尤其要注意跨域配置
handlerMapping.setCorsConfigurations(getCorsConfigurations());
return handlerMapping;
}
经过了所有 Web 配置类中的 addResourceHandlers
方法的修改,最终,在 WebMvcConfigurationSupport#resourceHandlerMapping
中调用 ResourceHandlerRegistry#getHandlerMapping
来返回最终的 HandlerMapping
对象。
ResourceUrlProvider
此外,我们有必要学习一下 ResourceUrlProvider
,ResourceUrlProvider
在这里作为 WebMvcConfigurationSupport#resourceHandlerMapping
方法的参数其实并没有参与静态资源解析的配置,ResourceUrlProvider
的功能也不在于配置静态资源解析,而是获取所有的跟静态资源处理相关的 handler(ResourceHttpRequestHandler
类型)以及其映射的 url 模板和获得提供给客户端的用于访问静态资源的公共的 URL 路径
具体的功能分析,请看
WebMvcConfigurationSupport
小节下的ResourceUrlProvider
小节,这个工具类非常好用
ResourceHandlerRegistry
addResourceHandlers
方法只有一个参数,就是 ResourceHandlerRegistry
类型的参数,ResourceHandlerRegistry
类型下只有一种注册信息封装类,即 ResourceHandlerRegistration
,用于配置一种类型的 handler,即 ResourceHttpRequestHandler
,尤其要注意 ResourceHandlerRegistration
中用于配置 ResourceHttpRequestHandler
的一个属性,即 ResourceChainRegistration
,其中保存了静态资源的处理链信息,有点像类似于 interceptor 之于 handler。
跟 addViewControllers 方法的内部结构和 configureDefaultServletHandling 方法的内部结构很像,尤其是跟 addViewControllers 方法,
addViewControllers
方法只有一个参数,就是ViewControllerRegistry
类型的参数,ViewControllerRegistry
类型的参数下有两种注册信息封装类,一个是ViewControllerRegistration
,一个是RedirectViewControllerRegistration
,但是他们都用于配置同一种 handler,即ParameterizableViewController
ResourceHandlerRegistry
的作用是存储处理静态资源的 handler(具体类型为 ResourceHttpRequestHandler
)的注册信息(具体类型为 ResourceHandlerRegistration
),这些 handler 用于通过 Spring MVC 提供静态资源,如图像、css 文件和其他,注册信息中还包括缓存头(ResourceHandlerRegistration#CacheControl
)的设置,以在 web 浏览器中高效加载。静态资源可以从 web 应用程序根目录下的位置提供(src\main\webapp
),也可以从类路径中提供(src\main\resources
),也可以从操作系统的文件系统中提供。
要创建一个处理静态资源的 handler,通过调用 addResourceHandler
提供 URL 路径模板 (例如 /resources/**
) 即可。然后在 addResourceHandler
返回的 ResourceHandlerRegistration
上使用 ResourceHandlerRegistration#addResourceLocations
方法添加一个或多个位置,从这些位置提供静态资源内容 (例如 /
对应web 应用程序根目录src\main\webapp
,classpath:/public-web-resources/
注意 classpath:
开头的路径表示编译输出路径,对应 web 应用程序根目录 src\main\webapp
下的 WEB-INF/classes
路径,对应 src\main\resources
,还可以使用系统文件路径例如 file:/c:/test/xxx.txt
),也可以在 addResourceHandler
返回的 ResourceHandlerRegistration
上调用其他方法(比如 setCacheControl
)为所服务的资源指定缓存周期.
public class ResourceHandlerRegistry {
private final ServletContext servletContext;
private final ApplicationContext applicationContext;
@Nullable
private final ContentNegotiationManager contentNegotiationManager;
@Nullable
private final UrlPathHelper pathHelper;
// 处理静态资源请求的handler的注册信息列表
private final List<ResourceHandlerRegistration> registrations = new ArrayList<>();
// 处理静态资源请求的handler的映射 即SimpleUrlHandlerMapping 的顺序
private int order = Ordered.LOWEST_PRECEDENCE - 1;
// 构造函数
......
// 指定路径模板,创建注册信息
public ResourceHandlerRegistration addResourceHandler(String... pathPatterns) {
ResourceHandlerRegistration registration = new ResourceHandlerRegistration(pathPatterns);
this.registrations.add(registration);
return registration;
}
// 当前以及添加的注册信息,是否以及包含了给定路径模板对应的handler的注册信息
public boolean hasMappingForPattern(String pathPattern) {
for (ResourceHandlerRegistration registration : this.registrations) {
if (Arrays.asList(registration.getPathPatterns()).contains(pathPattern)) {
return true;
}
}
return false;
}
// setOrder
......
@Nullable
@SuppressWarnings("deprecation")
protected AbstractHandlerMapping getHandlerMapping() {
if (this.registrations.isEmpty()) {
return null;
}
// 这个 Map其实很简单 key 为URL的匹配模式,value为 handler 即处理器
// 将这个 HandlerMapping 需要处理的所有URL的匹配模式和对应的处理器用urlMap存起来,用于初始化 HandlerMapping 实现类实例
Map<String, HttpRequestHandler> urlMap = new LinkedHashMap<>();
// 遍历注册信息
for (ResourceHandlerRegistration registration : this.registrations) {
// 每一个注册信息的每一个路径模板,都要单独添加一个映射关系,添加到urlMap中,当然这也很正常
for (String pathPattern : registration.getPathPatterns()) {
// 调用 registration.getRequestHandler(); 获取此注册信息注册的 handler,即 ResourceHttpRequestHandler
// 一个 ResourceHandlerRegistration 创建一个 ResourceHttpRequestHandler
// 如果 ResourceHandlerRegistration 中有多个 路径模板,那么这些路径模板公用一个 ResourceHttpRequestHandler
ResourceHttpRequestHandler handler = registration.getRequestHandler();
if (this.pathHelper != null) {
handler.setUrlPathHelper(this.pathHelper);
}
if (this.contentNegotiationManager != null) {
handler.setContentNegotiationManager(this.contentNegotiationManager);
}
handler.setServletContext(this.servletContext);
handler.setApplicationContext(this.applicationContext);
try {
// 因为 ResourceHttpRequestHandler 实现了 InitializingBean 接口,而 registration.getRequestHandler(); 中创建ResourceHttpRequestHandler实例是我们手动new出来的,并不是让IOC容器来初始化的,
// 所以合理需要手动调用 afterPropertiesSet 方法来初始化
handler.afterPropertiesSet();
}
catch (Throwable ex) {
throw new BeanInitializationException("Failed to init ResourceHttpRequestHandler", ex);
}
urlMap.put(pathPattern, handler);
}
}
// 最终返回的是一个 SimpleUrlHandlerMapping
return new SimpleUrlHandlerMapping(urlMap, this.order);
}
}
ResourceChainRegistration
在看 ResourceHandlerRegistration
前,先看看 ResourceChainRegistration
,
ResourceChainRegistration
的作用是注册资源处理链,这个资源处理链最终是要应用到静态资源处理器 ResourceHttpRequestHandler
之中的。
处理链有两种,解析链和转换连,对应着两个字段 List<ResourceResolver> resolvers
解析链和 List<ResourceTransformer> transformers
转换链
-
ResourceResolver
:解析请求找到请求对应到服务器端的静态资源。 -
ResourceTransformer
:用于按照一定的规则修改资源内容。一般为空
有关 ResourceResolver 和 ResourceTransformer 的分析,请看《SpringMVC 中的静态资源处理》中的 ResourceResolver 小节和 ResourceTransformer 小节
源码分析
public class ResourceChainRegistration {
private static final String DEFAULT_CACHE_NAME = "spring-resource-chain-cache";
private static final boolean isWebJarsAssetLocatorPresent = ClassUtils.isPresent(
"org.webjars.WebJarAssetLocator", ResourceChainRegistration.class.getClassLoader());
// 解析链
private final List<ResourceResolver> resolvers = new ArrayList<>(4);
// 转换链
private final List<ResourceTransformer> transformers = new ArrayList<>(4);
// 解析链中是否 VersionResourceResolver
private boolean hasVersionResolver;
// 解析链中是否 PathResourceResolver
private boolean hasPathResolver;
// 解析链中是否 WebJarsResourceResolver
private boolean hasWebjarsResolver;
// 转换链中是否 CssLinkResourceTransformer
private boolean hasCssLinkTransformer;
// 构造函数,根据参数是否添加缓存决定是否在解析链中添加CachingResourceResolver同时在缓存链中添加CachingResourceTransformer
public ResourceChainRegistration(boolean cacheResources) {
this(cacheResources, (cacheResources ? new ConcurrentMapCache(DEFAULT_CACHE_NAME) : null));
}
// 构造函数,根据参数是否添加缓存决定是否在解析链中添加CachingResourceResolver同时在缓存链中添加CachingResourceTransformer
public ResourceChainRegistration(boolean cacheResources, @Nullable Cache cache) {
Assert.isTrue(!cacheResources || cache != null, "'cache' is required when cacheResources=true");
if (cacheResources) {
this.resolvers.add(new CachingResourceResolver(cache));
this.transformers.add(new CachingResourceTransformer(cache));
}
}
// 添加解析链中的解析器的时候,判断 hasVersionResolver,hasPathResolver,hasWebjarsResolver
public ResourceChainRegistration addResolver(ResourceResolver resolver) {
Assert.notNull(resolver, "The provided ResourceResolver should not be null");
this.resolvers.add(resolver);
if (resolver instanceof VersionResourceResolver) {
this.hasVersionResolver = true;
}
else if (resolver instanceof PathResourceResolver) {
this.hasPathResolver = true;
}
else if (resolver instanceof WebJarsResourceResolver) {
this.hasWebjarsResolver = true;
}
return this;
}
// 添加转换链中的转换器的时候,判断 hasCssLinkTransformer
public ResourceChainRegistration addTransformer(ResourceTransformer transformer) {
Assert.notNull(transformer, "The provided ResourceTransformer should not be null");
this.transformers.add(transformer);
if (transformer instanceof CssLinkResourceTransformer) {
this.hasCssLinkTransformer = true;
}
return this;
}
// 获取解析链 ,此方法保证了解析链中至少包含了一个解析器,那就是PathResourceResolver
protected List<ResourceResolver> getResourceResolvers() {
// PathResourceResolver 一般都是解析链的默认的最后一个兜底的解析器,如果这个都没有,有可能是忘了加这个解析器,也有可能是一个解析器都没加
if (!this.hasPathResolver) {
// 如果没有,那就加上去
List<ResourceResolver> result = new ArrayList<>(this.resolvers);
// 先判断要不要加 WebJarsResourceResolver
if (isWebJarsAssetLocatorPresent && !this.hasWebjarsResolver) {
result.add(new WebJarsResourceResolver());
}
// 直接加PathResourceResolver
result.add(new PathResourceResolver());
return result;
}
return this.resolvers;
}
// 获取转换链,如果添加了 VersionResourceResolver 解析器但是没有添加 CssLinkResourceTransformer ,这里自动加上,而且加在最前面,如果前面有 CachingResourceTransformer,那就加在CachingResourceTransformer的后面
protected List<ResourceTransformer> getResourceTransformers() {
if (this.hasVersionResolver && !this.hasCssLinkTransformer) {
List<ResourceTransformer> result = new ArrayList<>(this.transformers);
boolean hasTransformers = !this.transformers.isEmpty();
boolean hasCaching = hasTransformers && this.transformers.get(0) instanceof CachingResourceTransformer;
result.add(hasCaching ? 1 : 0, new CssLinkResourceTransformer());
return result;
}
return this.transformers;
}
}
ResourceHandlerRegistration
ResourceHandlerRegistration
封装了创建一个静态资源处理器去即 ResourceHttpRequestHandler
所需要的信息,主要分几块,路径匹配模板(ResourceHandlerRegistration#pathPatterns
),资源查找文件夹(ResourceHandlerRegistration#locationValues
),handler 中的资源处理链(包含解析链和转换链,ResourceHandlerRegistration#resourceChainRegistration
),HTTP 响应中的缓存配置(ResourceHandlerRegistration#cachePeriod
、ResourceHandlerRegistration#cacheControl
、ResourceHandlerRegistration#useLastModified
),
看源码
public class ResourceHandlerRegistration {
// 此静态资源handler对应的路径模板
private final String[] pathPatterns;
// URL对应的资源到哪里去查,即资源查找文件夹
private final List<String> locationValues = new ArrayList<>();
// 静态资源处理链信息注册,主要有两条链,解析链和转换链
@Nullable
private ResourceChainRegistration resourceChainRegistration;
// 用于配置缓存,直接设置 Cache-Control 为 max-age 等于指定的值,跟cacheControl的区别还不清楚
// 一般用 cacheControl ,不用 cachePeriod
@Nullable
private Integer cachePeriod;
// 用于配置缓存,设置 响应的消息头中的 Cache-Control 消息头
// 一般用 cacheControl ,不用 cachePeriod
@Nullable
private CacheControl cacheControl;
// 用于配置缓存,是否使用请求的消息头中的 LastModified 消息头
private boolean useLastModified = true;
// 各个字段的getter,setter方法
......
// 获取注册信息配置的handler,即 ResourceHttpRequestHandler
// 这个方法在ResourceHandlerRegistry的getHandlerMapping中被调用
protected ResourceHttpRequestHandler getRequestHandler() {
// 直接new一个ResourceHttpRequestHandler
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
// 根据资源处理链中注册的处理链信息,初始化 ResourceHttpRequestHandler
if (this.resourceChainRegistration != null) {
// 设置资源解析链,解析资源最终的地址
handler.setResourceResolvers(this.resourceChainRegistration.getResourceResolvers());
// 设置资源转换链,对资源内容进行更改
handler.setResourceTransformers(this.resourceChainRegistration.getResourceTransformers());
}
handler.setLocationValues(this.locationValues);
// 设置ResourceHttpRequestHandler中的缓存信息
if (this.cacheControl != null) {
handler.setCacheControl(this.cacheControl);
}
else if (this.cachePeriod != null) {
handler.setCacheSeconds(this.cachePeriod);
}
handler.setUseLastModified(this.useLastModified);
return handler;
}
}
ResourceHttpRequestHandler
ResourceHandlerRegistration#getRequestHandler
中对 ResourceHttpRequestHandler
的初始化到底有什么含义?
请看《SpringMVC 中的静态资源处理》
实践
请看《SpringMVC 中的静态资源处理》
addCorsMappings
TODO
configureViewResolvers
已经接触过了,TODO,以后有时间在细细研究
视图解析相关的分析和实践,看《SpringMVC- 第五篇:视图》
实践
配置 ThymeleafViewResolver
,即 Thymeleaf 视图解析器
//------------------------------ 配置视图解析器 ---------------------------------------------------
// 对应xml中的
// <bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
// <!-- 可配置多个 ThymeleafViewResolver ,order设置采用顺序-->
// <property name="order" value="1"/>
// <property name="characterEncoding" value="UTF-8"/>
// <property name="templateEngine">
// <bean class="org.thymeleaf.spring5.SpringTemplateEngine">
// <property name="templateResolver">
// <bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
// <!-- 视图解析策略-->
// <!-- 视图前缀 -->
// <property name="prefix" value="/WEB-INF/templates/"/>
// <!-- 视图后缀 -->
// <property name="suffix" value=".html"/>
// <property name="templateMode" value="HTML5"/>
// <property name="characterEncoding" value="UTF-8"/>
// </bean>
// </property>
// </bean>
// </property>
// </bean>
//配置生成模板解析器
@Bean
public ITemplateResolver templateResolver(WebApplicationContext webApplicationContext) {
// 直接通过IOC诸如即可,不需要手动获取
// 不过这个写法也可以算是手动获取的方式
//WebApplicationContext webApplicationContext = ContextLoader.getCurrentWebApplicationContext();
// ServletContextTemplateResolver需要一个ServletContext作为构造参数,可通过WebApplicationContext 的方法获得
ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(webApplicationContext.getServletContext());
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
templateResolver.setCharacterEncoding("UTF-8");
templateResolver.setTemplateMode(TemplateMode.HTML);
return templateResolver;
}
//生成模板引擎并为模板引擎注入模板解析器
@Bean
public SpringTemplateEngine templateEngine(ITemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
//生成视图解析器并未解析器注入模板引擎
@Bean
public ViewResolver viewResolver(SpringTemplateEngine templateEngine) {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setCharacterEncoding("UTF-8");
viewResolver.setTemplateEngine(templateEngine);
return viewResolver;
}
addArgumentResolvers
调用栈
WebMvcConfigurationSupport#getArgumentResolvers
的作用是获取所有的用户自定义的参数解析器,这个方法很简单
protected final List<HandlerMethodArgumentResolver> getArgumentResolvers() {
if (this.argumentResolvers == null) {
this.argumentResolvers = new ArrayList<>();
// 调用所有的 Web配置类(一般都实现了WebMvcConfigurer接口)中的 addArgumentResolvers 方法
// 自定义的参数解析器会跟默认的参数解析器一起生效,具体的顺序,请看 《SpringMVC-RequestMappingHandlerAdapter源码解析.md》 的 参数解析器 的 初始化 小节
addArgumentResolvers(this.argumentResolvers);
}
return this.argumentResolvers;
}
我们细看 WebMvcConfigurationSupport#addArgumentResolvers
方法,可以注意到,在 WebMvcConfigurationSupport
中,这个方法是空的,意思是留给子类重写,WebMvcConfigurationSupport
只有两个子类,一个是 DelegatingWebMvcConfiguration
,我们直接看 DelegatingWebMvcConfiguration#addArgumentResolvers
。
另一个是
StandaloneConfiguration
,是StandaloneMockMvcBuilder
的内部类(从类名可以推断出是用于测试的 Mock 类),直接忽略
DelegatingWebMvcConfiguration#addArgumentResolvers
实际上委托给 DelegatingWebMvcConfiguration#configurers
,类型为 WebMvcConfigurerComposite
,包含了所有的 Web 配置类中的信息。
具体细节,看后面的小节
DelegatingWebMvcConfiguration
中查看具体细节
经过了所有 Web 配置类中的 addArgumentResolvers
方法的修改,最终,在 WebMvcConfigurationSupport#getArgumentResolvers
中获取了所有的用户自定义的参数解析器。
WebMvcConfigurationSupport#getArgumentResolvers
方法,在 WebMvcConfigurationSupport
方法中被多次调用
-
WebMvcConfigurationSupport#requestMappingHandlerAdapter
返回RequestMappingHandlerAdapter
-
WebMvcConfigurationSupport#addDefaultHandlerExceptionResolvers
返回类型为 void
我们重点看 WebMvcConfigurationSupport#requestMappingHandlerAdapter
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcValidator") Validator validator) {
RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
adapter.setContentNegotiationManager(contentNegotiationManager);
adapter.setMessageConverters(getMessageConverters());
adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer(conversionService, validator));
// 通过 getArgumentResolvers() 获取所有用户自定义的参数解析器之后,通过 setCustomArgumentResolvers 方法将其设置到 RequestMappingHandlerAdapter#customArgumentResolvers 属性中
// 注意,这个属性中包含的参数解析器会跟默认添加的参数解析器一起生效,他们之前的顺序关系,请看 《SpringMVC-RequestMappingHandlerAdapter源码解析.md》 的 参数解析器 的 初始化 小节
adapter.setCustomArgumentResolvers(getArgumentResolvers());
adapter.setCustomReturnValueHandlers(getReturnValueHandlers());
if (jackson2Present) {
adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice()));
adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
}
AsyncSupportConfigurer configurer = new AsyncSupportConfigurer();
configureAsyncSupport(configurer);
if (configurer.getTaskExecutor() != null) {
adapter.setTaskExecutor(configurer.getTaskExecutor());
}
if (configurer.getTimeout() != null) {
adapter.setAsyncRequestTimeout(configurer.getTimeout());
}
adapter.setCallableInterceptors(configurer.getCallableInterceptors());
adapter.setDeferredResultInterceptors(configurer.getDeferredResultInterceptors());
return adapter;
}
通过 getArgumentResolvers
获取所有用户自定义的参数解析器然后将其设置到 RequestMappingHandlerAdapter#customArgumentResolvers
属性中之后,这些用户自定义的参数解析器会跟系统默认添加的参数解析器一起生效,他们之前的顺序关系,请看 《SpringMVC-RequestMappingHandlerAdapter 源码解析.md》的 参数解析器
的 初始化
小节。
实践
请看《SpringMVC-RequestMappingHandlerAdapter 源码解析.md》的 自定义参数解析器
小节
addReturnValueHandlers
调用栈
WebMvcConfigurationSupport#getReturnValueHandlers
的作用是获取所有的用户自定义的返回值处理器,这个方法很简单
protected final List<HandlerMethodReturnValueHandler> getReturnValueHandlers() {
if (this.returnValueHandlers == null) {
this.returnValueHandlers = new ArrayList<>();
// 调用所有的 Web配置类(一般都实现了WebMvcConfigurer接口)中的 addReturnValueHandlers 方法
// 自定义的返回值处理器会跟默认的返回值处理器一起生效,具体的顺序,请看 《SpringMVC-RequestMappingHandlerAdapter源码解析.md》 的 返回值处理器 的 初始化 小节
addReturnValueHandlers(this.returnValueHandlers);
}
return this.returnValueHandlers;
}
我们细看 WebMvcConfigurationSupport#addReturnValueHandlers
方法,可以注意到,在 WebMvcConfigurationSupport
中,这个方法是空的,意思是留给子类重写,WebMvcConfigurationSupport
只有两个子类,一个是 DelegatingWebMvcConfiguration
,我们直接看 DelegatingWebMvcConfiguration#addReturnValueHandlers
。
另一个是
StandaloneConfiguration
,是StandaloneMockMvcBuilder
的内部类(从类名可以推断出是用于测试的 Mock 类),直接忽略
DelegatingWebMvcConfiguration#addReturnValueHandlers
实际上委托给 DelegatingWebMvcConfiguration#configurers
,类型为 WebMvcConfigurerComposite
,包含了所有的 Web 配置类中的信息。
具体细节,看后面的小节
DelegatingWebMvcConfiguration
中查看具体细节
经过了所有 Web 配置类中的 addReturnValueHandlers
方法的修改,最终,在 WebMvcConfigurationSupport#getReturnValueHandlers
中获取了所有的用户自定义的返回值处理器。
WebMvcConfigurationSupport#getReturnValueHandlers
方法,在 WebMvcConfigurationSupport
方法中被多次调用
-
WebMvcConfigurationSupport#requestMappingHandlerAdapter
返回RequestMappingHandlerAdapter
-
WebMvcConfigurationSupport#addDefaultHandlerExceptionResolvers
返回类型为 void
我们重点看 WebMvcConfigurationSupport#requestMappingHandlerAdapter
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcValidator") Validator validator) {
RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
adapter.setContentNegotiationManager(contentNegotiationManager);
adapter.setMessageConverters(getMessageConverters());
adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer(conversionService, validator));
adapter.setCustomArgumentResolvers(getArgumentResolvers());
// 通过 getReturnValueHandlers() 获取所有用户自定义的返回值处理器之后,通过 setCustomReturnValueHandlers 方法将其设置到 RequestMappingHandlerAdapter#customReturnValueHandlers 属性中
// 注意,这个属性中包含的返回值处理器会跟默认添加的返回值处理器一起生效,他们之前的顺序关系,请看 《SpringMVC-RequestMappingHandlerAdapter源码解析.md》 的 返回值处理器 的 初始化 小节
adapter.setCustomReturnValueHandlers(getReturnValueHandlers());
if (jackson2Present) {
adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice()));
adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
}
AsyncSupportConfigurer configurer = new AsyncSupportConfigurer();
configureAsyncSupport(configurer);
if (configurer.getTaskExecutor() != null) {
adapter.setTaskExecutor(configurer.getTaskExecutor());
}
if (configurer.getTimeout() != null) {
adapter.setAsyncRequestTimeout(configurer.getTimeout());
}
adapter.setCallableInterceptors(configurer.getCallableInterceptors());
adapter.setDeferredResultInterceptors(configurer.getDeferredResultInterceptors());
return adapter;
}
通过 getReturnValueHandlers
获取所有用户自定义的返回值处理器然后将其设置到 RequestMappingHandlerAdapter#customReturnValueHandlers
属性中之后,这些用户自定义的返回值处理器会跟系统默认添加的返回值处理器一起生效,他们之前的顺序关系,请看 《SpringMVC-RequestMappingHandlerAdapter 源码解析.md》的 返回值处理器
的 初始化
小节。
实践
请看《SpringMVC-RequestMappingHandlerAdapter 源码解析.md》的 自定义返回值解析器
小节
configureHandlerExceptionResolvers & extendHandlerExceptionResolvers
已经接触过了,TODO,以后有时间在细细研究
异常解析相关的分析和实践,看《SpringMVC- 第七篇:控制器方法异常处理》
从这里开始,TODO
@Bean
public HandlerExceptionResolver handlerExceptionResolver(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
configureHandlerExceptionResolvers(exceptionResolvers);
if (exceptionResolvers.isEmpty()) {
addDefaultHandlerExceptionResolvers(exceptionResolvers, contentNegotiationManager);
}
extendHandlerExceptionResolvers(exceptionResolvers);
HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
composite.setOrder(0);
composite.setExceptionResolvers(exceptionResolvers);
return composite;
}
实践
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();
Properties prop = new Properties();
prop.setProperty("java.lang.ArithmeticException", "error");
//设置异常映射
exceptionResolver.setExceptionMappings(prop);
//设置共享异常信息的键
exceptionResolver.setExceptionAttribute("ex");
resolvers.add(exceptionResolver);
}
getValidator
在使用注解配置 SpringMVC 的时候,在 WebMvcConfigurationSupport 的 mvcValidator 方法中,根据是否存在 javax.validation.Validator
判断,是否生成 OptionalValidatorFactoryBean
;
public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
...
@Bean
public Validator mvcValidator() {
Validator validator = getValidator();
if (validator == null) {
if (ClassUtils.isPresent("javax.validation.Validator", getClass().getClassLoader())) {
Class<?> clazz;
try {
String className = "org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean";
clazz = ClassUtils.forName(className, WebMvcConfigurationSupport.class.getClassLoader());
} catch (ClassNotFoundException | LinkageError ex) {
throw new BeanInitializationException("Failed to resolve default validator class", ex);
}
validator = (Validator) BeanUtils.instantiateClass(clazz);
} else {
validator = new NoOpValidator();
}
}
return validator;
}
...
}
getMessageCodesResolver
TODO
常用 Bean
文件上传
在 Web 配置类中添加 Bean。
@Bean
public CommonsMultipartResolver multipartResolver(){
return new CommonsMultipartResolver();
}
跟文件上传相关的 xml 配置和使用看《SpringMVC- 第三篇:在控制器方法中获取请求参数和构建响应》的
上传下载 - 处理 MediaType为 multipart/* 的请求
小节
Java 配置 SpringMVC 的源码分析
@EnableWebMvc 注解
将 @EnableWebMvc
注解添加到 @Configuration
修饰的类中会从 WebMvcConfigurationSupport
导入 Spring MVC 配置,当我们想要自定义导入的配置的时候,可以实现接口 WebMvcConfigurer
来重写各个方法。
注意: 只有一个 @Configuration
类可以使用 @EnableWebMvc
注释来导入 Spring Web MVC 配置。但是,可以有多个 @Configuration
类实现 WebMvcConfigurer
,以便自定义提供的配置。
查看注解源码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}
主要是通过 @Import(DelegatingWebMvcConfiguration.class)
引入了 DelegatingWebMvcConfiguration
这个配置类,DelegatingWebMvcConfiguration
继承了 WebMvcConfigurationSupport
,
当我们有多个 Web 配置类的时候,只需要在其中一个上面配置 @EnableWebMvc
注解即可,当然也可以每个 Web 配置类都使用 @EnableWebMvc
注解,即使这样写了,也只会引入一次 DelegatingWebMvcConfiguration
。
WebMvcConfigurationSupport
稍微看一下 WebMvcConfigurationSupport
的注释,了解到此类是一个帮助用户实现 Java 配置 SpringMVC 的类(所以后缀为 Support),它通常是通过在 @Configuration
修饰的类上再添加 @EnableWebMvc
来进行使用,当然你也可以让 @Configuration
修饰的类直接继承此类,这个类的作用跟通过 XML 配置 SpringMVC 中的 <mvc:annotation-driven>
标签很像,好家伙,连注释都跟 AnnotationDrivenBeanDefinitionParser
很像,继承此类后,会自动引入各种 HandlerMappings
、HandlerAdapters
、HandlerExceptionResolvers
等等各种请求处理过程中使用的组件。
具体引入了哪些组件,得学习学习,TODO
在静态代码块中根据第三方库是否存在来确定全局变量。
static {
ClassLoader classLoader = WebMvcConfigurationSupport.class.getClassLoader();
// 默认为false
romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader);
// 默认为true
jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
// 默认为false,引入 com.fasterxml.jackson.core 下的 jackson-databind 依赖后为trur
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
// 默认为false,引入 com.fasterxml.jackson.dataformat 下的 jackson-dataformat-xml 依赖后为trur
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
// 默认为false
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
// 默认为false
jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
// 默认为false
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
// 默认为false
jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
// 默认为false
kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader);
}
ResourceUrlProvider
这是静态资源处理的一个的中心 Bean,通过 ResourceUrlProvider#getHandlerMap
可以获取所有的跟静态资源处理相关的 handler(ResourceHttpRequestHandler
类型)以及其映射的 url 模板,ResourceUrlProvider
的主要功能是获得提供给客户端的用于访问静态资源的公共的 URL 路径。这个组件知道用于服务静态资源的 Spring MVC 处理程序映射,并使用已配置的 ResourceHttpRequestHandlers
的 ResourceResolver
链来做出决策。非常方便。
ResourceUrlProvider
主要是用在 WebMvcConfigurationSupport#getInterceptors
方法中,用于初始化一个 ResourceUrlProviderExposingInterceptor
。ResourceUrlProviderExposingInterceptor
的功能就是将 WebMvcConfigurationSupport#mvcResourceUrlProvider
方法配置的 ResourceUrlProvider
实例 bean 添加到请求域属性中,属性名为 ResourceUrlProvider.class.getName()
,具体名称为 org.springframework.web.servlet.resource.ResourceUrlProvider
。然后 ResourceUrlProvider
具体主要是用在了 ResourceTransformerSupport
和 ResourceUrlEncodingFilter
中。
请求域中包含了好多方便的工具,而且大部分都是在控制器拦截器(HandlerInterceptor)中添加的
WebMvcConfigurationSupport#mvcResourceUrlProvider
方法
@Bean
public ResourceUrlProvider mvcResourceUrlProvider() {
// 直接new一个 ResourceUrlProvider 对象
ResourceUrlProvider urlProvider = new ResourceUrlProvider();
// 只依赖于 getPathMatchConfigurer() 配置
urlProvider.setUrlPathHelper(getPathMatchConfigurer().getUrlPathHelperOrDefault());
urlProvider.setPathMatcher(getPathMatchConfigurer().getPathMatcherOrDefault());
return urlProvider;
}
ResourceUrlProvider
实现了 ApplicationListener<ContextRefreshedEvent>
,即此类的 onApplicationEvent
方法会在 ContextRefreshedEvent
发生的时候被触发,我们直接看 onApplicationEvent
方法
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 是否自动探测,默认为 true
if (isAutodetect()) {
// 清空缓存
this.handlerMap.clear();
// 找到IOC容器中所有SimpleUrlHandlerMapping类型的bean,然后检查其中配置的url对应的handler的类型是不是 ResourceHttpRequestHandler,如果是,则将url和ResourceHttpRequestHandler类型的handler实例保存在handlerMap中
// 这就是有针对性的查找 ResourceHttpRequestHandler
detectResourceHandlers(event.getApplicationContext());
if (!this.handlerMap.isEmpty()) {
this.autodetect = false;
}
}
}
注意对 ResourceUrlProvider#detectResourceHandlers
的调用,这个方法的功能是找到 IOC 容器中所有 SimpleUrlHandlerMapping
类型的 bean,然后检查其中配置的 url 对应的 handler 的类型是不是 ResourceHttpRequestHandler
,如果是,则将 url 和 ResourceHttpRequestHandler
类型的 handler 实例保存在 handlerMap 中。这就是有针对性的查找 ResourceHttpRequestHandler
,因为我们在注册 ResourceHttpRequestHandler
的时候,就是配合 SimpleUrlHandlerMapping
一起配置的。
获取了所有的跟静态资源处理相关的 url 和 handler 之后,就可以提供查找资源路径的方法,也是这个类的名字 ResourceUrlProvider
的含义,具体方法就是 getForLookupPath
和 getForRequestUrl
,具体功能为传入资源访问路径(注意,这个路径不是静态资源的绝对路径,而是匹配解析前的访问路径,可以简单理解为在写在 html 中写的或者在浏览器 URL 栏中填写的路径),然后将此路径与已配置的 ResourceHttpRequestHandler
的映射路径进行比较,如果匹配,则使用匹配路径对应的 ResourceHttpRequestHandler
的 ResourceResolver
解析链来解析要公开供公众使用的 URL 路径。
一般情况下,查找的路径和返回的路径都是一样的,但是有的情况下会不一样,比如如果 ResourceHttpRequestHandler
的 ResourceResolver
解析链中包含了 VersionResourceResolver
,会在静态资源内容(注意是内容不是修改时间)变化之后,改变资源的版本信息,访问此静态资源的 URL 也需要变更版本信息才能正常访问,此时,如果我们调用 getForLookupPath
,传入 /resourcesVersion/js/echarts.js
,经过 ResourceHttpRequestHandler
的 ResourceResolver
解析链的渲染之后,返回的 URL 就是 /resourcesVersion/js/echarts-47c29e85d7a1dc30a219eb423bea3bb2.js
,实际上 ResourceUrlEncodingFilter
就是这么用的。
我们还可以使用此方法判断,当前配置的所有的静态资源解析是否可以解析传入的资源访问路径,如果路径不正确,或者没有响应的 handler 解析此路径,最终就会返回 null。
实践起来也很简单,直接在控制器发方法中通过属性 ResourceUrlProvider.class.getName()
,获取 ResourceUrlProvider
即可
Web 配置
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 添加静态资源映射路径
registry.addResourceHandler("/resourcesSimple/**")
.addResourceLocations("/resourcesSimple/", "classpath:/staticSimple/","file:/C:\\test/")
.setCacheControl(CacheControl.maxAge(20, TimeUnit.SECONDS));
WebMvcConfigurer.super.addResourceHandlers(registry);
}
添加控制器
@RestController
@RequestMapping("/ResourceUrlTest")
public class ResourceUrlTestController {
@RequestMapping("/resourceURL")
public String testResourceUrlProvider(@RequestAttribute(value = "org.springframework.web.servlet.resource.ResourceUrlProvider") ResourceUrlProvider provider) {
String allResourceMap = provider.getHandlerMap().toString();
// 存在,可以成功渲染,结果跟 输入一样为 /resourcesSimple/images/aaa.jpg
String url = provider.getForLookupPath("/resourcesSimple/images/aaa.jpg");
// 不存在,为 null
String url1 = provider.getForLookupPath("/resourcesSimple/images/bbb.jpg");
return "allResourceMap:"+allResourceMap+"\n" +"url:" + url + "\n" + "url1:" + url1;
}
}
实际上,aaa.jpg
存在,bbb.jpg
不存在
访问 http://localhost:8080/SpringMVC_AnnotationConfig/ResourceUrlTest/resourceURL
页面返回
allResourceMap:{/resourcesSimple/**=ResourceHttpRequestHandler ["/resourcesSimple/", "classpath:/staticSimple/", "file:/C:\test/"]}
url:/resourcesSimple/images/aaa.jpg
url1:null
DelegatingWebMvcConfiguration
@EnableWebMvc
注解实际引入的配置类,这个类,才是整个 Web 应用在 Web 层的主体配置类。
DelegatingWebMvcConfiguration
是 WebMvcConfigurationSupport
的一个子类,探测所有类型为 WebMvcConfigurer
的 bean(也就是被 @Configuration
修饰同时也实现了 WebMvcConfigurer
接口的类,实际上就是我们经常写的 Web 配置类)汇总到 WebMvcConfigurerComposite
字段中,然后再重写从 WebMvcConfigurationSupport
中继承的方法的时候,委托给 WebMvcConfigurerComposite
来读取各个 Web 配置类的相应信息,简而言之是DelegatingWebMvcConfiguration 是对多个 Web 配置类的配置信息的聚合。
DelegatingWebMvcConfiguration 源码分析:
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
// 聚合所有Web配置类中的配置
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
// 自动注入所有实现了 WebMvcConfigurer 接口的bean,实际上也只有Web配置类会去实现 WebMvcConfigurer 接口
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
}
// 对 WebMvcConfigurationSupport 中的空方法的实现,基本上都委托为 configurers 了
......
}
可以这么理解:(在不考虑@Bean 方法的时候)一个 Web 配置类只有实现了 WebMvcConfigurer
接口并重写了相关方法(重写即配置)才算是有效的 Web 配置,因为如果 Web 配置类仅添加 @EnableWebMvc
注解,不实现 WebMvcConfigurer
接口(实现了此接口才能对从 WebMvcConfigurationSupport
中默认导入的配置进行重写),那么就相当于把默认配置又导入了一遍,但是实际上默认配置即使导入多次也只会生效一次(回看 @EnableWebMvc
注解),所以最终相当于啥也没干,所以,只有实现了 WebMvcConfigurer
接口并重写了相关方法(重写即配置)的 Web 配置类才是有效的 Web 配置类,所以 DelegatingWebMvcConfiguration#setConfigurers
方法才只考虑实现了 WebMvcConfigurer
接口的 bean。
多个同时生效 Web 配置中的@Bean 方法配置的 Bean 的关系,可以看下一小节
那如何理解 Web 配置呢?
从 WebMvcConfigurationSupport
中导入的默认配置只会导入一次,那么我们通过实现 WebMvcConfigurer
接口对其的自定义就相当于我们自己的自定义方案,重写 AbstractAnnotationConfigDispatcherServletInitializer#getServletConfigClasses
传入需要生效的 We 配置类时,我们可以一次性传入多个,也就是说自定义方案可以存在多个,那么我们可以将一个大的自定义配置方案,拆成很多个小的自定义配置方案,进行细粒度的管理,同时,每一个自定义配置方案都可以存在多个版本,根据实际情况,想用哪个版本就用哪个版本,最终结果甚至可以是在运行时动态决定的。
AbstractAnnotationConfigDispatcherServletInitializer#getServletConfigClasses
中传入的多个 Web 配置类的与,也就是同时生效的,那么多个 Web 配置可能会出现冲突,此时要注意 Web 配置类的顺序,排在后面的 Web 配置会排在前面的 Web 配置的基础上设置,即你在传进来的参数上能看到之前的 Web 配置的内容。Web 配置类的顺序取决于注入 DelegatingWebMvcConfiguration#setConfigurers
的时候,List<WebMvcConfigurer> configurers
中的 Web 配置类的顺序,而这个顺序,取决于 Bean 的顺序,此时我么可以用 @Order
注解来调整 Web 配置类的顺序,比如
@Configuration
@ComponentScan("xyz.xiashuo.springmvcannotationconfig")
@EnableWebMvc
@Order(2)
public class WebConfig implements WebMvcConfigurer {
......
}
@Configuration
@ComponentScan("xyz.xiashuo.springmvcannotationconfig")
@Order(1)
public class WebConfig2 implements WebMvcConfigurer {
......
}
然后注入的时候,顺序果然是 WebConfig2
在前,WebConfig
在后

WebMvcConfigurerComposite
实现 WebMvcConfigurer
接口,功能是对实现了 WebMvcConfigurer
的 Web 配置类进行简单的聚合,代码很简单,这里就不细看了。
多个同时生效 Web 配置中的@Bean 方法配置的 Bean
跟多个同时生效 Web 配置中重写了 WebMvcConfigurer
接口的方法一样,根据 @Order
注解,排在后面的 Web 配置会排在前面的 Web 配置的基础上设置。分两种情况
-
同名的 bean,根据 order,排在后面的 Web 配置类中的定义覆盖前面的 Web 配置类中的定义
-
不同名的 bean,共存。
TODO
我已经了解过的,还有三个没有学习
-
configureViewResolvers
-
configureHandlerExceptionResolvers & extendHandlerExceptionResolvers
-
getValidator
其余的,还在学习的,类型转换,数据绑定
- addFormatters,
将来要重点学习的
- configureAsyncSupport
还有很多我没有掌握,但是我现在得去学习其他的东西了。