SpringMVC源码解析系列(1)之Spring MVC的初始化流程

Spring MVC的初始化流程

重要的前置知识: ServletContext

1.创建时间

在WEB容器(tomcat)启动时,他会为每个WEB应用程序创建一个对应的ServletContext对象,他代表当前的web应用,又名application,对于一个web项目来说,只有全局唯一的一个ServletContext对象

2.获取方式:

①从当前Servlet对象中获得

ServletContext c1=this.getServletContext()

②从ServletConfig对象中获得

ServletContext c2=this.getServletConfig().getServletContext()

③从会话对象中获得

ServletContext c3=req.getSession().getServletContext()

④从HttpServletRequest请求对象中获得

ServletContext c4 = req.getServletContext()

3.用途:

①获取全局配置文件的参数(全局配置文件参数会保存在servletContext中)

  <!-- 全局配置参数 -->
<context-param>   
 <param-name>listener1</param-name>    
    <param-value>WEB-INF/classes/listener.txt</param-value>  
</context-param>
String path = c1.getInitParameter("listener1");

②获取web项目的绝对路径

String realPath = servletContext.getRealPath(path);

③用于获取WebRoot文件夹下的文件(.JSP,.PNG,.html,.doc,)资源

 c1.getResourceAsStream("index.jsp");

当一个 Web 应用部署到容器内时(例如 Tomcat),在 Web 应用开

始响应执行用户请求前,以下步骤会被依次执行:

  • 部署描述文件中(例如 Tomcat 的web.xml)由<listener>元素标记的事件监听器会被创建和初始化

  • 对于所有事件监听器,如果实现了ServletContextListener接口,将会执行其实现的contextInitialized()方法

  • 部署描述文件中由<filter>元素标记的过滤器会被创建和初始化,并调用其init()方法

  • 部署描述文件中由<servlet>元素标记的 Servlet 会根据<load-on-startup>的权值按顺序创建和初始化,并调用其init()方法

web应用部署初始化执行流程如下:(图片来自网图)

在 Tomcat 下 Web 应用的初始化流程是,先初始化listener接着初始化filter最后初始化servlet

接下来,以一个常见的tomcat的web.xml配置文件为例,进行Spring MVC的启动过程分析:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    
      <!--全局变量配置-->
  <context-param>
    <param-name>contextConfigLocation</param-name>
      <!-- 此处配置文件中的bean是放在根ioc容器中的 -->
    <param-value>classpath:applicationContext-*.xml</param-value>
  </context-param>
​
    <!-- 配置上下文加载监听器 -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
​
    <!-- 配置编码过滤器 -->
    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceResponseEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
​
    <!-- 配置前端控制器 -->
    <servlet>
        <servlet-name>DispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <!-- 此处配置文件中的bean是放在子ioc容器(当前servlet所关联的ioc容器)中的 -->
            <param-value>classpath:springMVC.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>DispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

1、因为我们定义了<context-param>标签,用于配置全局变量,所以tomcat会先去读取标签中的内容,放入之前创建好的servletcontext(application)中,作为Web应用的全局变量使用,然后再进行上述的初始化启动过程

Listener的初始化过程

2、tomcat会解析<listener>标签,创建相应的Listener对象,如果它实现了ServletContextListener接口,就会调用它的contextInitialized方法。

ServletContextListener接口

该接口只有两个方法contextInitializedcontextDestroyed,这里采用的是观察者模式,也称为为”订阅-发布“模式,实现了该接口的listener会向发布者进行订阅,当 Web 应用初始化或销毁时会分别调用上述两个方法。

public interface ServletContextListener extends EventListener {
    // servletContext初始化的时候被调用(tomcat启动)
    void contextInitialized(ServletContextEvent var1);
    // servletContext销毁的时候被调用(tomcat关闭)
    void contextDestroyed(ServletContextEvent var1);
}

ContextLoaderListener

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
    public ContextLoaderListener() {
    }
​
    public ContextLoaderListener(WebApplicationContext context) {
        super(context);
    }
    // 容器初始化时被调用
    public void contextInitialized(ServletContextEvent event) {
        // 初始化web的应用上下文
        this.initWebApplicationContext(event.getServletContext());
    }
​
    public void contextDestroyed(ServletContextEvent event) {
        this.closeWebApplicationContext(event.getServletContext());
        ContextCleanupListener.cleanupAttributes(event.getServletContext());
    }
}

ContextLoader#initWebApplicationContext

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
        /*
    首先通过WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
    (WebApplicationContext.class.getName() + ".ROOT")
    这个String类型的静态变量获取一个根IoC容器,根IoC容器作为全局变量
    存储在application对象中,如果存在则有且只能有一个
    如果在初始化根WebApplicationContext即根IoC容器时发现已经存在
    则直接抛出异常,因此web.xml中只允许存在一个ContextLoader类或其子类的对象
    */
    if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
        throw new IllegalStateException("Cannot initialize context because there is already a root application context present - check whether you have multiple ContextLoader* definitions in your web.xml!");
    } else {
        servletContext.log("Initializing Spring root WebApplicationContext");
        Log logger = LogFactory.getLog(ContextLoader.class);
        if (logger.isInfoEnabled()) {
            logger.info("Root WebApplicationContext: initialization started");
        }
​
        long startTime = System.currentTimeMillis();
​
        try {
            // 如果当前webapplicationcontext不存在就创建一个根webapplication
            if (this.context == null) {
                this.context = this.createWebApplicationContext(servletContext);
            }
​
            if (this.context instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)this.context;
                if (!cwac.isActive()) {
                    if (cwac.getParent() == null) {
             // 此处会默认返回null
                        ApplicationContext parent = this.loadParentContext(servletContext);
              // 设置当前根容器的父容器为null,表明当前容器为根容器
                        cwac.setParent(parent);
                    }
                // 配置并刷新根webApplicationContext,在这里会进行bean的创建和初始化
                    this.configureAndRefreshWebApplicationContext(cwac, servletContext);
                }
            }
 // 如果在进行容器刷新的时候没有抛出异常,会继续来到此处:将当前的根webApplicationcontext设置到servletContext中          
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
            ClassLoader ccl = Thread.currentThread().getContextClassLoader();
            if (ccl == ContextLoader.class.getClassLoader()) {
                currentContext = this.context;
            } else if (ccl != null) {
                currentContextPerThread.put(ccl, this.context);
            }
​
            if (logger.isInfoEnabled()) {
                long elapsedTime = System.currentTimeMillis() - startTime;
                logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
            }
            // 至此根webApplicationContext创建完成
            return this.context;
        } catch (Error | RuntimeException var8) {
            logger.error("Context initialization failed", var8);
            // 如果在刷新容器的过程中抛出异常(没有读取到我们的配置文件),会来到此处,将根webapplicationContext设置到servletContext中
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, var8);
            throw var8;
        }
    }
}

ContextLoader#createWebApplicationContext

下面是ApplicationContext的继承关系图:

protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
    // 决策webapplicationcontext的真实类型
    Class<?> contextClass = this.determineContextClass(sc);
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException("Custom context class [" + contextClass.getName() + "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
    } else {
        // 创建了一个ConfigurableWebApplicationContext类型的根web应用上下文
        return (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass);
    }
}

​protected Class<?> determineContextClass(ServletContext servletContext) {
    // 获取名称为contextClass的全局初始化参数,就是查看我们有没有自定义web应用上下文
        String contextClassName = servletContext.getInitParameter("contextClass");
        if (contextClassName != null) {
            try {
        // 如果我们自定义了应用上下文,就将加载对应的类,返回它的类对象
                return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
            } catch (ClassNotFoundException var4) {
                throw new ApplicationContextException("Failed to load custom context class [" + contextClassName + "]", var4);
            }
        } else {
            // 如果没有自己定义,就去默认的策略配置中获取,此时获取到的是类型是XMLWebApplicationContext,
            /**
             ClassPathResource resource = new ClassPathResource("ContextLoader.properties", ContextLoader.class);
            defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
            **/
            contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
​
            try {
                // 加载类,返回类对象(XMLWebApplicationContext)
                return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
            } catch (ClassNotFoundException var5) {
                throw new ApplicationContextException("Failed to load default context class [" + contextClassName + "]", var5);
            }
        }
    }

defaultStrategies的结构:

ContextLoader#configureAndRefreshWebApplicationContext

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
    String configLocationParam;
    if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
       // 给webapplicationcontext设置id,先去全局初始化参数中找,没有配置,就设置id为”类名:项目访问代表路径“,eg:org.springframework.web.context.WebApplicationContext:/springmvc_05_war_exploded
        configLocationParam = sc.getInitParameter("contextId");
        if (configLocationParam != null) {
            wac.setId(configLocationParam);
        } else {
            wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getContextPath()));
        }
    }
    // 向根web应用上下文中设置servletContext
    wac.setServletContext(sc);
    // 获取我们配置的全局初始化参数中的key=contextConfigLocation所对应的value
    // 也就是获取springmvc的配置文件的名称(路径)
    configLocationParam = sc.getInitParameter("contextConfigLocation");
    if (configLocationParam != null) {
        // 如果配置了,将其设置到webApplicationContext的配置文件路径数组中configLocations,如果没有配置会使用默认的配置文件路径/WEB-INF/applicationContext.xml
        wac.setConfigLocation(configLocationParam);
    }
​
    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        ((ConfigurableWebEnvironment)env).initPropertySources(sc, (ServletConfig)null);
    }
​
    this.customizeContext(sc, wac);
    // 刷新容器,调用Spring中的AbstractApplicationContext的refresh()方法(前面分析过注解版的,现在分析xml文件版的)
    // 1.创建bean工厂
    // 2.加载配置文件
    // 3.进行包扫描,将扫描到的组件注册到bean工厂中--->注册的是beanDefinition
    // 4.注册配置文件中的bean--->注册的是beanDefinition
    // 5.接下来就与注解配置一样了(调用bean工厂的后置处理器(扫描包、注册bean)、注册bean后置处理器、创建bean)
    // 但是,如果我们没有配置contextConfigLocation全局初始化参数,在默认路径下(/WEB-INF/applicationContext.xml)也没有相应的配置文件,创建完bean工厂后会抛出异常
    wac.refresh();
}

整个ContextLoaderListener类的启动过程到此就结束了,可以发现,创建ContextLoaderListener是比较核心的一个步骤,主要工作就是为了创建根IOC 容器并使用特定的key将其放入到application对象中,供整个 Web 应用使用,在ContextLoaderListener类中构造的根IOC 容器配置的 Bean 是全局共享的。

Filter的初始化

3.tomcat会解析<filter>标签,创建<filter-class>标签中指定的类对象,设置属性。

Servlet的初始化

Web 应用启动的最后一个步骤就是创建和初始化相关 Servlet,在开发中常用的 Servlet 就是DispatcherServlet前端控制器,前端控制器作为中央控制器是整个 Web 应用的核心,用于获取分发用户请求并返回响应

通过类图可以看出DispatcherServlet类的间接父类实现了Servlet接口,因此其本质上依旧是一个ServletDispatcherServlet类的设计很巧妙,上层父类不同程度的实现了相关接口的部分方法,并留出了相关方法用于子类覆盖,将不变的部分统一实现,将变化的部分预留方法用于子类实现

DispatcherServlet类的初始化过程将模板方法使用的淋漓尽致,其父类完成不同的统一的工作,并预留出相关方法用于子类覆盖去完成不同的可变工作。

DispatcherServelt类的本质是Servlet,通过文章开始的讲解可知,在 Web 应用部署到容器后进行Servlet初始化时会调用相关的init(ServletConfig)方法,因此,DispatchServlet类的初始化过程也由该方法开始。先看一下整体的调用流程图:

主要的逻辑实现从FrameworkServletinitWebApplicationContext()方法开始

FrameworkServlet#initWebApplicationContext()

protected WebApplicationContext initWebApplicationContext() {
    // 从servletContext中获取根webApplicationContext,由于我们在初始化监听器的时候创建了根webApplicationContext,所以可以获取到,如果我们没有配置监听器,就不会创建根WebApplicationContext
    WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
    WebApplicationContext wac = null;
    // 当前servlet的webApplicationContext=null,此时我们还没有给Servlet的webApplicationContext属性赋值,如果配置了监听器,创建的根webApplicationContext也只在servletContext中,被全局共享
    if (this.webApplicationContext != null) {
        wac = this.webApplicationContext;
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)wac;
            if (!cwac.isActive()) {
                if (cwac.getParent() == null) {
                    cwac.setParent(rootContext);
                }
​
                this.configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
// 此时,因为我们还没有给当前servlet设置属性,所偶一也找不到WebApplicationContext
    if (wac == null) {
        wac = this.findWebApplicationContext();
    }
​
    if (wac == null) {
        // 创建webApplicationContext,此处的创建逻辑和之前分析过的ContextLoader中的创建逻辑大体相同
        /**
        1.获取webApplicationContext类型对象
        2.创建相对应的实例对象
        3.将传进去的rootContext设置为它的父容器
        4.设置配置文件的路径
        5.配置并刷新容器(读取springMVC.xml配置文件中的bean将其注册到工厂中,创建bean实例...)   
        **/
        wac = this.createWebApplicationContext(rootContext);
    }
​
    if (!this.refreshEventReceived) {
        synchronized(this.onRefreshMonitor) {
            this.onRefresh(wac);
        }
    }
​
    if (this.publishContext) {
        String attrName = this.getServletContextAttributeName();
        this.getServletContext().setAttribute(attrName, wac);
    }
    // 至此
    return wac;
}

从上面createWebApplicationContext(rootContext)中可以看出,我们给当前的servlet创建了一个IOC容器,并且给它设置父容器,那为什么明明都有一个父容器了,还要创建子容器呢?

父子容器类似于类的继承关系,子类可以访问父类中的成员变量,而父类不可访问子类的成员变量,同样的,子容器可以访问父容器中定义的 Bean,但父容器无法访问子容器定义的 Bean

根 IoC 容器做为全局共享的 IoC 容器放入 Web 应用需要共享的 Bean,而子 IoC 容器根据需求的不同,放入不同的 Bean,这样能够做到隔离,保证系统的安全性。

当 IoC 子容器构造完成后调用了onRefresh()方法,该方法的调用与initServletBean()方法的调用相同,由父类调用但具体实现由子类覆盖,调用onRefresh()方法时将前文创建的 IoC 子容器作为参数传入,查看DispatcherServletBean类的onRefresh()方法源码如下:

protected void onRefresh(ApplicationContext context) {
    this.initStrategies(context);
}
​
// 以下的初始化操作,就是从ioc容器中获取创建好的bean,然后将其设置为DispatcherServlet的属性值
protected void initStrategies(ApplicationContext context) {
    this.initMultipartResolver(context);
    this.initLocaleResolver(context);
    this.initThemeResolver(context);
    // 初始化处理器映射器,会先从IOC容器中获取,获取不到去找默认的
    this.initHandlerMappings(context);
    // 初始化处理器适配器,会先从IOC容器中获取,获取不到去找默认的
    this.initHandlerAdapters(context);
    this.initHandlerExceptionResolvers(context);
    this.initRequestToViewNameTranslator(context);
    this.initViewResolvers(context);
    this.initFlashMapManager(context);
}

总结:Spring MVC的初始化流程

1.tomcat容器启动时,会去读取web项目下的web.xml文件

2.解析<context-param>标签中的参数

3.创建一个全局共享对象ServletContext,也叫application,将全局共享的参数放入ServletContext中

4.解析<Listener>标签,实例化Listener监听器对象,一般会使用ContextLoaderListener类,如果使用了ContextLoaderListener类,就会创建一个WebApplicationContext对象,这个对象就是根IOC容器,它会去读取<context-param>标签中设置的key为contextConfigLocationd所对应的value,作为要加载的配置文件,创建bean工厂,读取配置文件,创建bean实例,将根IOC容器放入ServletContext中,但是这个根IOC容器只能访问到自己容器中的bean实例,无法访问其子容器中的bean实例

5.解析<filter>标签,创建filter对象

6.解析<Servlet>标签,创建servlet对象,这个servlet通常指前端控制器DispatcherServlet,它间接实现了Servlet接口,所以会调用Servlet接口的init(ServletConfig config)方法,但是DispatcherServlet没有重写此方法,所以会调用它的父类GenericServlet重写的Servlet接口的init()方法,在重写父类的init()方法中又会调用自己的init()方法,但是自己的init()方法是个空方法,所以最终会调用它子类HttpServletBean重写它的init()方法,这样在父类中调用子类重写父类的空方法,最终会调用到FrameWorkServlet的initWenApplicationContext的方法,在此方法中创建一个当前Servlet对相应的子IOC容器,将全局IOC容器设置为它的父容器,读取<init-param>配置的xml文件并加载相关 Bean。

7.调用DispatcherServlet的onRefresh()方法,刷新容器,初始化属性,就是获取容器中的bean设置为自己的属性。

如有问题欢迎指正.....

参考文章:详述 Spring MVC 启动流程及相关源码分析_CG国斌的博客-CSDN博客_springmvc启动流程

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值