Spring MVC 父子容器是初学 Spring MVC 时最先接触到 Spring 知识点之一,还记得我刚工作那会,项目基础架构是其他同事搭建的,其中就用到了 Spring MVC 中的父子容器,还把 Spring MVC 中的不同层拆成了不同的 maven 模块。这里暂不讨论这种模块拆分方式的优劣,Spring 为什么设计出具有层次结构的容器呢?Web 环境中什么场景会用到这种具有层次结构的容器?
Spring 父子容器是什么?
父子容器并非 Spring MVC 的专利,在普通的 Spring 环境下 Spring 就已经设计出具有层次结构的容器了,这种设计方式也并非 Spring 独创,其工作方式和 ClassLoader 很相似,每个容器有一个自己的父容器,但是与 ClassLoader 不同的是,通过容器查找 bean 时是优先从子容器查找,如果找不到才会从父容器中查找。当应用中存在多个容器时,这种设计方式可以将公共的 bean 放到父容器中,如果父容器中的 bean 不适用,子容器还可以覆盖父容器中的 bean。
Spring 中的容器有两种,一种是常用的 ApplicationContext,父子容器相关的层次结构如下。
另一种容器是最底层的 BeanFactory,ApplicationContext 就依托于底层的 BeanFactory 查找 Bean。
BeanFactory 最终的实现是 DefaultListableBeanFactory,其查找 bean 的部分源码如下,从中可以看出 Spring 是优先从子容器中查找 bean 的,如果查不到会再次从父容器中查找。
Spring MVC 环境下父子容器应用场景
Spring MVC 负责控制整个请求流程的核心类是 DispatcherServlet,这个 DispatcherServlet 会关联一个 ApplicationContext,通常情况下,一个应用中有一个 DispatcherServlet 就足够了。
但是呢,凡事都有例外,如果你正在做一个商城,不同的模块如商品、购物车、订单模块使用了不同的 DispatcherServlet 来处理请求,这就意味着一个应用中出现了多个 ApplicationContext,由于每个 ApplicationContext 是独立的,因此订单模块就不能直接使用商品或购物车模块的服务来下单,订单模块把商品或购物车模块的服务注册到自身所在的容器中虽然能解决问题,但是这也意味着多个容器中保存了多个相同类型的 bean,那怎么解决呢?将这些公共的服务注册到相同的父容器中,这样每个子容器都能使用到父容器中的公共 bean。
ApplicationContext 在 Spring MVC 环境下的实现是其子接口 WebApplicationConetxt,这个公共的 WebApplicationContext 也被称为 Root WebApplicationContext。通过父子容器对 bean 进行拆分之后,可以将与 Web 环境有关的 bean ,如 Controller、ViewResolver、HandlerMapping 等保存到 DispatcherServlet 中的 WebApplicationContext,而跨越多个 DispatcherServlet 共享的 Service、Repository bean 可以存放到 Root WebApplicationContext。
使用父容器之后的商城应用,自然而然,负责订单的 DispatcherServlet 子容器中的 Controller 可以使用父容器中的购物车 Service,达到了复用 bean 的目的。
Spring MVC 环境下父子容器的初始化过程
Spring 子容器依赖父容器,因此需要先对父容器初始化,然后才能对子容器初始化。Spring MVC是怎么保证父子容器初始化顺序的呢?
Spring MVC 依托于 Servlet 规范。Servlet 规范中 ,所有的 Servlet 具有一个相同的上下文 ServletContext,ServletContext 将优先于 Servlet 初始化,Spring 利用了这个特性,在 ServletContext 初始化时创建父容器,并将其绑定到 ServletContext 的属性中,然后在每个 DispatcherServlet 初始过程中创建子容器并将 ServletContext 中的容器设置为父容器。
父容器初始化
Servlet 容器会在 ServletContext 初始化时触发事件,然后由 ServletContextListener 监听,Spring 提供了这个接口的实现 ContextLoaderListener 并在初始化时创建父容器。代码如下。
public class ContextLoaderListener extends ContextLoader implements ServletContextListener { ... 省略部分代码 @Override public void contextInitialized(ServletContextEvent event) { initWebApplicationContext(event.getServletContext()); } }
ContextLoaderListener 仅调用了父类 ContextLoader 中的 initWebApplicationContext方法创建容器,该方法会将创建后的容器存至名为WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE的属性中。
子容器初始化
子容器由 DispatcherServlet 进行初始化,简化后的类图如下所示。
首先 Spring 提供的 HttpServletBean 继承 HttpServlet 类,重写了 init 方法,并调用了抽象方法 initServletBean。
然后 HttpServletBean 的子类 FrameworkServlet 重写了 initServletBean 方法,并调用了 initWebApplicationContext 方法,这个方法将会完成子容器的初始化工作。子容器初始化时从 ServletContext 属性中取出并设置父容器。
DispatcherServlet 直接使用了父类 FrameworkServlet 的初始化方法。
Spring MVC 环境下父子容器配置
使用 Spring 需要将 bean 注入 Spring 容器,针对 web 环境,配置父容器需要使用 ContextLoaderListener,配置子容器需要使用 DispatcherServlet。具体来分又有 xml 和注解两种配置方式。
xml 配置 Spring MVC 容器
传统的 Java Web 应用需要将应用中使用的组件清单配置到 web.xml 文件中,针对 Spring 容器的配置如下。
<web-app> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/root-context.xml</param-value> </context-param> <servlet> <servlet-name>app1</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/app1-context.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>app1</servlet-name> <url-pattern>/app1/*</url-pattern> </servlet-mapping> </web-app>
ContextLoaderListener 被配置到监听器列表,ServletContext 初始化时会使用 context-param 中参数名为 contextConfigLocation 的值作为配置文件路径初始化容器。
DispatcherServlet 也需要添加到 Servlet 列表,DispatcherServlet 同时也会使用初始化参数 contextConfigLocation 作为配置文件的路径初始化容器。
注解配置 Spring MVC 容器
为了减少手工在 web.xml 文件进行配置的工作,Servlet 3.0 提供了一个 ServletContainerInitializer 接口,Servlet 容器启动时会扫描类路径,并在容器初始化时回调这个接口中的方法,将用户感兴趣的类型传递到方法参数中。这个接口的定义如下。
public interface ServletContainerInitializer { public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException; }
spring-framework 3.1 开始支持了这个特性,提供了一个 SpringServletContainerInitializer 类,这个类会对 WebApplicationInitializer 类型进行处理,因此将 WebApplicationInitializer 的实现直接添加到类路径中即可。WebApplicationInitializer 接口定义如下。
public interface WebApplicationInitializer { void onStartup(ServletContext servletContext) throws ServletException; }
为了便于配置上下文及 DispatcherServlet,Spring 提供了 WebApplicationInitializer 的实现类 AbstractAnnotationConfigDispatcherServletInitializer,因此我们实现这个类就可以配置上下文。
和上述 web.xml 文件等价的配置如下。
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { return new Class<?>[] { RootConfig.class }; } @Override protected Class<?>[] getServletConfigClasses() { return new Class<?>[] { App1Config.class }; } @Override protected String[] getServletMappings() { return new String[] { "/app1/*" }; } }
etRootConfigClasses 方法返回的是根应用上下文的配置,getServletConfigClasses 方法返回的是 DispatcherServlet 的上下文配置,getServletMappings 则用来指定 DispatcherServlet 处理的路径。
总结
Spring MVC 上下文虽然使用场景上来说并不多,但它却是了解 Spring MVC 必不可少的内容。后面会继续深入探索 Spring MVC 的使用及其设计实现。有问题欢迎留言探讨。