13.0 SpringMVC父子容器

一、 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>
</servlet‐mapping>

配置了DispatcherServlet前端控制器和ContextLoaderListener上下文加载监听器

二、 零配置SpringMVC实现

省略web.xml的方式

1. 注解方式

  • @WebServlet
  • @WebFilter
  • @WebListener

这种方式不利于扩展,如果编写在jar包中toamcat是无法感知的

2. SPI的方式

服务接口扩展,更加易于扩展可用于共享库可插拔的一种方式

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

  1. 在固定的目录放上接口的文件名 (META-INF/javax.servlet.ServletContainerInitializer)

  2. 文件中放入实现类(该实现类由你实现): 一行一个实现类。

    com.ppku.javaweb.spi.CustomServletContainerInitializer
    
  3. 通过java.util.ServiceLoader提供的ServiceLoader就可以完成SPI的实现类加载

    // 示例
    public class App {
        public static void main(String[] args) {
            ServiceLoader<IUserDao> daos = ServiceLoader.load(IUserDao.class);
            for (IUserDao dao : daos) {
            	dao.save();
            }
       }
    }
    
    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");
    	}
    }
    
    

三、实现基于SPI规范的SpringMVC

在SpringMVC中, 上面的接口文件和实现类都把我们实现好了,甚至 ContextLoaderListener和DispatcherServlet都帮我们注册好了,我们只要让他生效

//1. 此类继承AbstractAnnotationConfigDispatcherServletInitializer
//2. getRootConfigClasses 提供父容器的配置类
//3. getServletConfigClasses 提供子容器的配置类
//4. getServletMappings 设置DispatcherServlet的映射
// CustomStarterInitializer

public class CustomStarterInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

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

    /**
    * 方法实现说明 IOC子容器配置 web容器配置
    */
    @Override
    protected Class<?>[] getServletConfigClasses() {
    	return new Class[]{WebAppConfig.class};
    }

    /**
    * 方法实现说明
    * @return: 我们前端控制器DispatcherServlet的拦截路径
    */
    @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{

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

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

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

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


    /**
    * 方法实现说明:配置试图解析器
    */
    @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());
    }
}

可以访问试试了

四、SPI方式SpringMVC启动原理

SpringMVC 大致可以分为 启动请求两大部分

1.源码流程

  • 外置Tomcat启动的时候通过SPI 找到我们应用的/METAINF/service/javax.servlet.ServletContainerInitializer

  • 调用SpringServletContainerInitializer.onStartUp()

    1. 调用onStartUp()前会先找到 @HandlesTypes(WebApplicationInitializer.class) 所有实现了WebApplicationInitializer的类,传入到OnStartup的 webAppInitializerClasses参数中,并传入Servlet上下文对象(该对象由tomcat传入可以动态注册三大组件)

    2. 这些类组成了父子容器:

      • WebApplicationInitializer#onStartup()

      • AbstractContextLoaderInitializer#onStartup() 注册ContextLoaderListener

        AbstractContextLoaderInitializer#registerContextLoadderListener() 创建父容器模板方法

        AbstractContextLoaderInitializer#createRootApplicationContext()将Listener添加到ServletContext

      • AbstractDispatcherServletInitializer#onStartup() 注册DispatchServlet

        AbstractDispatcherServletInitializer#registerContextLoadderListener() 创建子容器模板方法

        registerContextLoadderListener#createRootApplicationContext() 将servlet添加到ServletContext

      • AbstractAnnotationConfigDispatcherServletInitializer#createRootApplicationContext() 实现创建父容器

        AbstractAnnotationConfigDispatcherServletInitializer#createServletApplicationContext() 实现创建子容器

        AbstractAnnotationConfigDispatcherServletInitializer#getRootConfigClasses() 获取父容器配置类模板方法

        AbstractAnnotationConfigDispatcherServletInitializer#getServletConfigClasses() 获取子容器配置类模板方法

      • CustomStarterInitializer#getRootConfigClasses() 提供父容器配置类实现

        CustomStarterInitializer#getServletConfigClasses() 提供子容器配置类实现

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

    4. 调用所有上一步实例化后的对象的onStartup方法

2.创建父容器

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

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException{
        //实例化我们的spring root上下文
        super.onStartup(servletContext);
        //注册我们的DispatcherServlet 创建我们spring web 上下文对象
        registerDispatcherServlet(servletContext);
    }
    
    
  2. 父类AbstractContextLoaderInitializer#onStartup执行 registerContextLoaderListener(servletContext);

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

3.创建子容器

  1. .回到AbstractDispatcherServletInitializer#onStartup再执行 registerDispatcherServlet(servletContext)
  2. registerDispatcherServlet方法说明:
    • 调用createServletApplicationContext创建子容器
    • 该方法是抽象方法,实现类是 AbstractAnnotationConfigDispatcherServletInitializer
      1. 创建子容器
      2. 调用抽象方法:getServletConfigClasses();获得配 置类(此抽象方法在我们自定义的子类中实现提供我们 自定义的配置类 )
      3. 配置类除了可以通过ApplicationContext()构造函数的方式传入 ,也可以通过这种方式动态添加
    • 调用createDispatcherServlet(servletAppContext);创建DispatcherServlet
    • 设置启动时加载:registration.setLoadOnStartup(1);
    • 调用抽象方法设置映射路径:getServletMappings()(此抽象方法在我们自定义的子 类中实现提供我们自定义的映射路径 )

4. 初始化ContextLoaderListener

ContextLoaderListener加载过程比较简单: 外置tomcat会帮我们调用ContextLoaderListener#contextInitialized 进行初始化

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

5. 初始化DispatcherServlet

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

  1. getWebApplicationContext(getServletContext()获得父容器(从之前的 Servlet域中拿到)

  2. cwac.setParent(rootContext);给子容器设置父容器

  3. 调用configureAndRefreshWebApplicationContext(cwac)

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

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

    1. publishEvent(new ContextRefreshedEvent(this));发布 ContextRefreshedEvent事件

    2. 触发上面的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);
      }
      

6. WebAppConfig

我们使用的一个@EnableWebMvc

  1. 导入了 DelegatingWebMvcConfiguration@Import(DelegatingWebMvcConfiguratio n.class)

  2. DelegatingWebMvcConfiguration的父类就配置了这些Bean

  3. SpringBoot也是用的这种方式

    @EnableWebMvc
    public class WebAppConfig implements WebMvcConfigurer{}
    

7. 总结

  1. Tomcat在启动时会通过SPI注册 ContextLoaderListener和DispatcherServlet对象

    • 同时创建父子容器
      • 分别创建在ContextLoaderListener初始化时创建父容器设置配置类
      • 在DispatcherServlet初始化时创建子容器 即2个 ApplicationContext实例设置配置类
  2. Tomcat在启动时执行ContextLoaderListener和DispatcherServlet对象的初始化方法, 执行容器refresh进行加载

  3. 在子容器加载时 创建SpringMVC所需的Bean和预准备的数据:(通过配置类 +@EnableWebMvc配置(DelegatingWebMvcConfiguration)——可实现 WebMvcConfigurer进行定制扩展)

    • RequestMappingHandlerMapping,它会处理 @RequestMapping 注解
    • RequestMappingHandlerAdapter,则是处理请求的适配 器,确定调用哪个类的哪个方法,并且构造方法参数,返回值。
    • HandlerExceptionResolver 错误视图解析器
    • addDefaultHttpMessageConverters 添加默认的消息转 换器(解析json、解析xml)
    • ....
  4. 子容器需要注入父容器的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);
        }
    }
    

Spring和SpringMVC为什么需要父子容器?不要不行吗?

  1. 父子容器的主要作用应该是早期Spring为了划分框架边界。有点单一 职责的味道。service、dao层我们一般使用spring框架来管理、controller 层交给springmvc管理
  2. 规范整体架构 使 父容器service无法访问子容器controller、子容器 controller可以访问父容器 service
  3. 方便子容器的切换。如果现在我们想把web层从spring mvc替换成struts, 那么只需要将spring­mvc.xml替换成Struts的配置文件struts.xml即可,而 spring­core.xml不需要改变。
  4. 为了节省重复bean创建

是否可以把所有Bean都通过Spring容器来管理?

不可以,这样会导致我们请求接口的时候产生404。 如果所有的Bean都交给父容器, SpringMVC在初始化HandlerMethods的时候(initHandlerMethods)无法根据 Controller的handler方法注册HandlerMethod,并没有去查找父容器的bean; 也就无法根据请求URI 获取到 HandlerMethod来进行匹配

是否可以把我们所需的Bean都放入Spring-mvc子 容器里面来管理?

可以 , 因为父容器的体现无非是为了获取子容器不包含的bean, 如果全部包含在子容器完全用 不到父容器了, 所以是可以全部放在springmvc子容器来管理的。

但是如果项目里有用到事务、或者aop记得也需要把这部分配置需要放到Spring-mvc子容器的配置文件来,不然一部分内容在子容器和一部分内容在父容器,可能就会导致你的事物 或者AOP不生效。

posted @ 2022-10-17 20:46  浮沉丶随心  阅读(680)  评论(0编辑  收藏  举报