回顾

SpringMVC的具体执行流程

SpringMVC之父子容器启动原理 - 图1

Spring整合SpringMVC

SpringMVC之父子容器启动原理 - 图2
说到Spring整合SpringMVC唯一体现就是父子容器

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

    XML实现方式

    1. <?xml version="1.0" encoding="UTF-8"?>
    2. <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4. xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
    5. version="4.0">
    6. <!--spring 基于web应用的启动-->
    7. <listener>
    8. <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    9. </listener>
    10. <!--全局参数:spring配置文件-->
    11. <context-param>
    12. <param-name>contextConfigLocation</param-name>
    13. <param-value>classpath:spring-core.xml</param-value>
    14. </context-param>
    15. <!--前端调度器servlet-->
    16. <servlet>
    17. <servlet-name>dispatcherServlet</servlet-name>
    18. <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    19. <!--设置配置文件的路径-->
    20. <init-param>
    21. <param-name>contextConfigLocation</param-name>
    22. <param-value>classpath:spring-mvc.xml</param-value>
    23. </init-param>
    24. <!--设置启动即加载-->
    25. <load-on-startup>1</load-on-startup>
    26. </servlet>
    27. <servlet-mapping>
    28. <servlet-name>dispatcherServlet</servlet-name>
    29. <url-pattern>/</url-pattern>
    30. </servlet-mapping>
    31. </web-app>

    注解实现方式

  • @WebServlet

  • @WebFilter
  • @WebListener

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

SPI的方式

Servlet-3-1的规范手册中,就提供了一种更加易于扩展可用于共享库可插拔的一种方式.
也就是在META-INF/services路径下放一个javax.servlet.ServletContainerInitailizer
什么是SPI?
SPI就是Service Provider Interface,服务提供商接口,我们叫它服务接口扩展.
其实这个是根据Servlet厂商提供要求的一个接口,在固定的目录(META-INF/services)放上以接口全类名为命名的文件,
文件中放入接口的实现的全类名,该类由我们自己实现,按照这种约定的方式,服务提供商会调用文件中实现类的方法,从而完成扩展.
SPI例子

  • 定义一个接口

    1. public interface IUserDao {
    2. void save();
    3. }
  • 在固定的目录放上接口的文件名

SpringMVC之父子容器启动原理 - 图3

  • 文件中放入实现类(该实现类由你实现)

    public class UserDaoImpl implements IUserDao {
      @Override
      public void save() {
          System.out.println("UserDaoImpl.save...");
      }
    }
    
  • 通过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();
          }
      }
    }
    

    小结

  • 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

protected <T> T  doGetBean(
        String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
        throws BeansException {
  ....

// Check if bean definition exists in this factory.
BeanFactory parentBeanFactory = getParentBeanFactory();
// 因为单例池没有找到Bean,如果有父BeanFactory,就从父BeanFactory的BeanDefinitionMap中找是否存在这个bean
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
    // Not found -> check parent.
    // 如果从父BeanFactory找到了
    // &&&&xxx---->&xxx
    String nameToLookup = originalBeanName(name);
    if (parentBeanFactory instanceof AbstractBeanFactory) {
        return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
                nameToLookup, requiredType, args, typeCheckOnly);
    }
    else if (args != null) {
        // Delegation to parent with explicit args.
        return (T) parentBeanFactory.getBean(nameToLookup, args);
    }
    else if (requiredType != null) {
        // No args -> delegate to standard getBean method.
        return parentBeanFactory.getBean(nameToLookup, requiredType);
    }
    else {
        return (T) parentBeanFactory.getBean(nameToLookup);
    }
}
...
}

总结

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

就实现层面来说不用父子容器也是可以实现功能的,因为我们可以参考SpringBoot就没有使用父子容器.

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

    是否可以把所有Bean都通过Spring容器来管理(Spring的配置文件applicationContext.xml中配置全局扫描)

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

    是否可以把我们所需的Bean都放入Spring MVC子容器里面来管理(SpringMVC的配置文件的spring-servlet.xml中配置全局扫描)

    可以,因为父容器的提现无非是为了获取子容器不包含的Bean,如果全部包含在子容器完全用不到父容器了,所以是可以全部放在SpringMVC子容器来管理的
    虽然可以这么做不过一般应该是不推荐这么去做的.如果项目里有用到事务,或者AOP需要把这部分配置放到SpringMVC子容器的配置文件里来,不然一部分内容在子容器和一部分在父容器,可能导致事务或者AOP不生效.
    所以如果AOP和事务不生效也有可能是通过父容器去增强子容器,也就无法增强