Spring MVC源码——Root WebApplicationContext

打算开始读一些框架的源码,先拿 Spring MVC 练练手,欢迎点击这里访问我的源码注释.


Spring MVC 的文档一开始就给出了这样的两段示例:

  1. public class MyWebApplicationInitializer implements WebApplicationInitializer {
  2. @Override
  3. public void onStartup(ServletContext servletCxt) {
  4. // Load Spring web application configuration
  5. AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
  6. ac.register(AppConfig.class);
  7. ac.refresh();
  8. // Create and register the DispatcherServlet
  9. DispatcherServlet servlet = new DispatcherServlet(ac);
  10. ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
  11. registration.setLoadOnStartup(1);
  12. registration.addMapping("/app/*");
  13. }
  14. }

web.xml :

  1. <web-app>
  2. <listener>
  3. <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  4. </listener>
  5. <context-param>
  6. <param-name>contextConfigLocation</param-name>
  7. <param-value>/WEB-INF/app-context.xml</param-value>
  8. </context-param>
  9. <servlet>
  10. <servlet-name>app</servlet-name>
  11. <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  12. <init-param>
  13. <param-name>contextConfigLocation</param-name>
  14. <param-value></param-value>
  15. </init-param>
  16. <load-on-startup>1</load-on-startup>
  17. </servlet>
  18. <servlet-mapping>
  19. <servlet-name>app</servlet-name>
  20. <url-pattern>/app/*</url-pattern>
  21. </servlet-mapping>
  22. </web-app>

我们按照 web.xml 中的实例来看一下 Spring MVC 初始化过程.

上下文层次结构

Spring MVC 的上下文有如下这样的层级:

图中的 Servlet WebApplicationContext 是与 DispatcherServlet 绑定的上下文, 其中还有 controllers、ViewResolver、HandlerMapping 等组件.

Root WebApplicationContext 不是必须的上下文, 在需要时,可以用来在多个 DispatcherServlet 间共享一些 bean.

Root WebApplicationContext 初始化和销毁

ContextLoaderListener

web.xml 中配置的 ContextLoaderListener 用于启动和终止 Spring 的 root WebApplicationContext.

ContextLoaderListener 本身的代码十分简单:

  1. public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
  2. public ContextLoaderListener() {
  3. }
  4. public ContextLoaderListener(WebApplicationContext context) {
  5. super(context);
  6. }
  7. /**
  8. * Initialize the root web application context.
  9. * Servlet 上下文初始化,调用父类的方法初始化 WebApplicationContext
  10. */
  11. @Override
  12. public void contextInitialized(ServletContextEvent event) {
  13. initWebApplicationContext(event.getServletContext());
  14. }
  15. /**
  16. * Close the root web application context.
  17. * Servlet 上下文被销毁,调用父类的方法销毁 WebApplicationContext
  18. */
  19. @Override
  20. public void contextDestroyed(ServletContextEvent event) {
  21. closeWebApplicationContext(event.getServletContext());
  22. // 销毁 ServletContext 上实现了 DisposableBean 的属性并移除他们
  23. ContextCleanupListener.cleanupAttributes(event.getServletContext());
  24. }
  25. }

ContextLoader

ContextLoaderListener 直接调用了父类 ContextLoader 的方法来初始化和销毁上下文.

ContextLoaderListener 在创建上下文时,会尝试读取 contextClass context-param 来指定上下文的类型,被指定的类需要实现 ConfigurableWebApplicationContext 接口. 如果没有获取到,默认会使用 WebApplicationContext.

初始化上下文时,会尝试读取 contextConfigLocation context-param, 作为 xml 文件的路径.

初始化上下文

initWebApplicationContext() 方法如下:

  1. public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
  2. // 检查有没有绑定上下文
  3. if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
  4. throw new IllegalStateException(
  5. "Cannot initialize context because there is already a root application context present - " +
  6. "check whether you have multiple ContextLoader* definitions in your web.xml!");
  7. }
  8. servletContext.log("Initializing Spring root WebApplicationContext");
  9. Log logger = LogFactory.getLog(ContextLoader.class);
  10. if (logger.isInfoEnabled()) {
  11. logger.info("Root WebApplicationContext: initialization started");
  12. }
  13. // 初始化开始时间
  14. long startTime = System.currentTimeMillis();
  15. try {
  16. // Store context in local instance variable, to guarantee that
  17. // it is available on ServletContext shutdown.
  18. // 保存上下文到本地实例变量中,保证上细纹能在 ServletContext 关闭时访问到
  19. if (this.context == null) {
  20. // 创建上下文
  21. this.context = createWebApplicationContext(servletContext);
  22. }
  23. if (this.context instanceof ConfigurableWebApplicationContext) {
  24. // 如果上下文实现了 ConfigurableWebApplicationContext
  25. ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
  26. if (!cwac.isActive()) {
  27. // 上下文还没有刷新,设置 父上下文(如果能找到),并且刷新
  28. // The context has not yet been refreshed -> provide services such as
  29. // setting the parent context, setting the application context id, etc
  30. if (cwac.getParent() == null) {
  31. // The context instance was injected without an explicit parent ->
  32. // determine parent for root web application context, if any.
  33. ApplicationContext parent = loadParentContext(servletContext);
  34. cwac.setParent(parent);
  35. }
  36. // 设置和刷新上下文
  37. configureAndRefreshWebApplicationContext(cwac, servletContext);
  38. }
  39. }
  40. // 将上下文绑定到 servletContext 的属性上
  41. servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
  42. // 获取当前线程的上下文类加载器
  43. ClassLoader ccl = Thread.currentThread().getContextClassLoader();
  44. if (ccl == ContextLoader.class.getClassLoader()) {
  45. currentContext = this.context;
  46. }
  47. else if (ccl != null) {
  48. // 如果有线程上下文类加载器,而且不是 ContextLoader 本身的类加载器,放入到 currentContextPerThread 中。这是一个 static 的域
  49. currentContextPerThread.put(ccl, this.context);
  50. }
  51. if (logger.isInfoEnabled()) {
  52. long elapsedTime = System.currentTimeMillis() - startTime;
  53. logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
  54. }
  55. return this.context;
  56. }
  57. catch (RuntimeException | Error ex) {
  58. // 发生异常, 把异常绑定到上下文对应的属性上,之后不会再进行初始化
  59. logger.error("Context initialization failed", ex);
  60. servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
  61. throw ex;
  62. }
  63. }

initWebApplicationContext() 方法调用了 createWebApplicationContext() 方法来创建上下文;调用了 configureAndRefreshWebApplicationContext() 来对实现了 ConfigurableWebApplicationContext 接口的上下文做初始化.


createWebApplicationContext() 会调用 determineContextClass() 来获取上下文类型的 Class 对象.

  1. protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
  2. // 获取上下文类型
  3. Class<?> contextClass = determineContextClass(sc);
  4. // 检查是否实现了 ConfigurableWebApplicationContext
  5. if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
  6. throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
  7. "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
  8. }
  9. // 实例化
  10. return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
  11. }
  12. protected Class<?> determineContextClass(ServletContext servletContext) {
  13. // 获取 serveltContext 的 'contextClass' 初始化参数。
  14. String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
  15. if (contextClassName != null) {
  16. // 指定过上下文类型,加载类
  17. try {
  18. return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
  19. }
  20. catch (ClassNotFoundException ex) {
  21. throw new ApplicationContextException(
  22. "Failed to load custom context class [" + contextClassName + "]", ex);
  23. }
  24. }
  25. else {
  26. // 去默认策略里获取默认的上下文类型名称
  27. contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
  28. try {
  29. // 加载类
  30. return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
  31. }
  32. catch (ClassNotFoundException ex) {
  33. throw new ApplicationContextException(
  34. "Failed to load default context class [" + contextClassName + "]", ex);
  35. }
  36. }
  37. }

configureAndRefreshWebApplicationContext() 方法

  1. protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
  2. // 用可以获取到的信息,获取一个更有意义的上下文
  3. if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
  4. // The application context id is still set to its original default value
  5. // -> assign a more useful id based on available information
  6. // 获取 ServletContext 的 'contextId' 初始化参数。
  7. String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
  8. if (idParam != null) {
  9. wac.setId(idParam);
  10. }
  11. else {
  12. // Generate default id...
  13. // 生成默认 id
  14. wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
  15. ObjectUtils.getDisplayString(sc.getContextPath()));
  16. }
  17. }
  18. // 设置 servletContext 属性
  19. wac.setServletContext(sc);
  20. // 设置配置文件路径
  21. String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
  22. if (configLocationParam != null) {
  23. wac.setConfigLocation(configLocationParam);
  24. }
  25. // The wac environment's #initPropertySources will be called in any case when the context
  26. // is refreshed; do it eagerly here to ensure servlet property sources are in place for
  27. // use in any post-processing or initialization that occurs below prior to #refresh
  28. // 初始化属性源, 确保 servlet 属性源到位并能够在任何 refresh 之前的后期处理和初始化中使用
  29. ConfigurableEnvironment env = wac.getEnvironment();
  30. if (env instanceof ConfigurableWebEnvironment) {
  31. ((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
  32. }
  33. // 在设置了配置文件之后上下文刷新之前,自定义上下文
  34. customizeContext(sc, wac);
  35. wac.refresh();
  36. }
  37. protected void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {
  38. // 根据 ServletContext 的 'contextInitializerClasses' 和 'globalInitializerClasses' 初始化参数 加载 ApplicationContextInitializer 的 class
  39. List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> initializerClasses =
  40. determineContextInitializerClasses(sc);
  41. for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerClass : initializerClasses) {
  42. // 获取范型参数类型
  43. Class<?> initializerContextClass =
  44. GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class);
  45. // 检查 Initializer 是否适用于当前上下文对象
  46. if (initializerContextClass != null && !initializerContextClass.isInstance(wac)) {
  47. throw new ApplicationContextException(String.format(
  48. "Could not apply context initializer [%s] since its generic parameter [%s] " +
  49. "is not assignable from the type of application context used by this " +
  50. "context loader: [%s]", initializerClass.getName(), initializerContextClass.getName(),
  51. wac.getClass().getName()));
  52. }
  53. // 创建 Initializer 实例,并添加到 contextInitializers
  54. this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
  55. }
  56. // 根据 org.springframework.core.Ordered 和 org.springframework.core.annotation.Order 排序,如果没有实现或注解,会被排到最后
  57. AnnotationAwareOrderComparator.sort(this.contextInitializers);
  58. // 执行每个 initializer 的 initialize() 方法
  59. for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
  60. initializer.initialize(wac);
  61. }
  62. }

销毁上下文

closeWebApplicationContext() 方法如下:

  1. public void closeWebApplicationContext(ServletContext servletContext) {
  2. servletContext.log("Closing Spring root WebApplicationContext");
  3. try {
  4. // 如果 context 是 ConfigurableWebApplicationContext 调用 close() 方法
  5. if (this.context instanceof ConfigurableWebApplicationContext) {
  6. ((ConfigurableWebApplicationContext) this.context).close();
  7. }
  8. }
  9. finally {
  10. ClassLoader ccl = Thread.currentThread().getContextClassLoader();
  11. if (ccl == ContextLoader.class.getClassLoader()) {
  12. currentContext = null;
  13. }
  14. else if (ccl != null) {
  15. currentContextPerThread.remove(ccl);
  16. }
  17. // 移除 servletContext 中的 context 属性
  18. servletContext.removeAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
  19. }
  20. }

Servlet 3.0+ 中初始化

Servlet 3.0+ 中可以通过 ServletContextaddlistener() 方法来添加监听器.因此可以先把 Spring 容器先创建好,再传给 ContextLoaderListener 的构造器.这里就不自己写例子了,选了单元测试中的 ContextLoaderTests.testContextLoaderListenerWithDefaultContext() 方法:

  1. public void testContextLoaderListenerWithDefaultContext() {
  2. MockServletContext sc = new MockServletContext("");
  3. sc.addInitParameter(ContextLoader.CONFIG_LOCATION_PARAM,
  4. "/org/springframework/web/context/WEB-INF/applicationContext.xml " +
  5. "/org/springframework/web/context/WEB-INF/context-addition.xml");
  6. ServletContextListener listener = new ContextLoaderListener();
  7. ServletContextEvent event = new ServletContextEvent(sc);
  8. listener.contextInitialized(event);
  9. String contextAttr = WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE;
  10. WebApplicationContext context = (WebApplicationContext) sc.getAttribute(contextAttr);
  11. assertTrue("Correct WebApplicationContext exposed in ServletContext", context instanceof XmlWebApplicationContext);
  12. assertTrue(WebApplicationContextUtils.getRequiredWebApplicationContext(sc) instanceof XmlWebApplicationContext);
  13. LifecycleBean lb = (LifecycleBean) context.getBean("lifecycle");
  14. assertTrue("Has father", context.containsBean("father"));
  15. assertTrue("Has rod", context.containsBean("rod"));
  16. assertTrue("Has kerry", context.containsBean("kerry"));
  17. assertTrue("Not destroyed", !lb.isDestroyed());
  18. assertFalse(context.containsBean("beans1.bean1"));
  19. assertFalse(context.containsBean("beans1.bean2"));
  20. listener.contextDestroyed(event);
  21. assertTrue("Destroyed", lb.isDestroyed());
  22. assertNull(sc.getAttribute(contextAttr));
  23. assertNull(WebApplicationContextUtils.getWebApplicationContext(sc));
  24. }

参考资料

芋道源码

Spring MVC源码——Root WebApplicationContext的更多相关文章

  1. Spring MVC源码——Servlet WebApplicationContext

    上一篇笔记(Spring MVC源码——Root WebApplicationContext)中记录了下 Root WebApplicationContext 的初始化代码.这一篇来看 Servlet ...

  2. 精尽Spring MVC源码分析 - WebApplicationContext 容器的初始化

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  3. 精尽Spring MVC源码分析 - 文章导读

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  4. 精尽Spring MVC源码分析 - 一个请求的旅行过程

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  5. 精尽Spring MVC源码分析 - 寻找遗失的 web.xml

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  6. 精尽Spring MVC源码分析 - HandlerMapping 组件(四)之 AbstractUrlHandlerMapping

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  7. spring mvc源码-》MultipartReques类-》主要是对文件上传进行的处理,在上传文件时,编码格式为enctype="multipart/form-data"格式,以二进制形式提交数据,提交方式为post方式。

    spring mvc源码->MultipartReques类-> MultipartReques类主要是对文件上传进行的处理,在上传文件时,编码格式为enctype="multi ...

  8. 精尽Spring MVC源码分析 - 调式环境搭建

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  9. 精尽Spring MVC源码分析 - MultipartResolver 组件

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

随机推荐

  1. Centos下Mysql密码忘记解决办法

    1.修改MySQL的登录设置: # vim /etc/my.cnf 在[mysqld]的段中加上一句:skip-grant-tables 例如: [mysqld] datadir=/var/lib/m ...

  2. 省队集训 Day4 a

    [题目大意] 求有多少区间只包含1个出现次数为1的数. $1\leq n \leq 5*10^5, 0 \leq a_i \leq 10^9$ [题解] 考虑枚举右端点,设这个数上一次出现位置为pre ...

  3. 「6月雅礼集训 2017 Day5」仰望星空

    [题目大意] 给你$n$个点,被一个半径为$R$的元圆划分成内(包含边界).外两个部分. 要连若干线,每个点只能连一条线,不存在重点和三点共线. 线只能连在内部点和外部点之间,线长度不超过$d$. 如 ...

  4. bzoj 2079: [Poi2010]Guilds——结论题

    Description Zy皇帝面临一个严峻的问题,两个互相抵触的贸易团体,YYD工会和FSR工会,他们在同一时间请求在王国各个城市开办自己的办事处.这里有n个城市,其中有一些以双向马路相连,这两个工 ...

  5. Java并发——关键字synchronized解析

    synchronized用法 在Java中,最简单粗暴的同步手段就是synchronized关键字,其同步的三种用法: ①.同步实例方法,锁是当前实例对象 ②.同步类方法,锁是当前类对象 ③.同步代码 ...

  6. Problem E. Matrix from Arrays(杭电2018年多校第四场+思维+打表找循环节)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6336 题目: 题意:给你一个l个元素的数组a,用题目中的程序构造一个新的矩阵,询问q次,问以(x1,y ...

  7. Js如何动态声明变量名

    做个笔记~ var a = 5; for (var i = 1; i <= a; i++) { eval("var a" + i + "=" + i); ...

  8. bzoj 1927 网络流

    首先我们可以知道这道题中每个点只能经过一次,那么我们引入附加源汇source,sink,那么我们可以将每个点拆成两个点,分别表示对于图中这个节点我们的进和出,那么我们可以连接(source,i,1,0 ...

  9. Python【模块】importlib,requests

    内容概要:      模仿django中间件的加载方式      importlib模块      requests模块  rsplit()   用实际使用的理解来解释两个模块 importlib模块 ...

  10. Django【进阶】数据库查询性能相关

    之前项目中没有考虑过数据库查询关于效率的问题,如果请求量大,数据庞大,不考虑性能的话肯定不行.     tips:如图之前我们遇到过,当添加一张表时,作为原来表的外键,要给个默认值,现在我们写null ...