目录
- SpringMVC官方文档
- SpringMVC的父子容器
- 父子关系的定义
- 自定义快速启动器
- 启动过程
- 容器创建的过程
- 容器刷新启动
- 父子容器示例图
- 网络请求链路分析
- DispatcherServlet请求链路
- DispatcherServlet详解(MVC核心功能类)
- DispatcherServlet九大组件
- 九大组件的初始化
- 默认策略
- 初始化时机
- HandlerMapping详解
- RequestMappingHandlerMapping
- HandlerAdapter详解
- 整体的流程图
- RequestMappingHandlerAdapter
- 初始化流程
- 执行流程(总体来讲)
- 参数解析原理
- 返回值处理器
- 处理结果
- 视图渲染解析
- SpringMVC异常处理
- ResponseStatus
- 异常注解版处理
- @EnableWebMvc原理
- EnableWebMvc图示
SpringMVC官方文档
先放最标准的官方文档在这,作为这篇技术文章的定海神针SpringMVC官方文档传送门
SpringMVC的父子容器
SpringMVC有一个父子容器的关系.Spring的IOC容器是为父(根)容器,SpringMVC的web-IOC容器是为子容器,子容器找组件的时候先找自己,在找父容器(递归调用),最后进行合并
注意这里父子容器不是一个强制标准,完全可以不指定父容器,这时候MVC的容器扫描所有的Bean组件信息
父子关系的定义
通过servlet规范中的init初始化方法调用过来(tomcat进行触发),在FrameworkServlet
的initServletBean
方法中的initWebApplicationContext
方法定义了两者之间的父子关系,此时父容器已经经历过了初始化,完全准备就绪,示例代码如下:
protected WebApplicationContext initWebApplicationContext() {//先尝试获取一下之前的父容器WebApplicationContext rootContext =WebApplicationContextUtils.getWebApplicationContext(getServletContext());WebApplicationContext wac = null;if (this.webApplicationContext != null) {//当前的web-ioc容器wac = this.webApplicationContext;if (wac instanceof ConfigurableWebApplicationContext) {ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;if (!cwac.isActive()) {if (cwac.getParent() == null) {//父子容器的体现cwac.setParent(rootContext);}//配置并刷新容器configureAndRefreshWebApplicationContext(cwac);}}}return wac;}
自定义快速启动器
AbstractAnnotationConfigDispatcherServletInitializer
属于是能够快速整合注解版的SpringMVC和Spring的一个抽象类,能够指定两个IOC容器的配置类和servlet的路径映射(通过的是模板方法设计模式,给子类预留了方法去填充,还有很多其他可以重写的方法但只有这三个是必须重写去实现的)
/*** 最快速的整合注解版SpringMVC和Spring的*/
public class QuickAppStarter extends AbstractAnnotationConfigDispatcherServletInitializer {//根容器的配置(spring的配置类==spring的配置文件)@Overrideprotected Class<?>[] getRootConfigClasses() {return new Class[]{SpringConfig.class};}//web容器的配置(springMVC的配置类==springMVC的配置文件)@Overrideprotected Class<?>[] getServletConfigClasses() {return new Class[]{SpringMVCConfig.class};}//servlet的映射:DispatcherServlet的映射路径@Overrideprotected String[] getServletMappings() {return new String[]{"/app/*"};}
}
启动过程
容器创建的过程
WebApplicationInitializer
先引导我们按照事先指定的配置类把父子容器创建出来(还是SPI那一套去调用到onStartup方法)
- tomcat启动,扫描所有的WebApplicationInitializer
- 找到我们自定义实现的快速启动类,他继承了抽象类
AbstractAnnotationConfigDispatcherServletInitializer
- 调用onStartup方法,他是继承自
AbstractDispatcherServletInitializer
- 先调用父类的onStartup方法,其中逻辑如下
registerContextLoaderListener(servletContext)
注册servlet规范的监听器- 根据返回的spring配置类创建了一个容器,类型是
AnnotationConfigWebApplicationContext
(此时还未初始化容器) - 把创建好的容器添加到刚才的监听器当中(监听器是tomcat自己加载完当前的web应用后会调用监听器的
contextInitialized
方法,此时会触发容器的刷新初始化)
- 在调用
registerDispatcherServlet
方法注册DispatcherServlet
- 根据返回的SpringMVC的配置类在创建一个容器,类型为
AnnotationConfigWebApplicationContext
,此容器也未初始化 - 直接new新建了一个DispatcherServlet,并且保存了上面刚新建的容器
容器刷新启动
- 在tomcat对web应用启动完成的时候,会触发监听器的钩子函数
contextInitialized
启动根容器(spring的ioc容器),在这个方法的底层会调用容器刷新的refresh方法 - 在tomcat启动以后,tomcat调用DispatcherServlet的初始化(init)方法,此时会初始化子容器(在此过程中行程了父子容器)
父子容器示例图
父子容器创建初始化完毕之后,整个应用就已经准备完毕了
网络请求链路分析
DispatcherServlet请求链路
- javax的servlet处理
Servlet
接口定义了service
方法GenericServlet
抽象类实现了servlet接口,不过没有重写service方法HttpServlet
继承了GenericServlet并重写了service方法,根据请求方法的类型,去分别调用不同的方法(类似于doGet,doPost等等)
- spring家的servlet处理
HttpServletBean
抽象类继承自HttpServlet没有处理这些被拆分的方法FrameworkServlet
抽象类继承自HttpServletBean重写了所有的拆分方法,这些方法都统一调用了processRequest(request, response)
方法,他是SpringMVC统一处理请求的入口方法DispatcherServlet
类继承了FrameworkServlet,上面的processRequest方法会调用到他的doService
方法- doService方法中会进行一些request域属性的准备,最后就会调用到大名鼎鼎的
doDispatch(request, response)
方法,相信大家或多或少应该都听过,这里就会进行请求的整个派发逻辑
DispatcherServlet详解(MVC核心功能类)
DispatcherServlet九大组件
DispatcherServlet的九大组件(官方文档只介绍了八个,只需要关注这八个就可以)全部都是接口,我们完全可以自定义其实现方式,不过spring默认都准备好了这些组件的实现,一般不需要我们去重写
MultipartResolver
:文件上传解析器LocaleResolver
:国际化解析器(区域信息)ThemeResolver
:主题解析器HandlerMapping
:Handler(处理器,能处理请求的组件(controller))的映射 保存的是所有的请求都由谁来处理的映射关系HandlerAdapter
:Handler(处理器,能处理请求的组件(controller))的适配器 超级反射工具HandlerExceptionResolver
:Handler的异常解析器RequestToViewNameTranslator
:把请求转成视图名(要跳转的页面地址)的翻译器,这个官方文档没有介绍,重要程度不高FlashMapManager
:闪存管理器ViewResolver
:视图解析器(去哪些页面,怎么过去)
九大组件的初始化
九大组件通过下面的方法进行初始化操作,其实都是先根据名称和类型先直接从IOC容器中获取,没有的话就根据默认策略去创建,这样就留给我们一个口子可以自己实现这些接口的具体实现放到容器中,从而使用自己的逻辑(一般不会这么做)
protected void initStrategies(ApplicationContext context) {//容器中有就拿来用,没有就是nullinitMultipartResolver(context);//下面的都是从容器中拿,拿不到根据策略自行创建initLocaleResolver(context);initThemeResolver(context);initHandlerMappings(context);initHandlerAdapters(context);initHandlerExceptionResolvers(context);initRequestToViewNameTranslator(context);initViewResolvers(context);initFlashMapManager(context);}
默认策略
默认的策略指定了去DispatcherServlet的类路径下面找到DispatcherServlet.properties
文件,他里面定义了默认的实现类.
九大组件除了文件上传组件,都会有默认值,文件上传功能,我们需要自己导入相关包并进行配置
# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolverorg.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolverorg.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\org.springframework.web.servlet.function.support.RouterFunctionMappingorg.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\org.springframework.web.servlet.function.support.HandlerFunctionAdapterorg.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolverorg.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslatororg.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolverorg.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager
初始化时机
容器刷新12大步的最后一步发送上下文环境刷新完成的事件,触发九大组件的初始化方法publishEvent(new ContextRefreshedEvent(this));
,最后调用到DispatcherServlet的如下方法
protected void onRefresh(ApplicationContext context) {//初始化九大组件initStrategies(context);}
是利用了Spring的事件机制去触发初始化流程的
HandlerMapping详解
它是能够保证,每一个需要处理的网络请求url映射能够注册进来,至于后面怎么处理它不管,它的默认策略一共初始化了三个具体实现类,分别为:
BeanNameUrlHandlerMapping
:bean的名字作为url路径,进行映射,基本没有这种用法(扫描所有名字以/开始的组件,注册url到映射中)RequestMappingHandlerMapping
:我们熟知的@RequestMapping
注解作为url路径进行映射,也是我们经常用到的(扫描所有的@Controller有方法标注了@RequestMapping注解的,注册到url映射中)RouterFunctionMapping
:支持函数式处理以及webflux相关功能
RequestMappingHandlerMapping
初始化的流程(书接上文):
- DispatcherServlet创建对象后,Tomcat会调用回调的钩子触发initServletBean函数
- 最终容器启动完成,Spring发送事件,回调到DispatcherServlet的onRefresh函数
- onRefresh函数会初始化九大组件
- RequestMappingHandlerMapping开始初始化
- 创建所有配置中指定的handlerMapping对象
- 调用了IOC容器的creatBean方法去开始创建RequestMappingHandlerMapping对象
- RequestMappingHandlerMapping创建完成赋值等等操作后,就开始初始化流程
- RequestMappingHandlerMapping实现了
InitializingBean
- 调用了RequestMappingHandlerMapping的初始化方法
afterPropertiesSet
- 拿到子(web)容器中的所有组件,挨个遍历处理,判断是否有Controller或者RequestMapping注解
- 把分析完成后的RequestMapping信息放入到registry当中
我们一直在使用的就是它,它保存了所有的映射处理信息,在MappingRegistry
这个内部类中.
//放到了这个registry之中private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
他的初始化利用了
InitializingBean
的生命周期.这里注意一个点位,在Spring中,如果是所有组件可能都会用的功能(比如自动装配,AOP等)都是用后置处理器BeanPostProcessor
来做到的.如果是单组件的增强,跟其他人没什么关系,最好是利用生命周期InitializingBean
来做
HandlerAdapter详解
属于是终极的适配器,通过它去适配执行目标方法并处理返回值,他一共有四个默认的适配器
HttpRequestHandlerAdapter
判断是否是HttpRequestHandler接口的SimpleControllerHandlerAdapter
判断是否是Controller接口的RequestMappingHandlerAdapter
判断是否是一个方法,直接返回TrueHandlerFunctionAdapter
流式编程,webflux的处理适配器
根据supports方法判断是否能够应用这个适配器,如果可以的话就去执行Adapter的handle方法
整体的流程图
RequestMappingHandlerAdapter
它是我们最常用的适配器,通过反射去调用我们定义的controller中的方法
初始化流程
同上面的RequestMappingHandlerMapping
初始化流程是一样的,在执行Bean的生命周期接口InitializingBean,此时会填充参数解析器和返回值处理器进来(放到组合对象中保存起来),而且会初始化了ControllerAdvice相关功能
执行流程(总体来讲)
- 调用adapter接口定义的handle方法,开始执行目标方法
- 判断是否采用会话锁(默认值是false不采用),它直接锁住了session会话,限制一个会话一次只处理一个请求线程
- 此时调用
invokeHandlerMethod
执行目标方法 - 做一些基本数据的准备
- 封装handlerMethod变为
ServletInvocableHandlerMethod
提供handlerMethod里面信息的快速获取,并且准备argumentResolvers参数解析器(未来用来反射解析目标方法中每一个参数的值)和returnValueHandlers返回值处理器(未来用来处理目标方法执行后的返回值,无论目标方法返回什么,想办法变成适配器能用的ModelAndView)放到封装好的HandlerMethod中 - 创建ModelAndViewContainer模型和视图的容器,把处理过程中产生的模型与视图相关的数据存放在这里,他是为了能够在整个请求处理线程期间共享数据ModelAndView
- 调用
invocableMethod.invokeAndHandle(webRequest, mavContainer)
执行目标方法,执行期间的数据存到mavContainer中 - 调用
getModelAndView(mavContainer)
方法提取出ModelAndView数据准备返回,后面就可以直接解析处理ModelAndView去view指定的页面展示出model中的数据
参数解析原理
- 通过
getMethodArgumentValues
获取到所有参数的值 - for循环
MethodParameter
参数列表来解析确定每个参数的值 - 通过解析器的
supportsParameter
方法循环遍历所有的解析器,找到能够处理该参数的参数解析器
- 注意这里边有一个参数解析器的缓存,符合条件的话会放入缓存中,下次这个接口的请求过来就不需要再去挨个遍历解析器判断了
- 基本上每一种参数解析器对应一个spring家的参数注解,基本上是通过注解还有一些固定类去判断的(一共有27个参数解析器),可以参考参数解析
- 调用
resolveArgument
方法进行真正的参数解析,此时就根据不同的参数解析器策略,执行不同的逻辑,这里不挨个一一介绍了,有兴趣的可以自己去看看
返回值处理器
SpringMVC的controller方法能够返回的返回值官网文档传送门
- 调用
handleReturnValue
进行返回值处理 - 同之前逻辑一样,调用
selectHandler
找到合适的返回值处理器(注意这里没有缓存),一共15个处理器挨个遍历,调用返回值处理器接口的supportsReturnType
方法(基本上通过函数的返回类型去做的判断) - 然后调用控制器的
handleReturnValue
方法处理解析返回值
其中像咱们最常用的
@ResponseBody
是用MessageConvert把对象转换成为了流直接write出去的,里面的过程比较复杂,不详细展开了,可以自己看下RequestResponseBodyMethodProcessor
这个类
处理结果
调用processDispatchResult
方法,上面流程如果捕捉到异常先不抛出,捕获到传递给processDispatchResult方法,此方法先判断异常是否为空,如果有异常先处理异常,用所有的异常解析器尝试去解析,处理完之后再度抛出异常,外层捕获后在调用triggerAfterCompletion
方法去判断执行链中是否有异常拦截器,如果有的话执行拦截器,最后在将异常抛出去.如果没有异常接下来会用视图解析器去解析得到视图
视图渲染解析
- 如果是
@ResponseBody
标注的方法,实际上在之前的步骤就已经直接把返回对象转为流写出去了,视图解析对他没意义 - 否则调用
render(mv, request, response)
方法开启视图的解析 - 先确定国际化的信息,
AcceptHeaderLocaleResolver
会根据请求头中的Accept-Language字段决定浏览器能接受哪种中文/英文页面 - 获取视图的名称,再根据名称通过
resolveViewName(viewName, mv.getModelInternal(), locale, request)
方法获取到真正的视图View
对象 - 跟之前的逻辑类似,动态策略,用多个视图解析器去挨个遍历解析,通过模板方法控制逻辑
- 先看缓存中有没有,没有的话真正去创建视图
- 进入到
UrlBasedViewResolver
解析器中,判断视图名称是否以**redirect:**开始,是的话就准备重定向视图RedirectView
对象 - 再判断视图名称是否以**forward:**开始,是的话就准备转发视图
InternalResourceView
对象 - 否则的话就直接调用父类的模板方法,去创建视图对象(创建出来的话会把视图放入到缓存中的,下次直接就从缓存去获取)
- 创建完视图之后,再去调用
AbstractView
的render方法,再此继续通过模板方法还有策略模式去调用到具体视图类型的render方法
SpringMVC异常处理
- 执行目标方法,抛出的所有异常暂存起来,继续调用最后的结果处理
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException)
方法 - 判断异常不为空,用所有的异常解析器去解析该异常,调用
processHandlerException(request, response, handler, exception)
方法 - 调用到模板方法的实现最终去解析异常
doResolveException
AbstractHandlerMethodExceptionResolver
:所有@ExceptionHandler
注解方式的异常处理交给他来做ResponseStatusExceptionResolver
找异常类上有没有@ResponseStatus
注解DefaultHandlerExceptionResolver
:看异常是否属于我指定去处理的那些异常,如果是的话直接返回错误页(错误代码和消息)
- 返回一个空的ModelAndView,不会再进行其他的处理,执行后续的拦截器
- 如果所有的异常解析器都发现处理不了异常,就再次抛出异常(下面的渲染方法就不再去执行)
- 上面捕获到异常,再次执行拦截器,然后再次抛出异常,直接就抛出给Tomcat等容器了(没人处理的异常就一直往外抛出,一直到最顶层)
ResponseStatus
用这个注解可以返回自己定义的状态码还有错误信息.示例代码如下
@ResponseStatus(value = HttpStatus.CONFLICT,reason = "非法用户")
public class InvalidUserException extends RuntimeException{private static final long serialVersionUID = -778778787878L;}
异常注解版处理
ExceptionHandlerExceptionResolver
借助InitializingBean
生命周期的初始化方法afterPropertiesSet
将所有的@ControllerAdvice
注解的Bean扫描到缓存中,并且解析Bean中所有标注了@ExceptionHandler
注解的方法,同样放入到了Map缓存中(为后续的处理做好了准备)- 后面进入到异常解析流程时,直接从缓存中获取到相应的异常处理方法(上一步同样初始化处理参数和返回值解析器),通过参数解析器拿到所有的入参再加上异常,执行反射调用到自定义的异常处理方法
@EnableWebMvc原理
- 通过Import注解给容器中导入
DelegatingWebMvcConfiguration
组件 - 拿到容器中所有的
WebMvcConfigurer
(通过这里可以对组件进行很多扩展,组件的核心方法都给他留了模板方法) - 最重要的一点是它继承自
WebMvcConfigurationSupport
类,这个类通过@Bean的方式把RequestMappingHandlerMapping
等等的DispatcherServlet所需要用到的九大组件全部注册到容器中
EnableWebMvc会给容器中导入了九大组件(我们还可以用
WebMvcConfigurer
去定制,给留下了模板方法入口),DispatcherServlet初始化组件直接就可以从容器中获取到了,而不是用配置文件(配置文件里面写死了一些默认的类路径)默认