SpringMVC源码:DispatcherServlet的初始化(二)

上一阶段,分析了 HttpServletBean 在初始化阶段中做的事情,它最后留了一个 initServletBean() 模板方法来交给子类 FrameworkServlet 继续初始化。

所以进入源码:

FrameworkServlet # initServletBean()

@Override
protected final void initServletBean() throws ServletException {
    //...

    try {
        this.webApplicationContext = initWebApplicationContext();
        initFrameworkServlet();
    }
    catch (...) {
        //...
    }

	//...
}

核心代码只有两句:

初始化 WebApplicationContext :this.webApplicationContext = initWebApplicationContext(); (创建并初始化 SpringWeb 容器)

初始化 initFrameworkServlet :initFrameworkServlet() 模板方法,留给子类实现(尚未发现有子类实现此方法,所以没有做事情,不用分析)

创建并初始化 SpringWeb容器:initWebApplicationContext()

先看注释:

protected WebApplicationContext initWebApplicationContext() {
    
    /*
    	1、获取Spring根容器 rootContext
    */
    WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
   
    WebApplicationContext wac = null;

	/*	
		如果在 FrameworkServlet 的构造方法中设置了webApplicationContext,则会使用这个webApplicationContext作为SpringWeb容器(在JavaConfig的配置情况下,可能会进入这个if)
		(注:在此方法执行前,this.webApplicationContext只会在构造方法中被赋予值(且没有set方法,无法通过web.xml配置被赋值))
    */
    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);
                }
                configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    
    
    /*	
  		2、查看是否在 ServletContext 中配置了 SpringWeb 容器(通常不会进入这个if)
    */
    if (wac == null) {
        wac = findWebApplicationContext();
    }
    
    
    /*
    	3、如果仍未找到,则直接创建一个 SpringWeb 容器 (一般会进入这个if)
    */
    if (wac == null) {
        wac = createWebApplicationContext(rootContext);
    }

    /*
    	4、调用onRefresh方法,具体实现在DispatcherServlet中(因为监听机制,此方法一定会被调用,且只会被调用一次)
    	   注意:这里刷新的是子容器,即 SpringWeb 容器
    */
    if (!this.refreshEventReceived) {
        onRefresh(wac);
    }

    /*
    	根据publishContext标志位(默认为true),决定是否将SpringWeb容器,添加到ServletContext中
    */
    if (this.publishContext) {
        String attrName = getServletContextAttributeName();
        getServletContext().setAttribute(attrName, wac);
    }

    return wac;
}

其中比较重要的代码分为 4 个部分,下面来分部分来讲解,每个部分都做了什么。

1、获取Spring根容器rootContext

此部分,是尝试从 ServletContext 中获取 Spring 根容器,方便后续将其设置为 SpringWeb 容器的父容器(看上一篇文章)

默认情况下,会将Spring 根容器设置在 ServletContext 的属性中(Attribute)(看上一篇文章),默认根容器的 key 为 org.springframework.web.context.WebApplicationContext.ROOT(可以查看WebApplicationContext类源码)

获取时,调用 ServletContext.getAttribute(“org.springframework.web.context.WebApplicationContext.ROOT”) ,就可以获取根容器了

WebApplicationContextUtils # getWebApplicationContext

@Nullable
public static WebApplicationContext getWebApplicationContext(ServletContext sc) {
    return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); //根据 org.springframework.web.context.WebApplicationContext.ROOT 查找
}

@Nullable
public static WebApplicationContext getWebApplicationContext(ServletContext sc, String attrName) {
    Assert.notNull(sc, "ServletContext must not be null");
    // 获取 ServletContext 中 key 为 org.springframework.web.context.WebApplicationContext.ROOT 的内容
    Object attr = sc.getAttribute(attrName);
    // 如果没有在web.xml中配置监听器 ContextLoaderListener,则根容器为null
    if (attr == null) {
        return null;
    }
    //...
    return (WebApplicationContext) attr;
}

可以看到,如果没有在web.xml中配置监听器 ContextLoaderListener ,那么父容器就设置为null

2、从ServletContext中决定 SpringWeb 容器

尝试从 ServletContext 找 SpringWeb容器

@Nullable
protected WebApplicationContext findWebApplicationContext() {
	//得到 contextAttribute 属性的值,作为查找依据 key  (这个 contextAttribute 属性的值在何时被注入的?  看文章:SpringMVC源码:DispatcherServlet的初始化(一))
    String attrName = getContextAttribute();
    /*
		若key为null,说明在 DispatcherServlet 中没有配置 contextAttribute 参数,则表明用户没打算从 ServletContext 中来决定SpringWeb容器。 直接返回null
    */
    if (attrName == null) {
        return null;
    }
    /*
    	若key不为null,说明在 DispatcherServlet 中配置了 contextAttribute 参数,则表明用户想从 ServletContext 中来决定SpringWeb容器。则根据此 key 去 ServletContext 中找,
    	找到,就用它来作为SpringWeb容器。没找到,则抛异常
    */
    WebApplicationContext wac = WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
    if (wac == null) {
        throw new IllegalStateException("No WebApplicationContext found: initializer not registered?");
    }
    return wac;
}

此方法的作用,适用于以下情况(了解即可):

比如,当前 ServletContext 中已经存在一个叫 “haha” 的 WebApplicationContext 了

//之前某些地方执行了以下代码:
servletContext.setAttribute("haha",new MyWebApplicationContext)

如果我们想使用这个 MyWebApplicationContext 来作为我们的SpringWeb容器,

那么就可以配置DispatcherServlet的 contextAttribute 属性值为 “haha” (一般我们不会在DispatcherServlet中配置contextAttribute属性,所以这种情况不常见)

<servlet>
    <servlet-name>DispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- 配置了contextAttribute参数,值为haha-->
    <init-param>
        <param-name>contextAttribute</param-name>
        <param-value>haha</param-value>
    </init-param>
</servlet>
3、创建SpringWeb容器,并将rootContext设置为父容器
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
    
    // 获取 contextClass 属性的值,默认为XmlWebApplicationContext.class   (contextClass属性有set方法,所以这里可以通过web.xml自定义contextClass的值)
    Class<?> contextClass = getContextClass();
 	//...
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException("..."); //若自定义了SpringWeb容器,那么此容器类型必须为ConfigurableWebApplicationContext类型,否则将抛异常
    }
    
    // 根据 contextClass 利用BeanUtils实例化SpringWeb容器对象,默认创建的SpringWeb容器为 XmlWebApplicationContext
    ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass); 

    wac.setEnvironment(getEnvironment());
    
    // 设置SpringWeb容器的"父容器"(为rootContext)
    wac.setParent(parent);
    
    // 得到web.xml中DispatcherServlet配置的 configLocation 值,设置给SpringWeb容器的 configLocation 属性  (方便后续XmlWebApplicationContext容器刷新阶段,载入BeanDefinition)
    // 注:如果没有配置,那么XmlWebApplicationContext默认会从"/WEB-INF/[Namespace]-servlet.xml"处加载,若Namespace属性也为空,则默认从"/WEB-INF/applicationContext.xml"处加载
    //(看源码:XmlWebApplicationContext#loadBeanDefinitions方法中的getConfigLocations()和XmlWebApplicationContext#getDefaultConfigLocations())
    String configLocation = getContextConfigLocation();
    if (configLocation != null) {
        wac.setConfigLocation(configLocation);
    }
    
    // 配置和刷新SpringWeb容器(后面解析)
    configureAndRefreshWebApplicationContext(wac);

    return wac;
}
  • 知识点1:可以看到,最终是根据 contextClass 属性的值,来确定SpringWeb容器类型的。默认创建的SpringWeb容器类型为XmlWebApplicationContext

    这里显然也可以通过配置DispatcherServlet的 contextClass 属性值,来指定使用我们自定义的SpringWeb容器。如:(通常不会自定义,了解)

    <servlet>
        <servlet-name>DispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name> contextClass </param-name>
            <param-value>com.itcode.springdemo.MyWebApplicationContext</param-value>
        </init-param>
    </servlet>
    
  • 知识点2:配置和刷新容器

    protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
        // 配置SpringWeb容器的id
        if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
            // 若在配置DispatcherServlet时,param name配置了"contextId"属性,那么就使用其值,作为容器id。  若没有配置,则默认生成id...
            if (this.contextId != null) {
                wac.setId(this.contextId);
            }
            else {
                wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                          ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
            }
        }
    	// 配置SpringWeb容器的 ServletContext 属性
        wac.setServletContext(getServletContext());
        // 配置SpringWeb容器的 ServletConfig 属性
        wac.setServletConfig(getServletConfig());
        // 配置SpringWeb容器的 Namespace 属性,默认为"[ServletName]-servlet"
        //(若在web.xml中没有配置configLocation,那么XmlWebApplicationContext加载BeanDefinition时,默认的configLocation就是 "/WEB-INF/[Namespace]-servlet.xml")
        //(此属性有set方法,所以可以通过web.xml配置DispatcherServlet时自定义)
        wac.setNamespace(getNamespace());
        /*  
        	给SpringWeb容器配置了一个监听器 SourceFilteringListener(待...)
        	SourceFilteringListener可以根据输入的参数进行选择,所以实际监听的是ContextRefreshListener所监听的事件。
        	ContextRefreshListener是FrameworkServlet的内部类,监听ContextRefreshedEvent事件,
        	当接收到消息时调用FrameworkServlet的onApplicationEvent方法,在onApplicationEvent中会调用一次onRefresh方法,并将refreshEventReceived标志设置为true,表示已经onRefresh过
        	保证了 onRefresh 方法只会被执行一次
        */
        wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));
        //(待...)
        ConfigurableEnvironment env = wac.getEnvironment();
        if (env instanceof ConfigurableWebEnvironment) {
            ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
        }    
    	// 留给子类实现(尚未有子类实现)
        postProcessWebApplicationContext(wac);
        //(待...)
        applyInitializers(wac);
        
        // 刷新SpringWeb容器
        wac.refresh();
    }
    
此部分总结:

此阶段完成了:
①、SpringWeb容器的创建,默认创建的SpringWeb容器为 XmlWebApplicationContext ,并为此容器设置了一些属性值
②、设置了父容器为rootContext
③、容器的刷新

4、调用 onRefresh 方法

刷新 SpringWeb 容器(这里的刷新,是向容器中设置了一些组件,并不是平常我们说的refresh)

此处逻辑交由 DispatcherServlet 完成(看下一篇)

【总结】

通过对 initWebApplicationContext 源码的分析,我们知道了:

0、SpringWeb容器的创建,发生在 DispatcherServlet 的初始化过程中。并且可以在web.xml中配置 DispatcherServlet 时,间接地配置 SpringWeb 容器(配置初始化参数< init-param>)

1、SpringWeb容器的来源有三处:

  • 通过new FrameworkServlet()构造方法传入(在JavaConfig的配置情况下会用到)
  • 在 ServletContext 中获取(不常用)
  • 直接创建一个SpringWeb容器(一般都会走到这里)

2、在创建SpringWeb容器的过程中

  • 默认创建的SpringWeb容器对象是 XmlWebApplicationContext。(也可以通过web.xml中DispatcherServlet配置 contextClass 属性,来指定自定义的SpringWeb容器(不常用))
  • 为SpringWeb容器指定了父容器(父容器保存在ServletContext 中, key 为 org.springframework.web.context.WebApplicationContext.ROOT),如果没有在web.xml中配置监听器 ContextLoaderListener ,那么父容器就为null
  • 完成了容器的刷新

3、可以总结下web.xml中关于DispatcherServlet 可配置的属性(理论上,在DispatcherServlet 中的属性,只要有对应的set方法,就都可以通过web.xml进行配置。 看笔记:SpringMVC源码:DispatcherServlet的初始化(一))

<servlet>
    <servlet-name>DispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    
    <!-- 用于决定SpringWeb容器的-->
    <!-- contextAttribute属性:尝试从ServletContext中的haha处决定SpringWeb容器-->
    <init-param>
        <param-name>contextAttribute</param-name>
        <param-value>haha</param-value>
    </init-param>
    <!-- contextClass属性:自定义决定SpringWeb容器-->
    <init-param>
        <param-name>contextAttribute</param-name>
        <param-value>com.itcode.springdemo.MyWebApplicationContext</param-value>
    </init-param>
    
    <!-- 给SpringWeb容器中的属性赋值的-->
    <!-- contextConfigLocation属性:设置SpringWeb容器的BeanDefinition加载路径-->       <!--只有此处是常用的-->
    <init-param>
        <param-name>contextConfigLocation</param-name>                     
        <param-value>classpath:spring.xml</param-value>
    </init-param>  
    <!-- contextId属性:设置SpringWeb容器的ID-->
    <init-param>
        <param-name>contextId</param-name>
        <param-value>...</param-value>
    </init-param>
	<!-- namespace属性:设置SpringWeb容器的namespace-->
    <init-param>
        <param-name>namespace</param-name>
        <param-value>...</param-value>
    </init-param>
    
    <!-- publishContext属性:用于决定是否将SpringWeb容器放到ServletContext域中-->
    <init-param>
        <param-name>publishContext</param-name>
        <param-value>false</param-value>
    </init-param>
    
</servlet>

所以重点来说, FrameworkServlet 在初始化阶段做的事情就是:
根据web.xml中配置的 DispatcherServlet 信息,创建了一个SpringWeb容器(并为其设置了一些属性:如父容器、contextConfigLocation、容器ID…)。此时,还需要向容器中添加一些重要组件(如:处理器映射器、处理器适配器、视图解析器…),这部分的工作在 onRefresh 中,交给了子类 DispatcherServlet 完成(看下一篇)

参考书籍:《看透 springMVC 源代码分析与实践》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值