• Spring系列-SpringMvc父子容器启动原理解析


    1、Spring整合SpringMVC

    特性:

    说到Spring整合SpringMVC唯一的体现就是父子容器:

    • 通常我们会设置父容器(Spring)管理Service、Dao层的Bean, 子容器(SpringMVC)管理Controller的Bean .
    • 子容器可以访问父容器的Bean, 父容器无法访问子容器的Bean。

    实现:

    相信大家在SSM框架整合的时候都曾在web.xml配置过这段:

    1. <listener>
    2. <listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
    3. listener>
    4. <context-param>
    5. <param-name>contextConfigLocationparam-name>
    6. <param-value>classpath:spring-core.xmlparam-value>
    7. context-param>
    8. <servlet>
    9. <servlet-name>dispatcherServletservlet-name>
    10. <servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
    11. <init-param>
    12. <param-name>contextConfigLocationparam-name>
    13. <param-value>classpath:spring-mvc.xmlparam-value>
    14. init-param>
    15. <load-on-startup>1load-on-startup>
    16. servlet>
    17. <servlet-mapping>
    18. <servlet-name>dispatcherServletservlet-name>
    19. <url-pattern>/url-pattern>

    但是它的作用是什么知道吗?

    0

    有人可能只知道DispatcherServlet叫前端控制器,是SpringMVC处理前端请求的一个核心调度器

    那它为什么能处理请求?处理之前做了什么准备工作呢?又是怎么和Spring结合起来的呢?

    为什么有了DispatcherServlet还要个ContextLoaderListener, 配一个不行吗?干嘛要配俩啊?

    看完本文你就会有答案!

    0

    还有人可能会觉得, 我现在都用SpringBoot开发, 哪还要配这玩意.......

    0

    这就是典型的SpringBoot使用后遗症,SpringBoot降低了使用难度,但是从某种程度来说,也让初级的程序员变得更加小白,把实现原理都隐藏起来了而我们只管用,一旦涉及扩展就束手无策。

    那当然我们今天不讲SpringBoot,我们今天用贴近SpringBoot的方式来讲SpringMVC。也就是零配置(零xml)的放式来说明SpringMVC的原理!!

    此方式作为我们本文重点介绍,也是很多人缺失的一种方式, 其实早在Spring3+就已经提供, 只不过我们直到SpringBoot才使用该方式进行自动配置, 这也是很多人从xml调到SpringBoot不适应的原因, 因为你缺失了这个版本。 所以我们以这种方式作为源码切入点既可以理解到XML的方式又能兼顾到SpringBoot的方式 。

    2、零配置SpringMVC实现方式:

    那没有配置就需要省略掉web.xml 怎么省略呢?

    在Servlet3.0提供的规范文档中可以找到2种方式:

    • 注解的方式
    1. @WebServlet
    2. @WebFilter
    3. @WebListener

    但是这种方式不利于扩展, 并且如果编写在jar包中tomcat是无法感知到的。

    • SPI的方式

    在Serlvet3-1的规范手册中:就提供了一种更加易于扩展可用于共享库可插拔的一种方式,参见8.2.4:

    也就是让你在应用META-INF/services 路径下 放一个 javax.servlet.ServletContainerInitailizer ——即SPI规范

    SPI 我们叫他服务接口扩展,(Service Provider Interface) 直译服务提供商接口, 不要被这个名字唬到了, 其实很好理解的一个东西:

    其实就是根据Servlet厂商(服务提供商)提供要求的一个接口, 在固定的目录(META-INF/services)放上以接口全类名 为命名的文件, 文件中放入接口的实现的全类名,该类由我们自己实现,按照这种约定的方式(即SPI规范),服务提供商会调用文件中实现类的方法, 从而完成扩展。

    ok 那我们知道了SPI是什么,我们是不是可以在Web应用中,在Servlet的SPI放入对应的接口文件:

    0

    放入实现类:

    0

    通过ServletContext就可以动态注册三大组件:以Servlet注册为例:

    1. public class TulingSpringServletContainerInitializer extends SpringServletContainerInitializer {
    2. @Override
    3. public void onStartup(Set> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
    4. // 通过servletContext动态添加Servlet
    5. servletContext.addServlet("spiServlet", new HttpServlet() {
    6. @Override
    7. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    8. resp.getWriter().write("spiServlet--doGet");
    9. }
    10. }).addMapping("/spiServlet.do");
    11. }

     当然在SpringMVC中, 这个接口文件和实现类都把我们实现好了,甚至ContextLoaderListener和DispatcherServlet都帮我们注册好了,我们只要让他生效,来,看看他是怎么做的:

    3、实现基于SPI规范的SpringMVC

    TulingStarterInitializer

    • 此类继承AbstractAnnotationConfigDispatcherServletInitializer 这是个啥? 待会我们讲原理来介绍
    • getRootConfigClasses 提供父容器的配置类
    • getServletConfigClasses 提供子容器的配置类
    • getServletMappings 设置DispatcherServlet的映射
    1. public class TulingStarterInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    2. /**
    3. * 方法实现说明:IOC 父容器的启动类
    4. * @author:xsls
    5. * @date:2019/7/31 22:12
    6. */
    7. @Override
    8. protected Class[] getRootConfigClasses() {
    9. return new Class[]{RootConfig.class};
    10. }
    11. /**
    12. * 方法实现说明 IOC子容器配置 web容器配置
    13. * @author:xsls
    14. * @date:2019/7/31 22:12
    15. */
    16. @Override
    17. protected Class[] getServletConfigClasses() {
    18. return new Class[]{WebAppConfig.class};
    19. }
    20. /**
    21. * 方法实现说明
    22. * @author:xsls
    23. * @return: 我们前端控制器DispatcherServlet的拦截路径
    24. * @exception:
    25. * @date:2019/7/31 22:16
    26. */
    27. @Override
    28. protected String[] getServletMappings() {
    29. return new String[]{"/"};

    RootConfig

    • 父容器的配置类 =以前的spring.xml
    • 扫描的包排除掉@Controller
    1. @Configuration
    2. @ComponentScan(basePackages = "com.tuling",excludeFilters = {
    3. @ComponentScan.Filter(type = FilterType.ANNOTATION,value={RestController.class,Controller.class}),
    4. @ComponentScan.Filter(type = ASSIGNABLE_TYPE,value =WebAppConfig.class ),
    5. })
    6. public class RootConfig {
    7. }

    WebAppConfig

    • 子容器的配置类 =以前的spring-mvc.xml
    • 扫描的包:包含掉@Controller
    1. @Configuration
    2. @ComponentScan(basePackages = {"com.tuling"},includeFilters = {
    3. @ComponentScan.Filter(type = FilterType.ANNOTATION,value = {RestController.class, Controller.class})
    4. },useDefaultFilters =false)
    5. @EnableWebMvc // ≈
    6. public class WebAppConfig implements WebMvcConfigurer{
    7. /**
    8. * 配置拦截器
    9. * @return
    10. */
    11. @Bean
    12. public TulingInterceptor tulingInterceptor() {
    13. return new TulingInterceptor();
    14. }
    15. /**
    16. * 文件上传下载的组件
    17. * @return
    18. */
    19. @Bean
    20. public MultipartResolver multipartResolver() {
    21. CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
    22. multipartResolver.setDefaultEncoding("UTF-8");
    23. multipartResolver.setMaxUploadSize(1024*1024*10);
    24. return multipartResolver;
    25. }
    26. /**
    27. * 注册处理国际化资源的组件
    28. * @return
    29. */
    30. /* @Bean
    31. public AcceptHeaderLocaleResolver localeResolver() {
    32. AcceptHeaderLocaleResolver acceptHeaderLocaleResolver = new AcceptHeaderLocaleResolver();
    33. return acceptHeaderLocaleResolver;
    34. }*/
    35. @Override
    36. public void addInterceptors(InterceptorRegistry registry) {
    37. registry.addInterceptor(tulingInterceptor()).addPathPatterns("/*");
    38. }
    39. /**
    40. * 方法实现说明:配置试图解析器
    41. * @author:xsls
    42. * @exception:
    43. * @date:2019/8/6 16:23
    44. */
    45. @Bean
    46. public InternalResourceViewResolver internalResourceViewResolver() {
    47. InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
    48. viewResolver.setSuffix(".jsp");
    49. viewResolver.setPrefix("/WEB-INF/jsp/");
    50. return viewResolver;
    51. }
    52. @Override
    53. public void configureMessageConverters(List> converters) {
    54. converters.add(new MappingJackson2HttpMessageConverter());
    55. }

    自己去添加个Controller进行测试

    OK, 现在可以访问你的SpringMVC了

    4、SPI的方式SpringMVC启动原理

    接着我们来看看SPI方式的原理是什么:

    SpringMVC 大致可以分为 启动 和请求 2大部分, 所以我们本文先研究启动部分

    流程图:

    源码流程

    1. 外置Tomcat启动的时候通过SPI 找到我们应用中的/META-INF/service/javax.servlet.ServletContainerInitializer

    0

    1. 调用SpringServletContainerInitializer.onStartUp()

    0

      1. 调用onStartUp()前会先找到@HandlesTypes(WebApplicationInitializer.class) 所有实现了WebApplicationInitializer的类,传入到OnStartup的webAppInitializerClasses参数中,并传入Servlet上下文对象。
      2. 重点关注这组类:他们组成了父子容器

    0

    1. 找到所有WebApplicationInitializer的实现类后, 不是接口、不是抽象则通过反射进行实例化(所以,你会发现内部实现类都是抽象的,你想让其起作用我们必须添加一个自定义实现类,在下文提供我的自定义实现类)
    2. 调用所有上一步实例化后的对象的onStartup方法

    0

    0

    1. 首先来到AbstractDispatcherServletInitializer#onStartup再执行super.onStartup(servletContext);

    1. @Override
    2. public void onStartup(ServletContext servletContext) throws ServletException {
    3. //实例化我们的spring root上下文
    4. super.onStartup(servletContext);
    5. //注册我们的DispatcherServlet 创建我们spring web 上下文对象
    6. registerDispatcherServlet(servletContext);

     创建父容器——ContextLoaderListener

    2.父类AbstractContextLoaderInitializer#onStartup执行registerContextLoaderListener(servletContext);

    1. createRootApplicationContext()该方法中会创建父容器
      1. 该方法是抽象方法,实现类是AbstractAnnotationConfigDispatcherServletInitializer
        1. 调用getRootConfigClasses();方法获取父容器配置类(此抽象方法在我们自定义的子类中实现提供我们自定义的映射路径 )
        2. 创建父容器,注册配置类

    0

    1. 会创建ContextLoaderListener并通过ServletContext注册

    0

    看完大家是不是感觉跟我们XML的配置ContextLoaderListener对上了:

    0

    创建子容器——DispatcherServlet

    3.回到AbstractDispatcherServletInitializer#onStartup再执行registerDispatcherServlet(servletContext);

    0

    registerDispatcherServlet方法说明:

    1. 调用createServletApplicationContext创建子容器
      1. 该方法是抽象方法,实现类是AbstractAnnotationConfigDispatcherServletInitializer
        1. 创建子容器(下图很明显不多介绍)
        2. 调用抽象方法:getServletConfigClasses();获得配置类(此抽象方法在我们自定义的子类中实现提供我们自定义的配置类 )
        3. 配置类除了可以通过ApplicationContext()构造函数的方式传入 , 也可以通过这种方式动态添加,不知道了吧~

    0

    1. 调用createDispatcherServlet(servletAppContext);创建DispatcherServlet
    2. 设置启动时加载:registration.setLoadOnStartup(1);
    3. 调用抽象方法设置映射路径:getServletMappings()(此抽象方法在我们自定义的子类中实现提供我们自定义的映射路径 )

    看完大家是不是感觉跟我们XML的配置DispatcherServlet对上了

    0

    4. 初始化ContextLoaderListener

    0

    ContextLoaderListener加载过程比较简单:

    外置tomcat会帮我们调用ContextLoaderListener#contextInitialized 进行初始化

    1. xml的方式下会判断容器为空时创建父容器
    2. 在里面会调用父容器的refresh方法加载
    3. 将父容器存入到Servlet域中供子容器使用

    0

    5. 初始化DispatcherServlet

    0

    可以看到流程比ContextLoaderListener流程更多

    外置tomcat会帮我们调用DispatcherServlet#init()   进行初始化--->重点关注:initWebApplicationContext方法

    1. getWebApplicationContext(getServletContext())获得父容器(从之前的Servlet域中拿到)
    2. cwac.setParent(rootContext);给子容器设置父容器
    3. 调用configureAndRefreshWebApplicationContext(cwac);

    0

      1. 注册一个监听器(该监听会初始化springmvc所需信息)
        1. ContextRefreshedEvent可以看到该监听器监听的是容器refreshed事件, 会在finishRefresh中发布
      2. 刷新容器

    0

    当执行refresh 即加载ioc容器 完了会调用finishRefresh():

    1. publishEvent(new ContextRefreshedEvent(this));发布ContextRefreshedEvent事件
    2. 触发上面的ContextRefreshListener监听器:

    ---->FrameworkServlet.this.onApplicationEvent(event);

    -------->onRefresh(event.getApplicationContext());

    -------------->initStrategies(context);

    1. protected void initStrategies(ApplicationContext context) {
    2. //初始化我们web上下文对象的 用于文件上传下载的解析器对象
    3. initMultipartResolver(context);
    4. //初始化我们web上下文对象用于处理国际化资源的
    5. initLocaleResolver(context);
    6. //主题解析器对象初始化
    7. initThemeResolver(context);
    8. //初始化我们的HandlerMapping
    9. initHandlerMappings(context);
    10. //实例化我们的HandlerAdapters
    11. initHandlerAdapters(context);
    12. //实例化我们处理器异常解析器对象
    13. initHandlerExceptionResolvers(context);
    14. initRequestToViewNameTranslator(context);
    15. //给DispatcherSerlvet的ViewResolvers处理器
    16. initViewResolvers(context);
    17. initFlashMapManager(context);

    这里面的每一个方法不用太细看, 就是给SpringMVC准备初始化的数据, 为后续SpringMVC处理请求做准备

    基本都是从容器中拿到已经配置的Bean(RequestMappingHandlerMapping、RequestMappingHandlerAdapter、HandlerExceptionResolver )放到dispatcherServlet中做准备:

    0

    0

    0

    ...

    但是这些Bean又是从哪来的呢?? 来来来, 回到我们的WebAppConfig

    我们使用的一个@EnableWebMvc

    1. 导入了DelegatingWebMvcConfiguration@Import(DelegatingWebMvcConfiguration.class)
    2. DelegatingWebMvcConfiguration的父类就配置了这些Bean
    3. 而且我告诉你SpringBoot也是用的这种方式,

    0

    总结

    1. Tomcat在启动时会通过SPI注册 ContextLoaderListener和DispatcherServlet对象
      1. 同时创建父子容器
        1. 分别创建在ContextLoaderListener初始化时创建父容器设置配置类
        2. 在DispatcherServlet初始化时创建子容器 即2个ApplicationContext实例设置配置类
    2. Tomcat在启动时执行ContextLoaderListener和DispatcherServlet对象的初始化方法, 执行容器refresh进行加载
    3. 在子容器加载时 创建SpringMVC所需的Bean和预准备的数据:(通过配置类+@EnableWebMvc配置(DelegatingWebMvcConfiguration)——可实现WebMvcConfigurer进行定制扩展)
      1. RequestMappingHandlerMapping,它会处理@RequestMapping 注解
      2. RequestMappingHandlerAdapter,则是处理请求的适配器,确定调用哪个类的哪个方法,并且构造方法参数,返回值。
      3. HandlerExceptionResolver 错误视图解析器
      4. addDefaultHttpMessageConverters 添加默认的消息转换器(解析json、解析xml)
      5. 等....
    4. 子容器需要注入父容器的Bean时(比如Controller中需要@Autowired Service的Bean); 会先从子容器中找,没找到会去父容器中找: 详情见AbstractBeanFactory#doGetBean方法

     

    1. /**
    2. * 一般情况下,只有Spring 和SpringMvc整合的时才会有父子容器的概念,
    3. * 作用:
    4. * 比如我们的Controller中注入Service的时候,发现我们依赖的是一个引用对象,那么他就会调用getBean去把service找出来
    5. * 但是当前所在的容器是web子容器,那么就会在这里的 先去父容器找
    6. */
    7. BeanFactory parentBeanFactory = getParentBeanFactory();
    8. //若存在父工厂,且当前的bean工厂不存在当前的bean定义,那么bean定义是存在于父beanFacotry中
    9. if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
    10. //获取bean的原始名称
    11. String nameToLookup = originalBeanName(name);
    12. //若为 AbstractBeanFactory 类型,委托父类处理
    13. if (parentBeanFactory instanceof AbstractBeanFactory) {
    14. return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
    15. nameToLookup, requiredType, args, typeCheckOnly);
    16. }
    17. else if (args != null) {
    18. // 委托给构造函数 getBean() 处理
    19. return (T) parentBeanFactory.getBean(nameToLookup, args);
    20. }
    21. else {
    22. // 没有 args,委托给标准的 getBean() 处理
    23. return parentBeanFactory.getBean(nameToLookup, requiredType);
    24. }

  • 相关阅读:
    C Primer Plus(6) 中文版 第4章 字符串和格式化输入/输出 4.2 字符串简介
    23种设计模式--简单工厂模式、工厂方法模式、抽象工厂模式
    1517_AURIX TC275 SRI中的仲裁功能
    【Qt+FFmpeg】 - FFmpeg解码详细流程
    CSS3之颜色渐变效果
    “智”造未来,江西同为科技(TOWE)参展第四届广州“两交会”圆满落幕
    国内外17个学术论文网站推荐,记得收藏哦!
    前端面试,备考第 12 天 - 原型与原型链
    经典OJ题:找环节点——代码解析
    python第三次作业
  • 原文地址:https://blog.csdn.net/qq_29434541/article/details/139394500