SpringMVC源码之解读DispatcherServlet初始化流程

原帖地址:http://blog.csdn.net/roderick2015/article/details/52846240,转载请注明。

我们先回顾下在Web应用中构建SpringMVC框架的大体流程:
1.使用Maven等工具引入依赖(依赖的jar包还不少)。
2.在web.xml中配置DispatcherServlet。
3.创建spring-mvc.xml文件,在这里写上我们对SpringMVC的定制化配置(或者使用注解类的方式)。
4.编写Controller类,使用注解的方式指定与URL对应的处理方法(当然你可能还得准备JSP之类的页面)。
5.发布到Web容器(本帖使用的是Tomcat)并启动。

整个流程并不复杂,但每个步骤所涉及的东西比较多,一不小心配错可就糟糕了。但这参与感十足的配置方式除了它的灵活性外,也能让我们更加清楚框架在为我们服务之前,到底做了哪些事情,接下来我们就到源码中去看看SpringMVC的核心入口类DispatcherServlet在Web容器启动后做了哪些准备。

从DispatcherServlet的名字可以知道它是个Servlet,我们看下它在web.xml中的具体配置,代码如下所示。

    <servlet>
        //servlet的名字
        <servlet-name>dispatcher</servlet-name>
        //指定具体的类
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        //以类似HashMap中key和value的方式给servlet配置一些参数信息
        <init-param>
            //指定context配置文件的路径
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:/spring-mvc.xml</param-value>
        </init-param>
        <init-param>
            //不要在意这个参数,帖子后面会提到的
            <param-name>publishContext</param-name>
            <param-value>true</param-value>
        </init-param>
        //大于或等于1表示web容器在启动的时候就会调用,值越大优先级越高
        //0或-1表示在第一次被使用的时候才会调用
        <load-on-startup>1</load-on-startup>
    </servlet>

    //url路径映射
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

接着看它的继承体系,如下图所示。

这里写图片描述

图中可以很清晰地看到DispatcherServlet的继承主线是比较单一的:DispatcherServlet —> FrameworkServlet —> HttpServletBean,我们主要分析这三个类,后面的HttpServlet到Servlet接口属于Java的Servlet原生代码,其中GenericServlet也参与了初始化,整个初始化流程如下图所示。
这里写图片描述

声明:本帖所示的代码一律省略源码中的log日志输出,以精简阅读。

在 Tomcat启动时会加载应用的Web.xml文件,这时就会扫描到DispatchServlet,然后调用init方法进行初始化(就是Servlet接口的init方法嘛),该方法的具体实现在DispatchServlet的父类GenericServlet中,也是DispatchServlet初始化的入口

GenericServlet

GenericServlet初始化的时候就干了两件事
1.接收Tomcat传过来的ServletConfig。
2.调用空方法init,供子类覆写实现后续的初始化。

    public void init(ServletConfig config) throws ServletException {
        this.config = config;
        this.init();
    }

    public void init() throws ServletException {

    }

这里看下config里面到底装了什么东西,如下图所示。
这里写图片描述

StandardWrapper和ApplicationContextFacade都是Tomcat中的对象,本帖不作详细说明,只需要知道ApplicationContextFacade里放的是整个Web应用的上下文信息,StandardWrapper则是针对DispatchServlet的,它两的作用域不同。如下图所示,可以看到我们在Web.xml文件中配置的相关数据。
这里写图片描述

HttpServletBean

接着HttpServletBean作为子类覆写了GenericServlet的init方法,代码如下所示。

    @Override
    public final void init() throws ServletException {
        try {
            PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
            ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
            bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
            initBeanWrapper(bw);
            bw.setPropertyValues(pvs, true);
        }
        catch (BeansException ex) {
            throw ex;
        }

        //进一步初始化,与GenericServlet的套路一样,供子类(FrameworkServlet)覆写
        initServletBean();
    }

总的来说,这里只干了三件事
1.把DispatchServlet封装成BeanWrapper。
2.给BeanWrapper设置属性。
3.调用空方法initServletBean,让子类接着干。

好,我们再逐行看下init方法中的代码。

PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);

ServletConfigPropertyValues的作用是从GenericServlet的config中取出我们在Web.xml中配置的信息, 生成对应的PropertyValue对象,由于我们可以配置多条参数信息,所以最后放入List< PropertyValue >集合中管理,这个集合由MutablePropertyValues类维护。ServletConfigPropertyValues的实现方式是开闭原则的体现,它作为HttpServletBean的内部类继承MutablePropertyValues,再通过扩展的方式写入自身的处理逻辑,最后看下继承关系就很明显了,如下图所示。
这里写图片描述

BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);

这句代码就是把this(DispatchServlet)封装成BeanWrapperImpl对象,然后由它的父类接口BeanWrapper接收引用,使用了简单工厂模式。

ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));

给BeanWrapper注册自定义的PropertyEditor,其中ResourceEditor用来把加载的文件或者URL等资源转成统一的Resource对象,为什么需要传入ResourceLoader参数呢,因为它是负责加载资源的啊,而ResourceEditor是定义如何转换的,就好比ResourceLoader负责买菜洗好切好,ResourceEditor提供菜谱,那谁负责炒菜呢?答案是TypeConverter,这个接口我会在后面提到。那ResourceLoader还在哪里用到过呢?我们看看Spring中的下面这句代码,你一定不会陌生。

ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml"); 

为什么ApplicationContext作为容器但也可以加载路径资源呢?因为他的两大父类接口,第一个是BeanFactory,第二个就是ResourceLoader了,这样是不是就好理解了呢。这里还使用到了getEnvironment方法,那Environment里又有什么?

别着急,其实这些大部分都是SpringFramework的知识,我会在文章末尾展开说明。

FrameworkServlet

FrameworkServlet手握接力棒,继续初始化之旅。

    @Override
    protected final void initServletBean() throws ServletException {
        try {
            this.webApplicationContext = initWebApplicationContext();
            initFrameworkServlet();
        }
        catch (ServletException ex) {
            throw ex;
        }
        catch (RuntimeException ex) {
            throw ex;
        }
    }

这个方法实际干活的只有两行代码,其余的都是log输出和异常处理,而initFrameworkServlet是空方法,目前并没有被使用,所以我们只剩一句代码需要分析了,initWebApplicationContext方法的作用就是创建并初始化WebApplicationContext容器,具体代码实现如下所示。

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

        //如果是调用ServletContext对象的addServlet方法添加的Servlet,
        //然后在构造函数中传入webApplicationContext,则采用下面的方式配置。
        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);
                }
            }
        }

        //与获取根容器的原理类似,只是寻找的key由Web.xml中Servlet <init-param>标签的
        //contextAttribute参数指定。
        if (wac == null) {
            wac = findWebApplicationContext();
        }

        //最后还没找到,就自己创建一个,正常配置的话就是采用这个方法。
        if (wac == null) {
            wac = createWebApplicationContext(rootContext);
        }

        if (!this.refreshEventReceived) {
            onRefresh(wac);
        }

        //本帖的Web.xml中是配置了该参数的,设置为true时,会把刚创建的WebApplicationContext
        //对象作为Attribute属性放入ServletContext中,key值是
        //FrameworkServlet.SERVLET_CONTEXT_PREFIX + 当前Servlet的名称,默认值是false。
        if (this.publishContext) {
            String attrName = getServletContextAttributeName();
            getServletContext().setAttribute(attrName, wac);
        }

        return wac;
    }

这里说明一下是如何获取根容器的,还记得我们在web.xml中给Spring配置的那个监听器吗,如下所示。

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

它会在Tomcat启动的时候触发监听创建WebApplicationContext容器(为什么是WebApplicationContext?因为我们是Web应用啊)然后解析我们的配置文件(比如spring.xml)装配容器,最后以org.springframework.web.context.WebApplicationContext.ROOT作为key放入ServletContext中,这里的ServletContext其实是GenericServlet中config的ApplicationContextFacade对象,如果没有配置Spring则没有父容器,SpringMVC会负责加载所有的bean来提供IOC功能,但是配了Spring就会出现双容器的情况,这时候bean该由谁加载,就得注意区分了,一般情况下SpringMVC容器管理Controller组件,其它的交给Spring容器管理。

接下来我们看下createWebApplicationContext方法,是怎么创建Web容器的,代码如下所示。

    protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) {
        //这里决定了要创建的容器类型,默认是XmlWebApplicationContext,
        //可以在<init-param>中配置contextClass参数指定类型。
        Class<?> contextClass = getContextClass();
        //这个类型必须属于ConfigurableWebApplicationContext,否则抛出异常。
        if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
            throw new ApplicationContextException(
                    "Fatal initialization error in servlet with name '" + getServletName() +
                    "': custom WebApplicationContext class [" + contextClass.getName() +
                    "] is not of type ConfigurableWebApplicationContext");
        }

        //创建对象,然后给context配置各种属性,
        //这也是为啥我们需要ConfigurableWebApplicationContext的类型
        ConfigurableWebApplicationContext wac =
                (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

        //放入HttpServletBean里创建的环境对象
        wac.setEnvironment(getEnvironment());
        //设置父容器
        wac.setParent(parent);
        //将我们指定的context配置文件的路径放进去,如果没有指定默认传入
        //WEB-INFO/[Servlet的名字]-Servlet.xml
        wac.setConfigLocation(getContextConfigLocation());
        //进一步配置其它属性
        configureAndRefreshWebApplicationContext(wac);

        return wac;
    }

再看configureAndRefreshWebApplicationContext方法,代码如下所示。

    protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
        if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
            if (this.contextId != null) {
                wac.setId(this.contextId);
            }
            else {
            //就是把它的id由org.springframework.web.context.support.XmlWebApplicationContext@c335c0c
            //改成了基于Servlet名字的org.springframework.web.context.WebApplicationContext:/dispatcher,
            //官方文档说是替换成一个更有意义的ID
                wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                        ObjectUtils.getDisplayString(getServletContext().getContextPath()) + "/" + getServletName());
            }
        }

        //配置ServletContext,放进去的是ApplicationContextFacade对象
        wac.setServletContext(getServletContext());
        //配置ServletConfig,StandardWrapperFacade对象
        wac.setServletConfig(getServletConfig());
        //配置命名空间,默认是[Servlet的名字]-servlet
        wac.setNamespace(getNamespace());
        //添加监听器,监听ContextRefreshedEvent事件,该事件会在WebApplicationContext初始化完毕或者主动调用
        //refresh()方法时触发,比如Spring容器加载完context配置文件后就会触发,所以会触发多次,触发后调用
        //onApplicationEvent方法,这个后面再讲。
        wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

        //补全之前创建的StandardServletEnvironment对象
        ConfigurableEnvironment env = wac.getEnvironment();
        if (env instanceof ConfigurableWebEnvironment) {
            ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
        }

        //空方法,也是供子类扩展的,目前还没有使用
        postProcessWebApplicationContext(wac);
        //主要是为了在调用refresh方法之前做一些准备工作
        applyInitializers(wac);
        //主动调用refresh方法,触发上面刚添加的监听器
        wac.refresh();
    }

看完代码后,需要弄清两个问题
1.ConfigurableWebEnvironment的initPropertySources方法对StandardServletEnvironment对象做了啥?
2.wac.refresh()是怎么触发监听事件的?

先看第一个问题,这里其实是填补了之前在HttpServletBean中留下的坑(详见文章末尾问题3),这里调用了StandardServletEnvironment的第二个方法填补了占位符的位置,把servletContext和servletConfig加进去了,而且是赶在refresh方法之前加进去的,便于后面调用,结果如下图所示。
这里写图片描述
这样我们就可以在Environment里取到这两个对象了,这个在开发中是非常有用的,比如我们可以把数据作为attribute放入servletContext中实现共享。

再看第二个问题,其实它调用的是父类AbstractApplicationContext中的refresh()方法,此时SpringMVC容器就会开始加载spring-mvc.xml文件,这个过程与Spring容器的配置类似,装配bean、注册listener等等,不过这属于容器的初始化了,过程更为复杂。配置完毕后发布ContextRefreshedEvent事件,触发调用onApplicationEvent方法,代码如下所示。

    public void onApplicationEvent(ContextRefreshedEvent event) {
        //设为true,其他地方则根据该标志位不再调用refresh方法,保证下面的onRefresh方法只会被调用一次
        this.refreshEventReceived = true;
        //这个ApplicationContext是事件的发起者,我们刚刚创建出来的WebApplicationContext
        onRefresh(event.getApplicationContext());
    }
    //给子类覆写扩展的,这个正是DispatcherServlet类初始化的入口
    protected void onRefresh(ApplicationContext context) {

    }

DispatcherServlet

我们的主角DispatcherServlet类终于要出场了,它的onRefresh方法代码如下所示。

    @Override
    protected void onRefresh(ApplicationContext context) {
        //将初始化策略和onRefresh方法分开,让子类灵活扩展
        initStrategies(context);
    }

    //将整个处理流程分成九个部分,处理非常灵活,我们可以在配置文件中对这些组件进行定制,
    //这也正是SpringMVC最为核心的部分
    protected void initStrategies(ApplicationContext context) {
        //处理文件上传请求
        initMultipartResolver(context);
        //国际化处理
        initLocaleResolver(context);
        //解析主题配置
        initThemeResolver(context);
        //把request映射到具体的处理器上(负责找handler)
        initHandlerMappings(context);
        //调用处理器来处理(负责让handler干活)
        initHandlerAdapters(context);
        //解析过程出了问题就交给我
        initHandlerExceptionResolvers(context);
        //从request中获取viewName
        initRequestToViewNameTranslator(context);
        //将我们返回的视图名字符串(如hello.jsp)解析为具体的view对象
        initViewResolvers(context);
        //管理FlashMap,主要用于redirect
        initFlashMapManager(context);
    }

代码注释部分已经简单介绍了各组件的功能,而它们初始化的套路是很类似的,因此我们这里看下比较熟悉的ViewResolver组件是如何初始化的就行了。

    private void initViewResolvers(ApplicationContext context) {
        this.viewResolvers = null;

        if (this.detectAllViewResolvers) {
            //到容器(包括父容器)中查找ViewResolver.class类型的bean,这也是为啥我们在配置文件里
            //只需要指定类而不用命名,详情见后面的讲解。
            Map<String, ViewResolver> matchingBeans =
                    BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false);
            if (!matchingBeans.isEmpty()) {
                //因为可以匹配到多个,所以放入list中管理
                this.viewResolvers = new ArrayList<ViewResolver>(matchingBeans.values());
                //排序,决定优先使用哪个来解析视图
                OrderComparator.sort(this.viewResolvers);
            }
        }
        else {
            try {
                //只以viewResolver名字去查找
                ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class);
                this.viewResolvers = Collections.singletonList(vr);
            }
            catch (NoSuchBeanDefinitionException ex) {
                // Ignore, we'll add a default ViewResolver later.
            }
        }

        //如果没有配置或者找到viewResolver,就采用默认的组件,其他组件初始化也是这思路。
        if (this.viewResolvers == null) {
            this.viewResolvers = getDefaultStrategies(context, ViewResolver.class);
        }
    }

比如我们对viewResolver最常用的配置如下所示。

    //InternalResourceViewResolver就是用来解析JSP的,其实默认采用的也是这个组件
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        //配置统一的前缀路径
        <property name="prefix" value="/WEB-INF/views/" />
        //配置统一的后缀名
        <property name="suffix" value=".jsp" />
    </bean>

再看下从容器中找到的viewResolver长啥样,如下图所示。
这里写图片描述

默认的组件信息保存在一个叫做DispatcherServlet.properties的文件里面,DispatcherServlet类在一开始就把内容加载到了defaultStrategies属性里(使用了静态代码块),该文件的路径如下图所示。
这里写图片描述

总结

最后回顾一下,Web容器启动后(发起调用),GenericServlet接收上下文对象,HttpServletBean给属性赋值,FrameworkServlet负责创建并初始化WebApplicationContext容器,最后DispatcherServlet初始化九大组件,一切准备就绪就等Request进来了,在下篇帖子《SpringMVC源码之DispatchServlet请求处理》中我们将了解是如何处理http请求的。

Spring Framework知识扩展

根据文中的内容,这里列出三个问题,内容较多请按需查看。
1.什么是BeanWrapper?
2.BeanWrapper是怎么设置属性的?
3.getEnvironment方法干了什么?

第一个问题
从定义上看BeanWrapper是用来操作JavaBean属性的工具,比如有一个Person类,代码如下所示。

    public class Person {
        private String name = "张三";

        public void setName(String name) {
            this.name = name;
        }

        public String getName() {
            return this.name;
        }
    }

把这个类封装成BeanWrapper后,可以通过反射的方式调用setter方法直接修改name的值,当然这个例子中的属性只是基本类型所以处理起来简单,但是当属性是数据集合、嵌套对象(比如Person包含属性对象B,通过Person对象来修改B对象的属性值)等情况的时候就变得复杂了,所以Spring就把这些属性统一封装成了PropertyValue对象,从外部屏蔽属性的差异,就能统一处理了。把对象封装成BeanWrapper也是一样的道理,比如Spring提供的IOC容器,它就是把我们配置的class生成对象后,使用BeanWrapper包裹生成统一的Bean(封装可变性),所以有时我们也称它为bean容器。

第二个问题
我们先看下BeanWrapper的实现类BeanWrapperImpl的继承体系,理清类之间的关系就很好理解了,如下图所示。
这里写图片描述

我们只需要看他的三个父接口:PropertyAccessor,PropertyEditorRegistry和TypeConverter。
PropertyAccessor赋予了BeanWrapperImpl访问和操作对象属性的功能,PropertyEditorRegistry赋予了BeanWrapperImpl注册PropertyEditor的功能,PropertyEditor这么来理解,比如从XML配置文件中读取的属性都是String类型,但放到代码中处理的时候,可能是int类型、布尔类型或者数组类型等,这时候就需要类型转换了,而PropertyEditor定义了类型转换的标准。Spring在PropertyEditorRegistrySupport类中会默认注册处理各种类型转换的PropertyEditor实现类,下图所示是该方法的部分截图。
这里写图片描述

要是默认的Editor不能满足需要,也可以实现自己的PropertyEditor,只要继承PropertyEditorSupport类然后覆写setAsText和getAsText方法,像这里给BeanWrapper注册的ResourceEditor就是由SpringMVC自己定义的,继承关系如下图所示。
这里写图片描述

最后由TypeConverter使用PropertyEditor执行类型转换操作。

第三个问题
getEnvironment方法代码如下所示。

    @Override
    public ConfigurableEnvironment getEnvironment() {
        //如果为空则创建一个,我们这里是首次调用,所以会创建该对象。
        if (this.environment == null) {
            this.environment = this.createEnvironment();
        }
        return this.environment;
    }

    protected ConfigurableEnvironment createEnvironment() {
        return new StandardServletEnvironment();
    }

这里创建了StandardServletEnvironment对象,这个类很简单,代码如下所示。

public class StandardServletEnvironment extends StandardEnvironment
    implements ConfigurableWebEnvironment {

    public static final String SERVLET_CONTEXT_PROPERTY_SOURCE_NAME = "servletContextInitParams";
    public static final String SERVLET_CONFIG_PROPERTY_SOURCE_NAME = "servletConfigInitParams";
    public static final String JNDI_PROPERTY_SOURCE_NAME = "jndiProperties";

    @Override
    protected void customizePropertySources(MutablePropertySources propertySources) {
        propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
        propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
        if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
            propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
        }
        super.customizePropertySources(propertySources);
    }

    @Override
    //该方法目前还没有被调用
    public void initPropertySources(ServletContext servletContext, ServletConfig servletConfig) {
        WebApplicationContextUtils.initServletPropertySources(getPropertySources(), servletContext, servletConfig);
    }

}

在它的父类AbstractEnvironment构造函数中调用了customizePropertySources方法,并创建了MutablePropertySources对象(实际是一个CopyOnWriteArrayList)用于存放PropertySource列表。但我们发现customizePropertySources的第一二行代码,只是放入了两个StubPropertySource对象,而这两个对象实际是作为占位符使用的,也就是说这两个位置上的属性还没准备好,我先给他占个位置,那什么时候会加进来呢,我们后面会讲到。接着添加了JndiPropertySource对象,是用来存Jndi信息的,最后调用父类的customizePropertySources方法,添加了MapPropertySource和SystemEnvironmentPropertySource对象分别用来存放虚拟机和电脑的环境变量信息,最终结构如下图所示。
这里写图片描述

bw.setPropertyValues(pvs, true);

最后把配置好的PropertyValues设置到BeanWrapper的对应属性上,这里相当于给DispatchServlet父类FrameworkServlet中的如下两个属性赋值,代码如下所示。

    public abstract class FrameworkServlet extends HttpServletBean 
        implements ApplicationContextAware {

    /** Explicit context config location */
    private String contextConfigLocation;
    /** Should we publish the context as a ServletContext attribute? */
    private boolean publishContext = true;
    //其余代码省略。。。

    }

版权声明:本文为Roderick2015原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。