本文档基于Spring 5.1.4.RELEASE
版本进行编写,高版本的代码中会多出了一些关于WebFlux
技术的内容。
Spring Web MVC是基于Servlet API构建的原始Web框架, 从一开始就包含在Spring Framework中。
正式名称"Spring Web MVC"来自其源模块(spring-webmvc)的名称,但它通常被称为"Spring MVC"。
Spring MVC
是基于DispatcherServlet
展开的(Spring WebFlux
是DispatcherHandler
.)
![](https://img.haomeiwen.com/i16156434/6f6f026631550c93.png)
1 Spring MVC的配置
Spring MVC有多种配置方式
1.web.xml方式配置
2.Java方式配置 【多种写法但都与ServletContainerInitializer有关】
关于使用Java代码的配置方式除了下例,还可以通过继承 AbstractContextLoaderInitializer 或者它的两个子类来完成配置,这里不做展示。
这里展示Spring MVC
的2种配置方式,下文是根据展示的Xml形式讲解的。
<servlet>
<servlet-name>SpringMVCConfig</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:config/spring-mvc-config.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVCConfig</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
XmlWebApplicationContext appContext = new XmlWebApplicationContext();
appContext.setConfigLocation("classpath:config/spring-mvc-config.xml");
DispatcherServlet servlet = new DispatcherServlet(ac);
ServletRegistration.Dynamic registration = servletContext.addServlet("SpringMVCConfig", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/");
}
}
2 Spring MVC 框架启动过程分析
最开始的图中可以看出
DispatcherServlet
继承自GenericServlet
,那么它就会遵守Servlet
的生命周期进行运作。
由于在web.xml
中添加了DispatcherServlet类
的Servlet
节点,所以Servlet容器
在启动时会调用DispatcherServlet#init(ServletConfig)
来实例化DispatcherServlet对象
,这是Servlet技术的逻辑。
2.1 DispatcherServlet类的构造方法
public DispatcherServlet() {
super();
setDispatchOptionsRequest(true);
}
public DispatcherServlet(WebApplicationContext webApplicationContext) {
super(webApplicationContext);
setDispatchOptionsRequest(true);
}
// FrameworkServlet#setDispatchOptionsRequest(boolean)
public void setDispatchOptionsRequest(boolean dispatchOptionsRequest) {
this.dispatchOptionsRequest = dispatchOptionsRequest;
}
2.2 DispatcherServlet类的分析
依照类关系图从下向上寻找init()
方法,最终会在HttpServletBean类
中找到。
注意:
1.init()
方法是GenericServlet
类提供的,会被init(ServletConfig)
主动调用,Servlet
接口中并没有init()
方法的声明。
2.init()
方法内的逻辑代表着Spring MVC的启动过程,init()
方法执行完成代表着Spring MVC框架的启动完成
// HttpServletBean
@Override
public final void init() throws ServletException {
// Set bean properties from init parameters.
// 获取web.xml中的<init-param>参数
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
/*
这里的 this 是 DispatcherServlet的实例对象。
通过 BeanWrapper 将 web.xml中 DispatcherServlet里的 <init-param>所配置的内容填充到 DispatcherServlet的实例对象内,
也就是将名为 contextConfigLocation 的属性值填充到 DispatcherServlet对象内 (这个属性定义在FrameworkServlet类中)。
BeanWrapper 在这可以简单理解为类似通过反射将属性填充到对象内
*/
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw); // 一个空钩子,也就是说内部没任何逻辑
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}
// Let subclasses do whatever initialization they like.
// (让子类自己定义过程) 我们主要看这里
initServletBean();
}
// FrameworkServlet
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
if (logger.isInfoEnabled()) {
logger.info("Initializing Servlet '" + getServletName() + "'");
}
long startTime = System.currentTimeMillis();
try {
// 这里是关注点
// 最终将处理过的ApplicationContext赋给FrameworkServlet类中的属性
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet(); // 这是个钩子, 空实现
}
catch (ServletException | RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}
// ... 一些日志操作
}
先看一下initWebApplicationContext()
方法的实现
这个方法的主要作用有2个
1.获取或创建出Spring MVC的Application,然后整合Spring MVC和Spring Core的ApplicaitonContext
2.调度起来整个Spring MVC框架功能(onRefresh方法)
protected WebApplicationContext initWebApplicationContext() {
/*
这行代码是去获取ContextLoaderListener运行过程中存入到ServletContext中的ApplicationContext,存储的过程是Spring Core的工作逻辑。
存入的值存在3种情况:
1.null 代表Spring Core没有被使用(没有使用Spring的监听器)
2.一个可以用的ApplicationContext对象 代表Spring Core逻辑执行正常
3.一个异常对象(在getWebApplicationContext中) 代表Spring Core逻辑执行失败,getWebApplicationContext方法逻辑中抛出了异常
将 Spring Core 的 ApplicationContext 作为 RootContext,以使 Spring Core 和 Spring MVC 功能整合在一起。
*/
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
// webApplicationContext就是通过DispatcherServlet构造函数传入的,这个ApplicationContext是Spring MVC自己的
// 如果使用web.xml方式配置,那么这个判断就不会成立。
if (this.webApplicationContext != null) {
// 有就直接用
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
// 这里显式的将Spring Core 的 ApplicationContext设置为父级
cwac.setParent(rootContext);
}
// 对 Spring MVC 的 ApplicationContext进行一些其他属性设置, 最后执行AbstractApplicationContext#refresh()
// 包括将 Servlet的相关内容(ServletContext、ServletConfig)交给ApplicationContext
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
/*
如果在构造DispatcherServlet时没有传入ApplicationContext实例,那么查看在ServletContext中有没有。
在 web.xml 中以如下形式在DispatcherServlet的配置中设置
<init-param>
<param-name>contextAttribute</param-name>
<param-value>qeqeqe</param-value>
</init-param>
这里的qeqeqe是随意命名的,最终会使用这个值前往ServletContext中找属性。
也就是说你得有个地方写了如下这样的代码而且这个类要比DispatcherServlet先运行,这个findWebApplicationContext()的逻辑才能成功。
getServletContext().setAttribute("qeqeqe", xmlWebApplicationContext);
目前尚为体验到过什么时候该用这种形式
*/
wac = findWebApplicationContext();
}
if (wac == null) {
// 经过上面步骤如果还没有,那么就自己创建一个,将Spring Core的 ApplicationContext作为 RootContext
// 默认创建的 ApplicationContext 是 XmlWebApplicationContext 类型的
// 其中会执行AbstractApplicationContext#refresh()方法
wac = createWebApplicationContext(rootContext);
}
// refreshEventReceived 默认为 false,用于检测onRefresh是否已经被调用的标志。
if (!this.refreshEventReceived) {
// 要么上下文不是具有刷新支持的ConfigurableApplicationContext,要么在构建时注入的上下文
// 已经被刷新 ->手动在这里触发初始onRefresh。
synchronized (this.onRefreshMonitor) {
// 这里是Spring MVC 逻辑的一个重点。
// 该方法被 DispatcherServlet重写,用来加载一些指定的功能逻辑模块,例如: Spring MVC映射路径解析、文件上传、视图解析器等
onRefresh(wac);
}
}
if (this.publishContext) {
// 将Spring MVC的 ApplicationContext 发布为 ServletContext属性。和上面 findWebApplicationContext() 里要找的没有关系。
// attrName -> FrameworkServlet.class.getName() + ".CONTEXT." + getServletName()
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
上面代码中有两次提到了对
AbstractApplicationContext#refresh()
方法的调用,这个方法在Spring框架的逻辑中是十分重要的,Spring 就是通过这个方法,在IOC容器内完成所有的 bean 的创建以及初始化的。
值得注意的是,refresh()
的调用是先于onRefresh(ApplicationContext)
的,这就保证了之后Spring MVC初始化策略时逻辑的正确性。
关于该方法的具体逻辑已经脱离本文的主题,这里不进行深究,在Spring的文章中有对它进行说明
ApplicationContext 是一种 IOC 容器,Spring 提供了两种 IOC 容器,另一种就是 BeanFactory,而 ApplicationContext 是 BeanFactory 的子接口,它添加了更多的支持。
我们这里看一下onRefresh(wac)
的实现,由于子类重写了该方法,我们跟回到DispatcherServlet
中查看
这里是Spring MVC 逻辑的一个重点,负责初始化各种Spring MVC的策略模块。
不同策略的作用在代码注释中进行了简单说明。
// DispatcherServlet
@Override
protected void onRefresh(ApplicationContext context) {
// 这个上下文携带了父级上下文
initStrategies(context);
}
// 方法名中带有 Handler 的都与 Controller 有关,可以把 Controller 视为 Handler
protected void initStrategies(ApplicationContext context) {
// 初始化文件上传处理
initMultipartResolver(context);
// 初始化本地化(国际化)解析器,提供国际化支持
initLocaleResolver(context);
// 初始化主题处理(与css样式有关系)---我没用过
initThemeResolver(context);
// 初始化Handler映射 (用来解析Controller中配置的RequestMapping与方法的映射等相关内容)
initHandlerMappings(context);
// 初始化Handler适配器 (用来处理Controller中方法的参数与返回值等相关内容,配合@ControllerAdvice使用)
initHandlerAdapters(context);
// 初始化Handler异常处理 (用来处理Controller中方法的异常等相关内容,配合@ControllerAdvice使用)
initHandlerExceptionResolvers(context);
// 初始化请求至视图名转换
initRequestToViewNameTranslator(context);
// 初始化视图解析器
initViewResolvers(context);
// 初始化flash映射管理器(和跳转路由有关)
initFlashMapManager(context);
}
2.2.1 九类策略
这些策略每种的作用都不一样,但有些策略并不是很常用,所以描述时会跳过一些我不常用的。
DispatcherServlet.properties
文件中存放了部分策略的默认值,如果不想使用这些默认实现,可以通过注册同类型的Bean来使用自定义的实现。
2.2.1.1 DispatcherServlet.properties 文件全貌
这个图片是高版本Spring的,里面每一项都可能多了一些默认值,可以自己去旧文件里看看。
![](https://img.haomeiwen.com/i16156434/2e3e04f199a8dbf6.png)
2.2.1.2 MultipartResolver
MultipartResolver
是用于处理文件上传的解析器,没有默认实现,当使用时需要手动配置相应的 Bean
private void initMultipartResolver(ApplicationContext context) {
// 首先尝试从IOC容器中确认是否存在对应的Bean,如果没有就设置为null,没有默认值。
try {
// MULTIPART_RESOLVER_BEAN_NAME 的值为 multipartResolver
this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
// ... 日志代码
}
catch (NoSuchBeanDefinitionException ex) {
// Default is no multipart resolver.
this.multipartResolver = null;
// ... 日志代码
}
}
在配置文件中添加如下代码可以启用
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/>
MultipartResolver
有两个实现,分别为CommonsMultipartResolver
和StandardServletMultipartResolver
。
前者使用时需要引入一个名为commons-fileupload
的依赖,而后者不需要。
Spring MVC Multipart Resolver 官方文档
2.2.1.3 LocaleResolver
LocaleResolver
是用于处理本地化/国际化解析器,默认实现为AcceptHeaderLocaleResolver
处理时从 request 中解析出来对应的 Locale 对象,并在请求响应式将该对象交给 response,保存到指定的域中。
private void initLocaleResolver(ApplicationContext context) {
try {
// LOCALE_RESOLVER_BEAN_NAME 的值为 localeResolver
this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class);
// ... 日志打印
}
catch (NoSuchBeanDefinitionException ex) {
// We need to use the default.
// 前往 DispatcherServlet.properties 文件内找到 LocaleResolver 的默认值,并将其实例化,默认为 AcceptHeaderLocaleResolver
this.localeResolver = getDefaultStrategy(context, LocaleResolver.class);
// ... 日志打印
}
}
在配置文件中添加如下代码可以更换LocaleResolver
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver"/>
默认的
AcceptHeaderLocaleResolver
实现对应的是 Http header 中的accept-language
。
LocaleResolver
一共提供了4种实现,其他3种分别是:
CookieLocaleResolver
、SessionLocaleResolver
、FixedLocaleResolver
2.2.1.4 HandlerMappings
关于Spring MVC的主逻辑,更应该较关注initHandlerMappings(context);
,所以看下它的操作.
HandlerMappings
是用于解析路径映射的处理策略。
在初始化时,会尝试从Controller中解析出来所有合法的@RequestMapping
和它们映射到的方法
,并将它们的关联信息保存到相应的处理策略中。
Handler
可视为控制器,也就是以@Controller
或者@RequestMapping
注解的类。
HandlerMathod
是对映射到的方法的包装。
RequestMappingInfo
是对@RequestMapping
内容的包装。
// DispatcherServlet
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
if (this.detectAllHandlerMappings) { // 默认情况下为true
// 从ApplicationContext容器(包括父容器)中查找所有实现HandlerMapping接口的类。
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// 保证HandlerMappings是有序的。
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
// 从 ApplicationContext 中获取 beanName 为 handlerMapping 的且类型是 HandlerMapping 的单例
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ignore) {
}
}
if (this.handlerMappings == null) {
// 主要看这里
/*
如果没有找到,则从DispatcherServlet.properties 文件中拿出默认值,并将其实例化。
默认值有3个,分别是
1.BeanNameUrlHandlerMapping
2.RequestMappingHandlerMapping
3.RouterFunctionMapping
*/
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
/*
怎么说呢,如下代码在早期的Spring中是没有的,这东西使用Spring WebFlux或者Spring Boot时才会用到,
而使用传统的基于web.xml与xml配置文件使用Spring MVC时是用不到的,这就是在本文中是用不到的。
*/
for (HandlerMapping mapping : this.handlerMappings) {
if (mapping.usesPathPatterns()) {
this.parseRequestPath = true;
break;
}
}
}
}
根据上述代码,在默认情况下我们从配置文件中得到3个
HandlerMapping
的实现
1.BeanNameUrlHandlerMapping
2.RequestMappingHandlerMapping
3.RouterFunctionMapping
(这个在早期是没有的,是WebFlux的东西,这里不管)
HandlerMapping类结构关系.png
到这里,initHandlerMappings
方法的逻辑实际上已经执行完成了,下面的分析则是更深入的对HandlerMappings
的工作逻辑进行说明。
2.2.1.4.1 关于 InitializingBean
InitializingBean
是一个接口,内部只声明了一个方法(afterPropertiesSet()
),在Spring技术初始化bean的时候,如果该bean实现了InitializingBean
接口,那么会自动调用其内部方法的实现。
在Spring技术的xml配置文件中,也有能达到类似效果的方法(init-method
),即如下代码:
<bean id="indexController" class="controller.IndexController" init-method="initTest" destroy-method="destroyTest"/>
如果这两种方式以正确的方式同时使用,那么InitializingBean
的逻辑先于init-method
发生,
原因可查阅AbstractAutowireCapableBeanFactory#invokeInitMethods(String, Object, RootBeanDefinition)
。
补充: init-method
是通过反射调用的,而afterPropertiesSet
是方法调用。
2.2.1.4.2 RequestMappingHandlerMapping
在HandlerMapping类结构关系
图中可以观察到BeanNameUrlHandlerMapping
是很普通的继承关系,而RequestMappingHandlerMapping
和InitializingBean
接口有关。
进入RequestMappingHandlerMapping
类中可以看看它里面的afterPropertiesSet
方法。
// RequestMappingHandlerMapping
@Override
public void afterPropertiesSet() {
// 创建一个 RequestMappingInfo.BuilderConfiguration() 实例对象,并为其设置一些属性
this.config = new RequestMappingInfo.BuilderConfiguration(); // 如果想看如何使用的,可以前往该类的createRequestMappingInfo方法
this.config.setUrlPathHelper(getUrlPathHelper()); // 默认UrlPathHelper的实例(AbstractHandlerMapping类中声明)
this.config.setPathMatcher(getPathMatcher()); // 默认AntPathMatcher的实例(AbstractHandlerMapping类中声明)
this.config.setSuffixPatternMatch(this.useSuffixPatternMatch); // 默认true
this.config.setTrailingSlashMatch(this.useTrailingSlashMatch); // 默认true
this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch); // 默认false
this.config.setContentNegotiationManager(getContentNegotiationManager()); // 默认ContentNegotiationManager的实例
// 主要看这里
super.afterPropertiesSet();
}
RequestMappingInfo.BuilderConfiguration
是一个用于 请求映射目的 的配置选项的容器,在创建RequestMappingInfo
实例的时候需要这些配置。
在上述代码中设置的属性的含义如下:
1.UrlPathHelper
:默认为UrlPathHelper
类的实例,用于URL路径匹配,被AbstractUrlHandlerMapping
和RequestContext
用于路径匹配 和/或 URI确定。
2.AntPathMatcher
:默认为AntPathMatcher
类的实例,用于匹配路径中的通配符,是一种ant-style路径模式
的路径匹配器实现。
~|ant-style 路径模式
是Spring参考的Apache Ant项目里面的路径风格,是Spring普遍使用的通配符匹配方式。
3.SuffixPatternMatch
:后缀匹配模式,用于能以 .xxx 结尾的方式进行匹配,默认为true。
~| 举例:假设有一个这样的路径映射:@GetMapping("/a")
当SuffixPatternMatch
为true
,/a.do
是可以访问到该路径的;如果为SuffixPatternMatch
为false
,则访问不到。
~| 这个东西在权限的验证的时候可能产生意想不到的影响,我限制了/a
的权限,攻击者使用/a.do
进行访问。
~| 在 Spring 5.3+ 中默认为false
,源码逻辑中提供了一个除AntPathMatcher
之外的PathPatternParser
匹配策略,目的是替代AntPathMatcher
策略。如果使用高版本Spring Boot,则可以使用spring.mvc.pathmatch.matching-strategy = path_pattern_parser
来指定。
URL Matching with PathPattern in Spring MVC
4.TrailingSlashMatch
:末尾斜杠匹配,用于能以 / 结尾的方式进行匹配,默认为true。
~| 举例:假设有一个这样的路径映射:@GetMapping("/a")
当TrailingSlashMatch
为true
,/a/
是可以访问到该路径的;如果为TrailingSlashMatch
为false
,则访问不到。
~| 这个东西在权限的验证的时候可能产生意想不到的影响,我限制了/a
的权限,攻击者使用/a/
进行访问。
5.RegisteredSuffixPatternMatch
和ContentNegotiationManager
:这2个东西是配合使用的,如果RegisteredSuffixPatternMatch
为true
,则只有在ContentNegotiationManager
内设置的后缀可以映射到请求上,但ContentNegotiationManager
默认的确是所有后缀都可以。
关于`SuffixPatternMatch`和`TrailingSlashMatch`:使用xml形式配置时,可用如下代码修改它的值。
<mvc:annotation-driven>
<mvc:path-matching suffix-pattern="false" trailing-slash="false"/>
</mvc:annotation-driven>
关于这两个参数还有个有趣的例子:
@GetMapping("/{keywords}")
public String index(@PathVariable String keywords) {
return keywords;
}
试试 /a /a.b /a.b.c 都返回什么,可以自己测试一下获取到内容。
关于
RequestMappingInfo
我们在编写Spring MVC时常用的@RequestMapping
是否与其有关?这个东西在下面的陈述中有展示出它是什么。
它确实与@RequestMapping
有关,Spring MVC会将@RequestMapping
的相关信息包装到一个RequestMappingInfo
对象中,以供之后的程序逻辑使用。
在继续分析代码之前,我们提前补充点内容:
1.@Controller、@RequestMapping这类注解
注解的类就叫做Handler
(关于@RequestMapping
在类上能起到与@Controller
等效作用的示例在之后会给出)。
2.@RequestMapping这类注解
注解的方法会被包装成一个HandlerMethod
对象。
3.Spring MVC的所有路径映射信息存储在AbstractHandlerMethodMapping.MappingRegistry
的实例对象内。
// AbstractHandlerMethodMapping
@Override
public void afterPropertiesSet() {
initHandlerMethods();
}
// AbstractHandlerMethodMapping
protected void initHandlerMethods() {
for (String beanName : getCandidateBeanNames()) {
// SCOPED_TARGET_NAME_PREFIX = "scopedTarget.",这个跟@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)注解有关。
// 在这里不需要深入了解该注解,只需要知道常规位于Spring MVC 容器内的 bean都会进入 if代码块就行。
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
// 就是在这里,会解析Spring MVC中的映射路径,并注册到 AbstractHandlerMethodMapping.MappingRegistry类实例对象中。
processCandidateBean(beanName);
}
}
/*
如果开启日志的Debug模式,那么日志输出一下 Spring MVC解析到的路径映射 的数量。
getHandlerMethods() 的返回值是一个Map类型数据,key 由 Http 请求方式和请求地址组成,value 则是 HandlerMethod 对象。
*/
handlerMethodsInitialized(getHandlerMethods());
}
// AbstractHandlerMethodMapping
protected String[] getCandidateBeanNames() {
// detectHandlerMethodsInAncestorContexts 默认为 false, 也就是说默认只获取Spring MVC 容器内的所有bean
return (this.detectHandlerMethodsInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
obtainApplicationContext().getBeanNamesForType(Object.class));
}
实际上,如果这个
initHandlerMethods()
执行完成,代表着整个Spring MVC的映射路径注册完成,也代表第4类策略(initHandlerMappings()
)的执行完成。
再直白点,执行完这个方法后Spring MVC知晓我们项目内有哪些可供访问的路径,他们又映射到了哪些方法上。
但这里还是继续看完解析路径映射的逻辑,我们将这个逻辑分为2部分来说明。
首先得理解什么是桥接方法与合成方法,这样才方便理解下面的代码。
类型擦除的影响 和 桥接方法 文档
Java 中冷门的 synthetic 关键字原理解读
桥接方法是和泛型、泛型擦除相关的内容。
合成方法是和静态内部类相关的内容。
第一部分:
// AbstractHandlerMethodMapping
protected void processCandidateBean(String beanName) {
Class<?> beanType = null;
try {
// 根据传入的beanName获取到bean的类型
beanType = obtainApplicationContext().getType(beanName);
}
catch (Throwable ex) {
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
if (logger.isTraceEnabled()) {
logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
}
}
if (beanType != null && isHandler(beanType)) {
// 看方法名和我们上面的约定叫法, 得知这是要检测类中的HandlerMethod
detectHandlerMethods(beanName);
}
}
// RequestMappingHandlerMapping
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
// AbstractHandlerMethodMapping
protected void detectHandlerMethods(Object handler) {
// beanName一般都是字符串, 则通常都是获取给定的bean的类
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());
if (handlerType != null) {
// 通常是返回给定的类,如果是CGLIB生成的子类则返回原始类
Class<?> userType = ClassUtils.getUserClass(handlerType);
// 这里使用了lambda, 我们先看下lambda内做了什么
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
// 整个lambda函数内,只是尝试调用该方法而已, 后面在第二部分会具体分析这个方法。
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));
}
// 遍历map
methods.forEach((method, mapping) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
// 这个也是核心, 后面在第二部分进行分析
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
// MethodIntrospector
/*
这个方法不止会被 AbstractHandlerMethodMapping#detectHandlerMethods(Object) 调用,也会被其它地方调用。
这个方法的目的:找出类中所有自己写的(非JVM生成、非Object内的)方法,挨个调用传递的metadataLookup函数。
*/
public static <T> Map<Method, T> selectMethods(Class<?> targetType, final MetadataLookup<T> metadataLookup) {
final Map<Method, T> methodMap = new LinkedHashMap<>();
Set<Class<?>> handlerTypes = new LinkedHashSet<>();
Class<?> specificHandlerType = null; // 记录的是当前原始类本身,也就是自己实打实写的代码类,而不是cglib代理生成的。
// -------------------------------- 获取到当前类的类型(开始) --------------------------------------------
// 一个类的类型不仅包括它自身,还有它的父类
if (!Proxy.isProxyClass(targetType)) { // 如果不是代理类
specificHandlerType = ClassUtils.getUserClass(targetType);
handlerTypes.add(specificHandlerType);
}
handlerTypes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetType)); // 获取当前类的父类型
// -------------------------------- 获取到当前类的类型(结束) --------------------------------------------
for (Class<?> currentHandlerType : handlerTypes) {
final Class<?> targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType);
/*
由于篇幅问题,这里不展示 doWithMethods 具体的内部代码,在这里只简单说明下该方法是干什么的。
作用如下:
针对当前类,包含其所有父类和所有上层接口内的 所有 非桥接的并且非合成的并且不是Object内的方法,
调用这里的第二个参数(也就是lambda函数)
说简单点就是 对类中你自己写的方法(包含重写的方法) 进行第二个参数(lambda函数)的运算。
*/
ReflectionUtils.doWithMethods(currentHandlerType, method -> {
/*
在 targetClass类 中找到 指定的method 并返回。
例如,方法可以是IFoo.bar(),目标类可以是DefaultFoo。在这种情况下,方法可能是DefaultFoo.bar()。
如果目标类中没有bar(),那返回结果将保持为传入的IFoo.bar() 不变。
*/
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
// metadataLookup是由上层传递过来的 lambda函数,代码应该退回上层 AbstractHandlerMethodMapping#detectHandlerMethods 查看
T result = metadataLookup.inspect(specificMethod);
if (result != null) {
// 判断是否为桥接方法,不是就直接返回,是的话则找到被桥接的方法返回回来,按照这里上下文的逻辑,传入是什么返回的就是什么。
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
if (bridgedMethod == specificMethod || metadataLookup.inspect(bridgedMethod) == null) {
methodMap.put(specificMethod, result);
}
}
}, ReflectionUtils.USER_DECLARED_METHODS);
/*
第三个参数 MethodFilter USER_DECLARED_METHODS =
(method -> (!method.isBridge() && !method.isSynthetic() && method.getDeclaringClass() != Object.class));
这是一个对方法的过滤条件,只有符合这个条件的方法才会进行第二个参数的运算。
*/
}
// 将解析到的方法和@RequestMapping对应关系返回
return methodMap;
}
到这里,解析路径映射的逻辑的第一部分就展示并描述完了。
第一部分实际上做了这么点事:
1.获取到所有可以称作Handler
的bean。
2.对当前 bean 以及其上层类/接口中的所有自己写的方法,挨个执行getMappingForMethod
调用。
而getMappingForMethod
方法则成为了第一部分和第二部分的分界点。
第二部分:
// RequestMappingHandlerMapping
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
// 根据 Method 解析出 RequestMappingInfo 对象,同时在此过滤掉没有 @RequestMapping 注解的方法(方式就是通过下面对 null的判断)
// 再次重申 RequestMappingInfo 对象是 @RequestMapping 注解信息的封装
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
// 这里是对类上面的 @RequestMapping 注解进行扫描
// 如果类上面也有 @RequestMapping,那么会把类上配置的映射和方法上的映射结合在一起
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
info = typeInfo.combine(info); // 结合的方法
}
// 这个我不知道是什么,没找到怎么配置的,目前看起来也不是很重要,但逻辑挺简单的
String prefix = getPathPrefix(handlerType);
if (prefix != null) {
info = RequestMappingInfo.paths(prefix).build().combine(info);
}
}
return info;
}
// RequestMappingHandlerMapping
@Nullable
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
// 在给予的元素(Class 或者 Method)上找 @RequestMapping, 如果没有就返回null
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
// 不管是 getCustomTypeCondition 还是 getCustomMethodCondition 都是空实现
RequestCondition<?> condition = (element instanceof Class ?
getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
}
protected RequestMappingInfo createRequestMappingInfo(
RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {
// paths() 会返回一个 Build,默认实现为RequestMappingInfo.DefaultBuilder
// 属性通过构造器模式来设置
RequestMappingInfo.Builder builder = RequestMappingInfo
.paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
.methods(requestMapping.method())
.params(requestMapping.params())
.headers(requestMapping.headers())
.consumes(requestMapping.consumes())
.produces(requestMapping.produces())
.mappingName(requestMapping.name());
if (customCondition != null) {
builder.customCondition(customCondition);
}
// 最后调用构造器的 build() 完成 RequestMappingInfo 对象的创建,具体内部逻辑就不再展示。
// 这里的 this.config 就是之前 在 afterPropertiesSet() 里创建的 RequestMappingInfo.BuilderConfiguration()。
return builder.options(this.config).build();
}
RequestMappingHandlerMapping#getMappingForMethod(Method, Class)
的逻辑到这里就完成了。
我们还需要回到AbstractHandlerMethodMapping#detectHandlerMethods(Object)
中,这个方法里面最先面有个forEach的代码块没进行说明。
// 这里的 methods 实际是 MethodIntrospector.selectMethods 的返回值,是一个Map
// key 是 Method,value 是 RequestMappingInfo 对象
methods.forEach((method, mapping) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
// 这个也是核心, 后面在第二部分进行分析
registerHandlerMethod(handler, invocableMethod, mapping);
});
// AbstractHandlerMethodMapping
private final MappingRegistry mappingRegistry = new MappingRegistry();
// MappingRegistry 是 AbstractHandlerMethodMapping 的内部类
// AbstractHandlerMethodMapping
/*
根据上下文来看,
handler 是 当前的 beanName
method 是 要执行的方法
mapping 是 @RequestMapping 内的信息
*/
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
this.mappingRegistry.register(mapping, handler, method);
}
// MappingRegistry
private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
public void register(T mapping, Object handler, Method method) {
this.readWriteLock.writeLock().lock();
try {
// 在这里 beanName、IOC容器、要被调用的方法 一同组成一个 HandlerMethod
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
// 根据 mapping 判断 mappingLookup 中是否存在重复的 HandlerMethod,
// 简而言之就是,确保一个路径映射只能存在一个对应的 HandlerMethod
assertUniqueMethodMapping(handlerMethod, mapping);
this.mappingLookup.put(mapping, handlerMethod);
/*
获取到 @RequestMapping 中配置的路径
例如: @PostMapping(value = {"/test", "/foo", "/bar"})
那么 getDirectUrls(mapping) 得到的结果就是 ["/test", "/foo", "/bar"]
*/
List<String> directUrls = getDirectUrls(mapping);
for (String url : directUrls) {
this.urlLookup.add(url, mapping);
}
// 这里逻辑是:对映射到的方法根据特定的命名策略进行命名,然后将这个名词作为key,HandlerMethod作为value中的一员存入nameLookup
String name = null;
if (getNamingStrategy() != null) {
name = getNamingStrategy().getName(handlerMethod, mapping);
addMappingName(name, handlerMethod);
}
CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
if (corsConfig != null) {
this.corsLookup.put(handlerMethod, corsConfig);
}
/*
将 这些数据放入到 registry中,registry是 AbstractHandlerMethodMapping.MappingRegistry 中持有的一个属性
MappingRegistration 是一个持有RequestMappingInfo、HandlerMethod和directUrl的类,信息很全。
*/
this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
}
finally {
this.readWriteLock.writeLock().unlock();
}
}
protected HandlerMethod createHandlerMethod(Object handler, Method method) {
HandlerMethod handlerMethod;
if (handler instanceof String) {
String beanName = (String) handler;
// 这里可以看出来 HandlerMethod 是什么成分,beanName、容器、要被调用的方法
handlerMethod = new HandlerMethod(beanName,
obtainApplicationContext().getAutowireCapableBeanFactory(), method);
}
else {
handlerMethod = new HandlerMethod(handler, method);
}
return handlerMethod;
}
无论是
readWriteLock
、mappingLookup
、urlLookup
、nameLookup
还是registry
这些属性都是AbstractHandlerMethodMapping.MappingRegistry
类内部的,而AbstractHandlerMethodMapping
持有这个类的对象实例。
其中AbstractHandlerMethodMapping
内部又提供了诸多对于这些属性操作的方法,可以供它的子类(RequestMappingHandlerMapping
、RequestMappingInfoHandlerMapping
)等其他地方使用。
到这里 第4类策略就算是处理完成了,我们最终得到了Spring MVC中关于方法的映射信息。
这个部分的内容有点多,可以慢慢看,多看几遍。
至于BeanNameUrlHandlerMapping
这个东西,它主要是用来处理bean的id作为url的情况,在Spring MVC到本节的启动逻辑中没有发挥作用,所以不做分析。
2.2.1.5 HandlerAdapters
HandlerAdapters
是对Handler进行的功能扩展,以使在请求进入HanlderMethod之前或者之后提供一些额外的服务,虽然不是拦截器,但却发挥拦截器相似的效果。
再次说明 Handler 可以理解为 Controller
HandlerMethod 可以理解为被映射到的方法
private void initHandlerAdapters(ApplicationContext context) {
this.handlerAdapters = null;
if (this.detectAllHandlerAdapters) { // 默认为true
// 从当前ApplicationContext(包括祖先的ApplicationContext)中找到所有HandlerAdapters。
Map<String, HandlerAdapter> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerAdapters = new ArrayList<>(matchingBeans.values());
// 保证HandlerAdapters的顺序。
AnnotationAwareOrderComparator.sort(this.handlerAdapters);
}
}
else {
try {
// HANDLER_ADAPTER_BEAN_NAME 的值为 handlerAdapter
HandlerAdapter ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class);
this.handlerAdapters = Collections.singletonList(ha);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerAdapter later.
}
}
if (this.handlerAdapters == null) {
/*
从 DispatcherServlet.properties 文件中拿出默认值,并将其实例化。
默认值有3个,分别是
1.HttpRequestHandlerAdapter
2.SimpleControllerHandlerAdapter
3.RequestMappingHandlerAdapter
*/
this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class);
// ... 记录日志
}
}
根据上述代码,在默认情况下我们从配置文件中得到3个
HandlerAdapter
的实现
1.HttpRequestHandlerAdapter
,如果Handler
实现自HttpRequestHandler
类,该适配器在请求到来时工作。
2.SimpleControllerHandlerAdapter
,如果Handler
实现自Controller
类,该适配器在请求到来时工作。
3.RequestMappingHandlerAdapter
HandlerAdapter类结构关系图.png
有了HandlerMappings
的经验,我们就可以直接进入RequestMappingHandlerAdapter#afterPropertiesSet()
了
2.2.1.5.1 RequestMappingHandlerAdapter
public void afterPropertiesSet() {
// Do this first, it may add ResponseBody advice beans
initControllerAdviceCache();
if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.initBinderArgumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}
大体进行了这样4个步骤
1.initControllerAdviceCache()
2.处理argumentResolvers
有关内容
3.处理initBinderArgumentResolvers
有关内容
4.处理returnValueHandlers
有关内容
这4个步骤的加载过程是很简单的
2.2.1.5.1.1 initControllerAdviceCache()
private void initControllerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
// 在给定的容器内查找带@ControllerAdvice注解的bean,并且将其包装为ControllerAdviceBean实例对象
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
AnnotationAwareOrderComparator.sort(adviceBeans);
List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
// 查找当前bean内,被@ModelAttribute注解的方法
Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
if (!attrMethods.isEmpty()) {
this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
}
// 查找当前bean内,被@InitBinder注解的方法
Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
if (!binderMethods.isEmpty()) {
this.initBinderAdviceCache.put(adviceBean, binderMethods);
}
// 看看当前bean是否是 RequestBodyAdvice 或者 RequestBodyAdvice 的派生类
if (RequestBodyAdvice.class.isAssignableFrom(beanType)) {
requestResponseBodyAdviceBeans.add(adviceBean);
}
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
requestResponseBodyAdviceBeans.add(adviceBean);
}
}
if (!requestResponseBodyAdviceBeans.isEmpty()) {
this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
}
// ... 日志打印
}
这些代码的是围绕
@ControllerAdvice
注解进行工作的。
在initControllerAdviceCache()
内有其他2个注解和2个接口配合该注解工作,分别是:
1.@ModelAttribute
2.@InitBinder
3.RequestBodyAdvice
:只支持@RequestBody
注解的 Controller 方法
4.ResponseBodyAdvice
:只支持@ResponseBody
注解的 Controller 方法
至于这些东西的用法,已经脱离了本文档的叙述范围,可以看看别人的文章,常用的全局异常处理就是这些内容使用的一个示例。
SpringMVC 中 @ControllerAdvice 注解的三种使用场景!
RequestBodyAdvice 和 RequestBodyAdvice 的使用示例
主要做的工作就是解析到相应内容后存放起来。
2.2.1.5.1.2 argumentResolvers
if (this.argumentResolvers == null) {
// 里面加载了很多很多的解析器,有基于注解的,也有基于类型的
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
在Spring MVC框架加载过程中,这里会通过
getDefaultArgumentResolvers()
来注册特别多的参数解析器。
这些解析器有基于注解的、基于类型、还可以有自定义的,它们核心功能就是用来处理解析参数。
只有当请求到来时,这些解析器才会被调用起来进行各自的工作内容。
2.2.1.5.1.3 initBinderArgumentResolvers
if (this.initBinderArgumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
在Spring MVC框架加载过程中,这里会通过
getDefaultInitBinderArgumentResolvers()
来注册一些和@InitBinder相关的参数解析器。
这些解析器有基于注解的、基于类型、还可以有自定义的,它们核心功能就是用来处理解析参数。
只有当请求到来时,这些解析器才会被调用起来进行各自的工作内容。
2.2.1.5.1.4 returnValueHandlers
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
在Spring MVC框架加载过程中,这里会通过
getDefaultReturnValueHandlers()
来注册一些返回值处理器。
这些处理器有基于注解的、基于类型、还可以有自定义的,它们核心功能就是用来处理解析参数。
只有当请求到来时,这些处理器才会被调用起来进行各自的工作内容。
2.2.1.6 HandlerExceptionResolvers
HandlerExceptionResolvers
是用于处理 请求执行时出现异常之后应该如何做 的问题,会实例化多个默认实现。
ExceptionHandlerExceptionResolver
是其默认实现之一,也是排在第一位执行的,内部包含了对@ControllerAdvice
和@ExceptionHandler
注解的支持,使得我们可以通过全局异常的形式来捕获并处理请求执行时遇到的问题。
private void initHandlerExceptionResolvers(ApplicationContext context) {
this.handlerExceptionResolvers = null;
if (this.detectAllHandlerExceptionResolvers) {
// 从当前ApplicationContext(包括祖先的ApplicationContext)中找到所有 HandlerExceptionResolvers。
Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
// 保证HandlerExceptionResolvers的顺序。
AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
}
}
else {
try {
// HANDLER_ADAPTER_BEAN_NAME 的值为 handlerExceptionResolver
HandlerExceptionResolver her =
context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class);
this.handlerExceptionResolvers = Collections.singletonList(her);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, no HandlerExceptionResolver is fine too.
}
}
if (this.handlerExceptionResolvers == null) {
/*
从 DispatcherServlet.properties 文件中拿出默认值,并将其实例化。
默认值有3个,分别是
1.ExceptionHandlerExceptionResolver
2.ResponseStatusExceptionResolver
3.DefaultHandlerExceptionResolver
*/
this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class);
// ... 日志打印
}
}
根据上述代码,在默认情况下我们从配置文件中得到3个
HandlerExceptionResolver
的实现
1.ExceptionHandlerExceptionResolver
2.ResponseStatusExceptionResolver
3.DefaultHandlerExceptionResolver
HandlerExceptionResolver类文件结构.png
2.2.1.6.1 ExceptionHandlerExceptionResolver
public void afterPropertiesSet() {
// Do this first, it may add ResponseBodyAdvice beans
initExceptionHandlerAdviceCache();
if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}
大体进行了这样3个步骤
1.initExceptionHandlerAdviceCache()
2.处理argumentResolvers
有关内容
4.处理returnValueHandlers
有关内容
这3个步骤的加载过程是很简单的,和HandlerAdapters
中的做法差不多一样
2.2.1.6.1.1 initExceptionHandlerAdviceCache
private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
// 在给定的容器内查找带@ControllerAdvice注解的bean,并且将其包装为ControllerAdviceBean实例对象
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
AnnotationAwareOrderComparator.sort(adviceBeans);
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
}
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
this.responseBodyAdvice.add(adviceBean);
}
}
// ... 日志打印
}
public ExceptionHandlerMethodResolver(Class<?> handlerType) {
// 查找当前bean内,被@ExceptionHandler注解的方法
for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
// 这里进行的这些行为,目的是获取到定义的异常类型 并且和 异常的处理方法 以key-value形式保存起来。
// 如果已经看过HandlerMappings的处理过程,那这个就更容易看懂了
for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
addExceptionMapping(exceptionType, method);
}
}
}
这些代码的是围绕
@ControllerAdvice
注解进行工作的。
在initExceptionHandlerAdviceCache()
内有其他1个注解和1个接口配合该注解工作,分别是:
1.@ExceptionHandler
2.ResponseBodyAdvice
:只支持@ResponseBody
注解的 Controller 方法至于这些东西的用法,已经脱离了本文档的叙述范围,可以看看别人的文章,常用的全局异常处理就是这些内容使用的一个示例。
Spring异常处理@ExceptionHandler
2.2.1.6.1.2 argumentResolvers 和 returnValueHandlers
与之前的
RequestMappingHandlerAdapter
的过程差不多,自己去ExceptionHandlerExceptionResolver
类中查看。
2.2.1.7 RequestToViewNameTranslator
RequestToViewNameTranslator
是用于处理Controller中方法返回值没有指定具体ViewName的问题,默认实现是DefaultRequestToViewNameTranslator
。
注意: 现在陈述的是Spring MVC的框架启动过程,它内部的实现逻辑不做说明。
当Controller中映射方法的返回值内没有指定具体的ViewName时,
RequestToViewNameTranslator
将负责 把请求地址翻译为ViewName,以使之后的逻辑进行工作。
如果你的方法返回的是Json、Xml等类型的数据,那跟这个就没关系了。
这里列举个示例来解释 RequestToViewNameTranslator 到底是干什么的
@GetMapping("/page/goods")
public String goodsHtml() {
return null;
}
// @GetMapping("/page/goods") ModelAndView goodsHtml() { return new ModelAndView() }
// @GetMapping("/page/goods") void goodsHtml() {}
请求:"Ge localhost:8080/page/goods"
当遇到这种情况时,RequestToViewNameTranslator 才会开始工作。
它会获取到路径中的 "/page/goods",然后将经过一些规则处理后的值当作默认的ViewName,在默认情况下 "/page/goods" 会被处理成 "page/goods"。
也就是说虽然你方法内返回值为 "null",但实际上 Spring MVC 却认为你返回了 "page/goods"。
可以使用下面的方式进行配置,这个配置内虽然也有对前缀和后缀的配置,但它和之后ViewResolvers中展示的那种不是一个概念(但效果一样。。)。
<bean class="org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator">
<property name="prefix" value="classpath:/templates"/>
<property name="suffix" value=".html"/>
<property name="stripLeadingSlash" value="false"/>
<property name="stripTrailingSlash" value="true"/>
</bean>
2.2.1.8 ViewResolvers
ViewResolvers
是用于处理使用模板引擎技术时模板上数据的填充问题,默认实现是InternalResourceViewResolver
。
注意: 现在陈述的是Spring MVC的框架启动过程,它内部的实现逻辑不做说明。
在一个请求处理过程中,
ViewResolvers
会将 Spring MVC 的 Controller 中指定返回的viewName
以及解析到的Locale
处理成一个View
对象,View
对象是用来渲染页面的,不同的解析器会解析成不同类型的View
对象。
在默认的InternalResourceViewResolver
实现中,默认生成的是JstlView
类型的View
对象,是 Jsp 技术的对应类型。
可以使用下面的方式进行配置,这个配置在以前还是挺常见的
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/> <!-- 默认就是,可以不写 -->
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
当使用Thymeleaf技术时,则会更换解析器,这里没有展示Thymeleaf的完整配置,如有需要自行百度。
<!-- ThymeleafViewResolver也是ViewResolver的实现,不过是由Thymeleaf技术提供的实现,会生成ThymeleafView类型的View对象 -->
<bean class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
<property name="templateEngine" ref="templateEngine"/>
<property name="characterEncoding" value="UTF-8"/>
</bean>
Spring 5.0+ 已经放弃对Velocity的支持了,至于FreeMarker的配置和上面的差不了多少
2.2.1.9 FlashMapManager
FlashMapManager
是用于处理重定向时的参数的传递问题,默认实现是SessionFlashMapManager
。
注意: 现在陈述的是Spring MVC的框架启动过程,它内部的实现逻辑不做说明。
常规情况下,重定向时想要把参数也传递过去,则需要在目标 path 上进行参数拼接,使用这种方式不是很理想。
FlashMapManager
策略提供了一种新的重定向参数的传递方式,这种方式,可以不用再在路径上拼接参数了,而是依赖Spring框架完成传递,下面展示简单使用示例。
// 我常用这种写法,对于其他人博客里面的用法我没用过。
@PostMapping("/a")
public void a(HttpServletRequest request, HttpServletResponse response) throws IOException {
FlashMap flashMap = RequestContextUtils.getOutputFlashMap(request);
flashMap.put("a", "1");
flashMap.put("b", "2");
String redirect = "/b";
// 将 FlashMap 交由 FlashMapManager 管理
RequestContextUtils.saveOutputFlashMap(redirect, request, response);
response.sendRedirect(redirect);
}
@GetMapping("/b")
public String b(HttpServletRequest request) {
Map<String, ?> inputFlashMap = RequestContextUtils.getInputFlashMap(request);
Object returnValue = null;
if (inputFlashMap != null) {
returnValue = inputFlashMap.get("a");
}
return (String) returnValue;
}
// 在配置文件中添加如下代码可以对细节内容进行调整
<!-- 使用 SessionFlashMapManager 时,保存的属性会以 HttpSession 作为底层进行存取 -->
<bean id="flashMapManager" class="org.springframework.web.servlet.support.SessionFlashMapManager">
<property name="flashMapTimeout" value="180" />
<property name="urlPathHelper" ref="xxx"/> <!-- 这个属性的值必须是 UrlPathHelper 对象实例,不建议修改 -->
</bean>
注意:FlashMap
是有时效性的,默认为180秒,在AbstractFlashMapManager
类中有定义
2.2.1.10 ThemeResolver
用于解析主题,跟样式有关。没用过,不了解。
3 4种创建Handler的方式
3.1 @Controller
@Controller // @RestController
public class IndexController {
}
3.2 @RequestMapping
@Component
@RequestMapping
public class IndexController {
}
3.3 Controller 接口
@Component
public class IndexController implements Controller {
}
3.4 HttpRequestHandler 接口
@Component
public class IndexController implements HttpRequestHandler {
}
结束
到这里,Spring MVC 的框架配置与启动过程大体已经陈述,其中的一些细节需要时自己查看就行。
下一篇Spring MVC 源码阅读-请求的处理流程
网友评论