惊落一身雪 发表于 2024-6-14 21:30:50

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

1、Spring整合SpringMVC

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


[*]通常我们会设置父容器(Spring)管理Service、Dao层的Bean, 子容器(SpringMVC)管理Controller的Bean .
[*]子容器可以访问父容器的Bean, 父容器无法访问子容器的Bean。
实现:
相信大家在SSM框架整合的时候都曾在web.xml设置过这段:
<!--spring 基于web应用的启动-->
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--全局参数:spring配置文件-->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-core.xml</param-value>
</context-param>
<!--前端调度器servlet-->
<servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!--设置配置文件的路径-->
    <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-mvc.xml</param-value>
</init-param>
    <!--设置启动即加载-->
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>dispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern> 但是它的作用是什么知道吗?

https://img-blog.csdnimg.cn/img_convert/702ef3bd810056bcaff581ebedda267b.png
有人大概只知道DispatcherServlet叫前端控制器,是SpringMVC处理前端请求的一个核心调理器
那它为什么能处理请求?处理之前做了什么准备工作呢?又是怎么和Spring联合起来的呢?
为什么有了DispatcherServlet还要个ContextLoaderListener, 配一个不行吗?干嘛要配俩啊?
看完本文你就会有答案!

https://img-blog.csdnimg.cn/img_convert/52df3a54633aed630040ad5b57c5eee2.png
还有人大概会以为, 我现在都用SpringBoot开发, 哪还要配这玩意.......

https://img-blog.csdnimg.cn/img_convert/d0554dcabe4fe22a1eb534d1e17f9cb0.png
这就是典型的SpringBoot使用后遗症,SpringBoot降低了使用难度,但是从某种程度来说,也让初级的程序员变得更加小白,把实现原理都隐藏起来了而我们只管用,一旦涉及扩展就束手无策。
那当然我们本日不讲SpringBoot,我们本日用贴近SpringBoot的方式来讲SpringMVC。也就是零设置(零xml)的放式来说明SpringMVC的原理!!
   此方式作为我们本文重点先容,也是很多人缺失的一种方式, 实在早在Spring3+就已经提供, 只不外我们直到SpringBoot才使用该方式举行自动设置, 这也是很多人从xml调到SpringBoot不适应的原因, 因为你缺失了这个版本。 以是我们以这种方式作为源码切入点既可以理解到XML的方式又能兼顾到SpringBoot的方式 。


2、零设置SpringMVC实现方式:

那没有设置就需要省略掉web.xml 怎么省略呢?
在Servlet3.0提供的规范文档中可以找到2种方式:


[*]注解的方式

[*]@WebServlet
[*]@WebFilter
[*]@WebListener
但是这种方式倒霉于扩展, 并且如果编写在jar包中tomcat是无法感知到的。


[*]SPI的方式
在Serlvet3-1的规范手册中:就提供了一种更加易于扩展可用于共享库可插拔的一种方式,参见8.2.4:
https://img-blog.csdnimg.cn/direct/95a9e92de95e4544a3d7f60608e035f6.png
也就是让你在应用META-INF/services 路径下 放一个 javax.servlet.ServletContainerInitailizer ——即SPI规范
SPI 我们叫他服务接口扩展,(Service Provider Interface) 直译服务提供商接口, 不要被这个名字唬到了, 实在很好理解的一个东西:
实在就是根据Servlet厂商(服务提供商)提供要求的一个接口, 在固定的目录(META-INF/services)放上以接口全类名 为定名的文件, 文件中放入接口的实现的全类名,该类由我们本身实现,按照这种约定的方式(即SPI规范),服务提供商会调用文件中实现类的方法, 从而完成扩展。
ok 那我们知道了SPI是什么,我们是不是可以在Web应用中,在Servlet的SPI放入对应的接口文件:

https://img-blog.csdnimg.cn/img_convert/e8abf89ddcda3bf128c0ad879d6d323a.png
放入实现类:

https://img-blog.csdnimg.cn/img_convert/1ac3ab499435f957212b2a73a0b9907b.png
通过ServletContext就可以动态注册三大组件:以Servlet注册为例:
public class TulingSpringServletContainerInitializer extends SpringServletContainerInitializer {

    @Override
    public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {

      // 通过servletContext动态添加Servlet
      servletContext.addServlet("spiServlet", new HttpServlet() {
            @Override
            protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                resp.getWriter().write("spiServlet--doGet");
            }
      }).addMapping("/spiServlet.do");


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

TulingStarterInitializer


[*]此类继承AbstractAnnotationConfigDispatcherServletInitializer 这是个啥? 待会我们讲原理来先容
[*]getRootConfigClasses 提供父容器的设置类
[*]getServletConfigClasses 提供子容器的设置类
[*]getServletMappings 设置DispatcherServlet的映射
publicclass TulingStarterInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

   /**
    * 方法实现说明:IOC 父容器的启动类
    * @author:xsls
    * @date:2019/7/31 22:12
    */
   @Override
   protected Class<?>[] getRootConfigClasses() {
      return new Class[]{RootConfig.class};
   }

   /**
    * 方法实现说明 IOC子容器配置 web容器配置
    * @author:xsls
    * @date:2019/7/31 22:12
    */
   @Override
   protected Class<?>[] getServletConfigClasses() {
      return new Class[]{WebAppConfig.class};
   }

   /**
    * 方法实现说明
    * @author:xsls
    * @return: 我们前端控制器DispatcherServlet的拦截路径
    * @exception:
    * @date:2019/7/31 22:16
    */
   @Override
   protected String[] getServletMappings() {
      return new String[]{"/"}; RootConfig



[*]父容器的设置类 =以前的spring.xml
[*]扫描的包排撤除@Controller
@Configuration
@ComponentScan(basePackages = "com.tuling",excludeFilters = {
      @ComponentScan.Filter(type = FilterType.ANNOTATION,value={RestController.class,Controller.class}),
      @ComponentScan.Filter(type = ASSIGNABLE_TYPE,value =WebAppConfig.class ),
})
public class RootConfig {

} WebAppConfig



[*]子容器的设置类 =以前的spring-mvc.xml
[*]扫描的包:包罗掉@Controller
@Configuration
@ComponentScan(basePackages = {"com.tuling"},includeFilters = {
      @ComponentScan.Filter(type = FilterType.ANNOTATION,value = {RestController.class, Controller.class})
},useDefaultFilters =false)
@EnableWebMvc   // ≈<mvc:annotation-driven/>
public class WebAppConfig implements WebMvcConfigurer{

   /**
    * 配置拦截器
    * @return
    */
   @Bean
   public TulingInterceptor tulingInterceptor() {
      return new TulingInterceptor();
   }

   /**
    * 文件上传下载的组件
    * @return
    */
   @Bean
   public MultipartResolver multipartResolver() {
      CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
      multipartResolver.setDefaultEncoding("UTF-8");
      multipartResolver.setMaxUploadSize(1024*1024*10);
      return multipartResolver;
   }

   /**
    * 注册处理国际化资源的组件
    * @return
    */
/* @Bean
   public AcceptHeaderLocaleResolver localeResolver() {
      AcceptHeaderLocaleResolver acceptHeaderLocaleResolver = new AcceptHeaderLocaleResolver();
      return acceptHeaderLocaleResolver;
   }*/

   @Override
   public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(tulingInterceptor()).addPathPatterns("/*");
   }


   /**
    * 方法实现说明:配置试图解析器
    * @author:xsls
    * @exception:
    * @date:2019/8/6 16:23
    */
   @Bean
   public InternalResourceViewResolver internalResourceViewResolver() {
      InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
      viewResolver.setSuffix(".jsp");
      viewResolver.setPrefix("/WEB-INF/jsp/");
      return viewResolver;
   }



   @Override
   public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
      converters.add(new MappingJackson2HttpMessageConverter());
   } 本身去添加个Controller举行测试
OK, 现在可以访问你的SpringMVC了
4、SPI的方式SpringMVC启动原理

接着我们来看看SPI方式的原理是什么:
SpringMVC 大致可以分为 启动 和请求 2大部分, 以是我们本文先研究启动部分
流程图:
https://img-blog.csdnimg.cn/direct/91e000244527410c84e7facb7697706f.png
源码流程

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

https://img-blog.csdnimg.cn/img_convert/dd65ab48e26cd020317c059864ba744b.png

[*]调用SpringServletContainerInitializer.onStartUp()

https://img-blog.csdnimg.cn/img_convert/87eaa3d582ea77eacc313005ea2a551d.png


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


https://img-blog.csdnimg.cn/img_convert/425da093cf74122f2a394ac5178829b6.png

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

https://img-blog.csdnimg.cn/img_convert/fab8abe1ebffd2df969185989471fc4d.png

https://img-blog.csdnimg.cn/img_convert/ef516941de280b21d111afaceba94912.png
1. 起首来到AbstractDispatcherServletInitializer#onStartup再执行super.onStartup(servletContext);
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
   //实例化我们的spring root上下文
   super.onStartup(servletContext);
   //注册我们的DispatcherServlet   创建我们spring web 上下文对象
   registerDispatcherServlet(servletContext);  创建父容器——ContextLoaderListener

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

[*]createRootApplicationContext()该方法中会创建父容器
[*]该方法是抽象方法,实现类是AbstractAnnotationConfigDispatcherServletInitializer
[*]调用getRootConfigClasses();方法获取父容器设置类(此抽象方法在我们自界说的子类中实现提供我们自界说的映射路径 )
[*]创建父容器,注册设置类



https://img-blog.csdnimg.cn/img_convert/2ef121c513756e49f5f7a6bfa9878d81.png

[*]会创建ContextLoaderListener并通过ServletContext注册

https://img-blog.csdnimg.cn/img_convert/5f37dc22f9d684ed1001822c5d6a211f.png
看完大家是不是感觉跟我们XML的设置ContextLoaderListener对上了:

https://img-blog.csdnimg.cn/img_convert/9832df9faea9197380189f39b5433a55.png
创建子容器——DispatcherServlet
3.回到AbstractDispatcherServletInitializer#onStartup再执行registerDispatcherServlet(servletContext);

https://img-blog.csdnimg.cn/img_convert/a008462065a7edc7a8bf6ed183d42f9f.png
registerDispatcherServlet方法说明:

[*]调用createServletApplicationContext创建子容器
[*]该方法是抽象方法,实现类是AbstractAnnotationConfigDispatcherServletInitializer
[*]创建子容器(下图很显着不多先容)
[*]调用抽象方法:getServletConfigClasses();获得设置类(此抽象方法在我们自界说的子类中实现提供我们自界说的设置类 )
[*]设置类除了可以通过ApplicationContext()构造函数的方式传入 , 也可以通过这种方式动态添加,不知道了吧~



https://img-blog.csdnimg.cn/img_convert/41b9b905c89abc6b6785d5426f5c63d1.png

[*]调用createDispatcherServlet(servletAppContext);创建DispatcherServlet
[*]设置启动时加载:registration.setLoadOnStartup(1);
[*]调用抽象方法设置映射路径:getServletMappings()(此抽象方法在我们自界说的子类中实现提供我们自界说的映射路径 )
看完大家是不是感觉跟我们XML的设置DispatcherServlet对上了

https://img-blog.csdnimg.cn/img_convert/1bb8113a3993543171715d4ff9781e04.png
4. 初始化ContextLoaderListener

https://img-blog.csdnimg.cn/img_convert/43ee589ee08d43a8c6281a2a70de1792.png
ContextLoaderListener加载过程比较简朴:
外置tomcat会帮我们调用ContextLoaderListener#contextInitialized 举行初始化

[*]xml的方式下会判断容器为空时创建父容器
[*]在内里会调用父容器的refresh方法加载
[*]将父容器存入到Servlet域中供子容器使用

https://img-blog.csdnimg.cn/img_convert/2a452d2b62fed33309e5c08ac8204903.png
5. 初始化DispatcherServlet

https://img-blog.csdnimg.cn/img_convert/8fbbb6da957be04a2ff759099f8af6e4.png
可以看到流程比ContextLoaderListener流程更多
外置tomcat会帮我们调用DispatcherServlet#init()   举行初始化--->重点关注:initWebApplicationContext方法

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

https://img-blog.csdnimg.cn/img_convert/57bed43d68f124026af42610d6b06dd9.png


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

[*]刷新容器


https://img-blog.csdnimg.cn/img_convert/3d40ce0ac1cfac5679250fa1bab8dd3d.png
当执行refresh 即加载ioc容器 完了会调用finishRefresh():

[*]publishEvent(new ContextRefreshedEvent(this));发布ContextRefreshedEvent事件
[*]触发上面的ContextRefreshListener监听器:
---->FrameworkServlet.this.onApplicationEvent(event);
-------->onRefresh(event.getApplicationContext());
-------------->initStrategies(context);
protected void initStrategies(ApplicationContext context) {
   //初始化我们web上下文对象的 用于文件上传下载的解析器对象
   initMultipartResolver(context);
   //初始化我们web上下文对象用于处理国际化资源的
   initLocaleResolver(context);
   //主题解析器对象初始化
   initThemeResolver(context);
   //初始化我们的HandlerMapping
   initHandlerMappings(context);
   //实例化我们的HandlerAdapters
   initHandlerAdapters(context);
   //实例化我们处理器异常解析器对象
   initHandlerExceptionResolvers(context);
   initRequestToViewNameTranslator(context);
   //给DispatcherSerlvet的ViewResolvers处理器
   initViewResolvers(context);
   initFlashMapManager(context); 这内里的每一个方法不用太细看, 就是给SpringMVC准备初始化的数据, 为后续SpringMVC处理请求做准备
根本都是从容器中拿到已经设置的Bean(RequestMappingHandlerMapping、RequestMappingHandlerAdapter、HandlerExceptionResolver )放到dispatcherServlet中做准备:

https://img-blog.csdnimg.cn/img_convert/678ff3e9164d83dd283078f23323e00f.png

https://img-blog.csdnimg.cn/img_convert/6396e3ff97df14b6bcfc2147419d915e.png

https://img-blog.csdnimg.cn/img_convert/fc8d8401420a4b6943afb93d414f8189.png
...
但是这些Bean又是从哪来的呢?? 来来来, 回到我们的WebAppConfig
我们使用的一个@EnableWebMvc

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

https://img-blog.csdnimg.cn/img_convert/3f51df47a991e8a3e6bbb6a7826e75bf.png


总结


[*]Tomcat在启动时会通过SPI注册 ContextLoaderListener和DispatcherServlet对象
[*]同时创建父子容器
[*]分别创建在ContextLoaderListener初始化时创建父容器设置设置类
[*]在DispatcherServlet初始化时创建子容器 即2个ApplicationContext实例设置设置类


[*]Tomcat在启动时执行ContextLoaderListener和DispatcherServlet对象的初始化方法, 执行容器refresh举行加载
[*]在子容器加载时 创建SpringMVC所需的Bean和预准备的数据:(通过设置类+@EnableWebMvc设置(DelegatingWebMvcConfiguration)——可实现WebMvcConfigurer举行定制扩展)
[*]RequestMappingHandlerMapping,它会处理@RequestMapping 注解
[*]RequestMappingHandlerAdapter,则是处理请求的适配器,确定调用哪个类的哪个方法,并且构造方法参数,返回值。
[*]HandlerExceptionResolver 错误视图剖析器
[*]addDefaultHttpMessageConverters 添加默认的消息转换器(剖析json、剖析xml)
[*]等....

[*]子容器需要注入父容器的Bean时(比如Controller中需要@Autowired Service的Bean); 会先从子容器中找,没找到会去父容器中找: 详情见AbstractBeanFactory#doGetBean方法
 

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

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: Spring系列-SpringMvc父子容器启动原理剖析