Spring Web MVC 5源码分析(1)——DispatcherServlet的加载过程

本文中使用的Spring框架的版本为5.1

前置知识

从设计上说,Spring Web MVC 使用前端控制器模式围绕一个中心Servlet进行设计,这个中心Servlet就是DispatcherServlet,在DispatcherServlet中提供了用于处理请求的通用逻辑,而具体工作委托给可配置的组件执行,通过这种模式使得Spring Web MVC框架变得非常灵活。

与其它的Servlet一样,DispatcherServlet需要根据Servlet规范使用Java代码或者在web.xml文件中进行配置。

Spring Web MVC在启动时,最先加载DispatcherServlet,然后DispatcherServlet再根据配置加载请求映射、视图解析、异常处理所需的组件。

下面是在web.xml文件中对DispatcherServlet进行配置的示例:

<web-app>

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

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/app-context.xml</param-value>
    </context-param>

    <servlet>
        <servlet-name>app</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value></param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>app</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>

</web-app>
复制代码

一些完成具体工作的Bean

在对请求进行处理时,DispatcherServlet会把委托下面这些Bean对请求进行处理,以及给出适当的响应结果。

HandlerMapping

将请求与handler进行映射,HandlerMapping有两个主要的实现:

  • RequestMappingHandlerMapping
  • SimpleUrlHandlerMapping

前者用于对@RequestMapping注解提供支持,后者支持控制器的显示注册。

HandlerAdapter

帮助DispatcherServlet调用与请求路径匹配的handler去处理请求。

通过使用适配器的方式使DispatcherServlet不用关心handler的具体实现细节,比如:调用@Controller注解的控制器需要对该注解进行处理。

HandlerExceptionResolver

使用不同的策略对异常进行处理,比如:返回的HTTP状态码是5xx还是4xx。

ViewResolver

对视图进行解析,比如对JSP与FreeMarker的模版文件进行解析。

LocaleResolver, LocaleContextResolver

提供本地化支持,比如:多国语言、时区等。

ThemeResolver

提供主题支持,用于个性化布局。

MultipartResolver

用于解析multi-part请求。

FlashMapManager

用于管理存放Falsh AttributeFlashMapFalsh Attribute用于跨请求传递数据。

源码分析

从源码中可以找到DispatcherServlet类的定义如下:

public class DispatcherServlet extends FrameworkServlet
复制代码

可以看出,它继承自类FrameworkServlet,下面我们再来看一下类的整体继承关系:

从类的继承关系来看,最后会通过实现Servlet接口来处理HTTP请求,其中与Servlet有关系的类按继承顺序从上至下分别是:

  • GenericServlet,一个抽象类,实现了Servlet接口。
  • HttpServlet,一个抽象类,继承自GenericServlet
  • HttpSerlvetBean,一个抽象类,继承自HttpServlet
  • FrameworkServlet,一个抽象类,继承自HttpServlet
  • DispatcherServlet,一个具体类,继承自FrameworkServlet

其中GenericServletHttpServlet类只是简单对Servlet接口做了一些封装与扩展,因此可以把分析的重点放在HttpSerlvetBeanFrameworkServletDispatcherServlet这三个类上面。

DispatcherServlet初始化时,这三个类之间调用顺序如下图所示:

根据Servlet规范,在Servlet接口中会存在一个init()方法,在一个Servlet实被例化之后容器将会调用一次该方法。Spring Web MVC通过在HttpSerlvetBean类中覆写init()方法从而实现整个框架的加载。

HttpServletBean类

HttpSerlvetBean类的init()方法中主要做了两件事,一是从web.xml文件中读取初始化参数,比如:contextConfigLocation参数。二是调用由子类实现的initServletBean()方法完成具体的初始化工作。

init()方法的实现如下:

@Override
public final void init() throws ServletException {

    // 从web.xml文件中读取初始化参数
    PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
    if (!pvs.isEmpty()) {
        try {
            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) {
            if (logger.isErrorEnabled()) {
            	logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
            }
            throw ex;
        }
    }

    // 调用由子类实现的`initServletBean()`方法。
    initServletBean();
}
复制代码

在上面这段代码中有点意思的是从web.xml文件中读取初始化参数的方式,在这里我们拿contextConfigLocation参数来举例子。

对于参数contextConfigLocation而言,在HttpServletBean的子类FrameworkServlet中存在一个具有以下定义的私有变量:

@Nullable
private String contextConfigLocation;
复制代码

以及对应的Get与Set方法:

public void setContextConfigLocation(@Nullable String contextConfigLocation) {
	this.contextConfigLocation = contextConfigLocation;
}

@Nullable
public String getContextConfigLocation() {
	return this.contextConfigLocation;
}
复制代码

但是Spring并不会直接调用setContextConfigLocation()方法来给contextConfigLocation变量赋值,而是由BeanWrapper搭配ResourceEditor来给变量赋值。

init()方法在读取初始化参数之后,便会调用initServletBean()方法来做初始化工作,该方法的在HttpServletBean中是一个被protected修饰的空方法,其定义如下:

protected void initServletBean() throws ServletException
复制代码

而具体的初始化工作则在HttpServletBean的子类FrameworkServlet中通过覆写initServletBean()方法来完成。

FrameworkServlet类

initServletBean()方法的实现如下:

protected final void initServletBean() throws ServletException {
    try {
        // 初始化WebApplicationContext
    	this.webApplicationContext = initWebApplicationContext();
    	
    	// 一个Hook,让子类有机会在上下文初始化后做一些相关的工作
    	initFrameworkServlet();
    }
    catch (ServletException | RuntimeException ex) {
    	logger.error("Context initialization failed", ex);
    	throw ex;
    }
}
复制代码

在该方法中主要做了下面两件事:

  • 调用initWebApplicationContext()方法初始化webApplicationContext
  • 调用预留的initFrameworkServlet()方法让子类在初始化之后有机会做一些额外的工作。

事实上,目前initFrameworkServlet()是一个没有使用的空函数,并且子类也没有对它进行覆写,所以我们只需要关注initWebApplicationContext()方法即可。

initWebApplicationContext()方法的实现如下:

protected WebApplicationContext initWebApplicationContext() {
    WebApplicationContext rootContext =
            WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;

    if (this.webApplicationContext != null) {
        // 实例化时如果提供了webApplicationContext参数,则使用它。
        wac = this.webApplicationContext;
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
            if (!cwac.isActive()) {
                // 如果该上下文还没有进行过refresh则为它设置一个ID并进行refresh
                if (cwac.getParent() == null) {
                    // 如果该的上下文没有父上下文则为它设置一个。
                    cwac.setParent(rootContext);
                }
                configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    if (wac == null) {
        // 如果实例化时没有提供上下文,则查找ServletContext中有没有提供。
        wac = findWebApplicationContext();
    }
    if (wac == null) {
        // 如果上面没找到一个已经存在的上下文,则自己创建一个
        wac = createWebApplicationContext(rootContext);
    }

    // 如果onRefresh还没有被调用,则手动调用一次
    if (!this.refreshEventReceived) {
        synchronized (this.onRefreshMonitor) {
            // 调用子类实现的onRefresh初始化策略对象
            onRefresh(wac);
        }
    }

    if (this.publishContext) {
        // 将WebApplicationContext放入ServletContext之中
        String attrName = getServletContextAttributeName();
        getServletContext().setAttribute(attrName, wac);
    }

    return wac;
}
复制代码

这个方法中主要做了下面这些事:

第一,检查是不是通过构造函数FrameworkServlet(WebApplicationContext webApplicationContext)传入了webApplicationContext参数。如果是,则会对传入的webApplicationContext进行一些配置,比如:设置父上下文与上下文ID。

第二,检查ServletContext中有没有提供webApplicationContext。如果有,拿过来直接使用。

第三,如果在第一步与第二步中都没有发现可用的webApplicationContext,那就调用createWebApplicationContext()方法自己创建一个。

第四,做一次兜底,通过refreshEventReceived变量的值判断是否调用过onRefresh()方法,如果从未调用过,则触发一次调用。

onRefresh方法除了在initWebApplicationContext()方法中调用了之外,在FrameworkServletonApplicationEvent()方法中也调用了onRefresh方法。

onApplicationEvent()方法的实现如下:

public void onApplicationEvent(ContextRefreshedEvent event) {
    this.refreshEventReceived = true;
    synchronized (this.onRefreshMonitor) {
        onRefresh(event.getApplicationContext());
    }
}
复制代码

在上面的代码中除了调用onRefresh()方法之外,还给变量refreshEventReceived赋值为真,确保onRefresh()方法只会被调用一次。

那么问题来了,又是谁在调用onApplicationEvent()方法?

对于这个问题我们先来看一下用于创建webApplicationContextcreateWebApplicationContext()方法的实现:

protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
    Class<?> contextClass = getContextClass();
    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");
    }
    ConfigurableWebApplicationContext wac =
            (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

    wac.setEnvironment(getEnvironment());
    wac.setParent(parent);
    // 使用configLocation所指定的配置文件
    String configLocation = getContextConfigLocation();
    if (configLocation != null) {
        wac.setConfigLocation(configLocation);
    }
    // 对webApplicationContext进行配置
    configureAndRefreshWebApplicationContext(wac);

    return wac;
}
复制代码

在这个方法中,主要做了两件事:一是使用configLocation所指定的配置文件来加载Bean,二是调用configureAndRefreshWebApplicationContext()方法对webApplicationContext进行配置。

如果你还有印象的话,你可能记得下面这段位于initWebApplicationContext()方法中的代码也调用过 configureAndRefreshWebApplicationContext()方法:

if (this.webApplicationContext != null) {
    // 实例化时如果提供了webApplicationContext参数,则使用它。
    wac = this.webApplicationContext;
    if (wac instanceof ConfigurableWebApplicationContext) {
        ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
        if (!cwac.isActive()) {
            // 如果该上下文还没有进行过refresh则为它设置一个ID并进行refresh
            if (cwac.getParent() == null) {
                // 如果该的上下文没有父上下文则为它设置一个。
                cwac.setParent(rootContext);
            }
            // 对webApplicationContext进行配置
            configureAndRefreshWebApplicationContext(cwac);
        }
    }
}
复制代码

于是乎,我们要研究一下在configureAndRefreshWebApplicationContext()方法中做了哪些不为人知的事情。

configureAndRefreshWebApplicationContext()方法的实现如下:

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
    if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
        // 如果上下文的ID原装的,则为其设置一个更具可读性的ID
        // 使用配置文件指定的ID
        if (this.contextId != null) {
            wac.setId(this.contextId);
        }
        else {
            // 生成一个默认的ID
            wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                    ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
        }
    }

    wac.setServletContext(getServletContext());
    wac.setServletConfig(getServletConfig());
    wac.setNamespace(getNamespace());
    // 添加一个Listener用于监听webApplicationContext的refresh事件
    wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

    // 初始化PropertySource
    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
    }

    postProcessWebApplicationContext(wac);
    
    // 调用ApplicationContextInitializer
    applyInitializers(wac);
    
    // 调用webApplicationContext的refresh方法
    wac.refresh();
}
复制代码

在上面的代码中做了这么几件事:

  • 第一步,为webApplicationContext设置一个ID。
  • 第二步,添加一个ContextRefreshListener用于监听webApplicationContext的refresh事件。
  • 第三步,初始化PropertySource。
  • 第四步,调用webApplicationContext的refresh方法。

在上面第二步中,ContextRefreshListener类的实现如下:

private class ContextRefreshListener implements ApplicationListener<ContextRefreshedEvent> {

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        FrameworkServlet.this.onApplicationEvent(event);
    }
}
复制代码

通过这个类的实现可以看出,这个类作为FrameworkServlet的内部类在收到上下文已刷新的事件后会调用onApplicationEvent()方法。

现在我们已经可以前面的问题了:是谁在调用onApplicationEvent()方法?。

通过上述的分析我们可以知道在configureAndRefreshWebApplicationContext()方法中对webApplicationContext设置了一个监听器,这个监听器在监听到上下文refresh之后会调用onApplicationEvent()方法。

既然onApplicationEvent()方法与initWebApplicationContext()都会调用onRefresh()方法,那么在onRefresh()方法中又做了哪些事情?

onRefresh()方法在FrameworkServlet中的定义如下:

protected void onRefresh(ApplicationContext context) {
}
复制代码

可见,它在FrameworkServlet类中是一个空方法。具体逻辑由FrameworkServlet的子类DispatcherServlet覆写这个方法来实现,下文将对它的具体实现进行分析。

DispatcherServlet类

DispatcherServlet类中onRefresh()方法的实现如下:

@Override
protected void onRefresh(ApplicationContext context) {
	initStrategies(context);
}

protected void initStrategies(ApplicationContext context) {
    // 初始化解析multi-part请求需要的组件
    initMultipartResolver(context);
    // 初始化用于提供本地化支持的主键
    initLocaleResolver(context);
    // 初始化用于提供主题支持的组件
    initThemeResolver(context);
    // 初始化用于请求映射的组件
    initHandlerMappings(context);
    // 初始化用于对handler进行适配的组件
    initHandlerAdapters(context);
    // 初始化用于异常处理的组件
    initHandlerExceptionResolvers(context);
    // 初始化用于视图解析的组件
    initRequestToViewNameTranslator(context);
    initViewResolvers(context);
    // 初始化用于管理Flash Attribute的组件
    initFlashMapManager(context);
}
复制代码

通过上面的代码中可以看出,onRefresh()方法将具体的工作委托给了initStrategies()方法。

initStrategies()方法中,初始化了一系列的策略对象用于对不同的功能提供支持,比如:请求映射、视图解析、异常处理等。

至此,DispatcherServlet初始化完毕。

未完,待续。

猜你喜欢

转载自juejin.im/post/5e363291f265da3e413f6089