微信搜索
江南一点雨

SpringMVC 初始化流程分析

[TOC]

框架源码是我们 Coding 晋级中的必修课,SSM 应该算是小伙伴们日常接触最多的框架了,这其中 SpringMVC 初始化流程相对来说要简单一些,因此今天松哥就先来和大家分析一下 SpringMVC 初始化流程。

即使你没看过 SpringMVC 的源码,估计也听说过:DispatcherServlet 是 SpringMVC 的大脑,它负责整个 SpringMVC 的调度工作,是 SpringMVC 中最最核心的类,SpringMVC 整个顶层架构设计都体现在这里,所以搞明白 DispatcherServlet 的源码,基本上 SpringMVC 的工作原理也就了然于胸了。

然而 DispatcherServlet 继承自 FrameworkServlet,FrameworkServlet 又继承自 HttpServletBean,如下图:

因此我们的分析就从 HttpServletBean 开始。

1.HttpServletBean

HttpServletBean 继承自 HttpServlet,它负责将 init-param 中的参数注入到当前 Servlet 实例的属性中,同时也为子类提供了增加 requiredProperties 的能力,需要注意的是 HttpServletBean 并不依赖于 Spring 容器。

大家知道,HttpServlet 的初始化是从 init 方法开始的,所以我们就先从 HttpServletBean 的 init 方法开始看起:

@Override
public final void init() throws ServletException {
    // Set bean properties from init parameters.
    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;
        }
    }
    // Let subclasses do whatever initialization they like.
    initServletBean();
}

在这个方法里,首先获取到 Servlet 的所有配置并转为 PropertyValues,然后通过 BeanWrapper 修改目标 Servlet 的相关属性。BeanWrapper 是 Spring 中提供一个工具,使用它可以修改一个对象的属性,像下面这样:

public class Main {
    public static void main(String[] args) {
        User user = new User();
        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(user);
        beanWrapper.setPropertyValue("username", "itboyhub");
        PropertyValue pv = new PropertyValue("address", "www.itboyhub.com");
        beanWrapper.setPropertyValue(pv);
        System.out.println("user = " + user);
    }
}

最终输出:

user = User{username='itboyhub', address='www.itboyhub.com'}

所以前面的 bw 实际上就代表当前 DispatcherServlet 对象。

通过 BeanWrapper 修改目标 Servlet 的相关属性时,有一个 initBeanWrapper 方法是空方法,开发者如有需要可以在子类中实现该方法,并且完成一些初始化操作。

属性配置完成后,最终调用 initServletBean 方法进行 Servlet 初始化,然而该方法也是一个空方法,在子类中实现。

这就是 HttpServletBean 所做的事情,比较简单,加载 Servlet 相关属性并设置给当前 Servlet 对象,然后调用 initServletBean 方法继续完成 Servlet 的初始化操作。

2.FrameworkServlet

从前面的介绍可知,FrameworkServlet 初始化的入口方法就是 initServletBean,因此我们就从 FrameworkServlet#initServletBean 方法开始看起:

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

这个方法原本挺长的,但是抛开日志打印异常抛出,剩下的核心代码其实就两行:

  1. initWebApplicationContext 方法用来初始化 WebApplicationContext。
  2. initFrameworkServlet 方法用来初始化 FrameworkServlet,但是这个方法是一个空方法,没有具体的实现。本来子类可以重写该方法做一些初始化操作,但是实际上子类并没有重写该方法,所以这个方法我们就暂且忽略之,不去分析了。

那么这里最为重要的其实就是 initWebApplicationContext 方法了,我们一起来看下:

protected WebApplicationContext initWebApplicationContext() {
    WebApplicationContext rootContext =
            WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;
    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);
            }
        }
    }
    if (wac == null) {
        wac = findWebApplicationContext();
    }
    if (wac == null) {
        wac = createWebApplicationContext(rootContext);
    }
    if (!this.refreshEventReceived) {
        synchronized (this.onRefreshMonitor) {
            onRefresh(wac);
        }
    }
    if (this.publishContext) {
        String attrName = getServletContextAttributeName();
        getServletContext().setAttribute(attrName, wac);
    }
    return wac;
}

这里的逻辑也比较清晰:

  1. 首先获取 rootContext。在默认情况下,Spring 会将容器设置为 ServletContext 的一个属性,属性的 key 为 org.springframework.web.context.WebApplicationContext.ROOT,所以根据这个 key 就可以调用 ServletContext#getAttribute 方法获取到 rootContext 了。
  2. 获取 WebApplicationContext 实例,也就是给 wac 变量赋值的过程,这里存在三种可能性:1.如果已经通过构造方法给 webApplicationContext 赋值了,则直接将其赋给 wac 变量,同时,如果需要设置 parent 就设置,需要刷新就刷新。这种方式适用于 Servlet3.0 以后的环境,因为从 Servlet3.0 开始,才支持直接调用 ServletContext.addServlet 方法去注册 Servlet,手动注册的时候就可以使用自己提前准备好的 WebApplicationContext 了,这块松哥在我录制的 Spring Boot 视频中也讲过,感兴趣的小伙伴可以在公众号后台回复 vhr 查看视频详情;2.如果第一步没能成功给 wac 赋值,那么调用 findWebApplicationContext 方法尝试去 ServletContext 中查找 WebApplicationContext 对象,找到了就赋值给 wac;3.如果第二步没能成功给 wac 赋值,那么调用 createWebApplicationContext 方法创建一个 WebApplicationContext 对象并赋值给 wac,一般来说都是通过这种方式创建的 WebApplicationContext。这三套组合拳下来,wac 肯定是有值了。
  3. 当 ContextRefreshedEvent 事件没有触发时,调用 onRefresh 方法完成容器刷新(由于第一种和第三种获取 WebApplicationContext 的方式最终都会调用 configureAndRefreshWebApplicationContext 方法,然后发布事件,再将 refreshEventReceived 变量标记为 true,所以实际上只有第二种方式获取 wac 实例的时候,这里才会刷新,具体可以看下文分析)。
  4. 最后将 wac 保存到到 ServletContext 中。保存的时候会根据 publishContext 变量的值来决定是否保存,publishContext 可以在 web.xml 中配置 Servlet 时通过 init-param 进行配置,保存的目的是为了方便获取。

上面的这些步骤中,通过 createWebApplicationContext 方法创建 WebApplicationContext 对象需要和大家细说下,因为一般情况下就是通过这种方式创建的 WebApplicationContext。我们来看一下相关的方法:

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);
    String configLocation = getContextConfigLocation();
    if (configLocation != null) {
        wac.setConfigLocation(configLocation);
    }
    configureAndRefreshWebApplicationContext(wac);
    return wac;
}
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
    if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
        // The application context id is still set to its original default value
        // -> assign a more useful id based on available information
        if (this.contextId != null) {
            wac.setId(this.contextId);
        }
        else {
            // Generate default id...
            wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                    ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
        }
    }
    wac.setServletContext(getServletContext());
    wac.setServletConfig(getServletConfig());
    wac.setNamespace(getNamespace());
    wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));
    // The wac environment's #initPropertySources will be called in any case when the context
    // is refreshed; do it eagerly here to ensure servlet property sources are in place for
    // use in any post-processing or initialization that occurs below prior to #refresh
    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
    }
    postProcessWebApplicationContext(wac);
    applyInitializers(wac);
    wac.refresh();
}

这里一共涉及到两个方法:

createWebApplicationContext

首先获取到创建类型,并检查创建类型,没问题的话调用 instantiateClass 方法完成创建工作,然后给创建好的 wac 对象配置各种属性,配置的 configLocation 就是我们在 web.xml 文件中配置的 SpringMVC 配置文件路径,默认的文件路径是 /WEB-INF/[servletName]-servlet.xml

configureAndRefreshWebApplicationContext

configureAndRefreshWebApplicationContext 方法主要也是配置&刷新 WebApplicationContext,在这个方法里会调用 addApplicationListener 为 wac 添加一个监听器,监听的是 ContextRefreshedEvent 事件,当收到该事件后,会调用 FrameworkServlet 的 onApplicationEvent 方法,并在该方法中调用 onRefresh 方法完成刷新,刷新之后,会将 refreshEventReceived 变量标记为 true。

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

这就是 FrameworkServlet#initServletBean 方法的大致工作逻辑。这里涉及到了 onRefresh 方法,但是这是一个空方法,在子类 DispatcherServlet 中实现了,所以接下来我们就来看 DispatcherServlet。

3.DispatcherServlet

这里我们就不废话了,直接来看 onRefresh 方法,如下:

@Override
protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
    initMultipartResolver(context);
    initLocaleResolver(context);
    initThemeResolver(context);
    initHandlerMappings(context);
    initHandlerAdapters(context);
    initHandlerExceptionResolvers(context);
    initRequestToViewNameTranslator(context);
    initViewResolvers(context);
    initFlashMapManager(context);
}

在 onRefresh 方法中调用了 initStrategies 进行初始化操作。initStrategies 的内容其实很简单,就是九个组件的初始化。九个的初始化流程比较类似,这里我们以常见的视图解析器的初始化方法 initViewResolvers 为例,来一起看看初始化流程:

private void initViewResolvers(ApplicationContext context) {
    this.viewResolvers = null;
    if (this.detectAllViewResolvers) {
        // Find all ViewResolvers in the ApplicationContext, including ancestor contexts.
        Map<String, ViewResolver> matchingBeans =
                BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.viewResolvers = new ArrayList<>(matchingBeans.values());
            // We keep ViewResolvers in sorted order.
            AnnotationAwareOrderComparator.sort(this.viewResolvers);
        }
    }
    else {
        try {
            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.
        }
    }
    // Ensure we have at least one ViewResolver, by registering
    // a default ViewResolver if no other resolvers are found.
    if (this.viewResolvers == null) {
        this.viewResolvers = getDefaultStrategies(context, ViewResolver.class);
        if (logger.isTraceEnabled()) {
            logger.trace("No ViewResolvers declared for servlet '" + getServletName() +
                    "': using default strategies from DispatcherServlet.properties");
        }
    }
}

一开始的 viewResolvers 变量是一个集合,解析出来的视图解析器对象都将放入这个集合中。

首先判断 detectAllViewResolvers 变量是否为 true,如果为 true,则直接去查找 Spring 容器中的所有视图解析器,将查找结果赋值给 viewResolvers,然后进行排序。默认情况下 detectAllViewResolvers 变量的值为 true,如果有需要,可以在 web.xml 中进行配置,像下面这样:

<servlet>
    <servlet-name>springmvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-servlet.xml</param-value>
    </init-param>
    <init-param>
        <param-name>detectAllViewResolvers</param-name>
        <param-value>false</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>springmvc</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

如果 detectAllViewResolvers 的值为 false,那么接下来就会去 Spring 容器中查找一个名为 viewResolver 的视图解析器,此时查找到的就是一个单独的视图解析器。

一般来说,我们并不需要在 web.xml 中去配置 detectAllViewResolvers 的值,视图解析器有多少个就加载多少个。

举个简单例子,我们在 SpringMVC 的配置文件中可能像下面这样配置视图解析器:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">
    <property name="prefix" value="/WEB-INF/jsp/"/>
    <property name="suffix" value=".jsp"/>
</bean>

默认情况下,这个 bean 的 id 有没有都行,如果有,取什么值都可以,反正最终都是通过类型而不是 id 去查找的视图解析器。但是如果你在 web.xml 中将 detectAllViewResolvers 修改为 false,那么这个 bean 的 id 取值就比较重要了,就一定要是 viewResolver。

如果在 Spring 容器中通过这两种方式(通过类型查找或通过 id 查找)都没有找到 ViewResolver 实例,那么会调用 getDefaultStrategies 方法去获取一个默认的 ViewResolver 实例。默认实例的获取方式如下:

protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
    if (defaultStrategies == null) {
        try {
            // Load default strategy implementations from properties file.
            // This is currently strictly internal and not meant to be customized
            // by application developers.
            ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
            defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
        }
        catch (IOException ex) {
            throw new IllegalStateException("Could not load '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage());
        }
    }
    String key = strategyInterface.getName();
    String value = defaultStrategies.getProperty(key);
    if (value != null) {
        String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
        List<T> strategies = new ArrayList<>(classNames.length);
        for (String className : classNames) {
            try {
                Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
                Object strategy = createDefaultStrategy(context, clazz);
                strategies.add((T) strategy);
            }
            catch (ClassNotFoundException ex) {
                throw new BeanInitializationException(
                        "Could not find DispatcherServlet's default strategy class [" + className +
                        "] for interface [" + key + "]", ex);
            }
            catch (LinkageError err) {
                throw new BeanInitializationException(
                        "Unresolvable class definition for DispatcherServlet's default strategy class [" +
                        className + "] for interface [" + key + "]", err);
            }
        }
        return strategies;
    }
    else {
        return Collections.emptyList();
    }
}

这段代码其实也比较简单,就是通过反射去获取默认的视图解析器。

首先给 defaultStrategies 赋值,defaultStrategies 的值实际上就是从 DispatcherServlet.properties 文件中加载到的,我们来看下这个文件内容:

可以看到,这里一共定义了 8 个默认的键值对,有的值是一个,有的值是多个。前面 initStrategies 方法中一共要初始化九个组件,这里默认只定义了 8 个,少了一个 MultipartResolver,这也好理解,并非所有的项目都有文件上传,而且即使有文件上传,用哪一个具体的 MultipartResolver 也不好确定,还是要开发者自己决定。

defaultStrategies 其实加载到的就是这 8 个键值对,其中视图解析器对应的是 org.springframework.web.servlet.view.InternalResourceViewResolver,通过反射创建该类的实例,当 Spring 容器中不存在任何视图解析器的时候,默认的视图解析器即此。

这就是 initViewResolvers 的工作流程,另外 8 个也和它差不多,唯一不同的是 initMultipartResolver,如下:

private void initMultipartResolver(ApplicationContext context) {
    try {
        this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
    }
    catch (NoSuchBeanDefinitionException ex) {
        this.multipartResolver = null;
    }
}

可以看到,它只是根据 bean 的名字去查找 bean 实例,没有去查找默认的 MultipartResolver。

说到这里,松哥和大家多说一句 SpringMVC 配置中的小细节,

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">
    <property name="prefix" value="/WEB-INF/jsp/"/>
    <property name="suffix" value=".jsp"/>
</bean>
<bean class="org.springframework.web.multipart.commons.CommonsMultipartResolver" id="multipartResolver">
</bean>

上面这个关于视图解析器和文件上传解析器的配置,不知道小伙伴们有没有注意过,视图解析器的 id 可有可无,而文件上传解析器的 id 必须是 multipartResolver,回顾我们上面的源码分析,你就知道为啥了!

4.小结

好啦,这就是松哥和小伙伴们分享的 SpringMVC 的初始化流程,主要涉及到了 HttpServletBean、FrameworkServlet 以及 DispatcherServlet 三个实例,HttpServletBean 主要是加载 Servlet 配置的各种属性并设置到 Servlet 上;FrameworkServlet 则主要是初始化了 WebApplicationContext;DispatcherServlet 则主要是初始化了自身的九个组件。

这只是初始化的流程,那么当请求到来之后,请求的流程又是怎么样的呢?这个松哥下篇文章来和大家分享~好啦,今天就先和小伙伴们聊这么多。

赞(2)
未经允许不得转载:江南一点雨 » SpringMVC 初始化流程分析
分享到: 更多 (0)
扫码关注微信公众号【江南一点雨】,回复 1024,查看松哥原创 Java 实战教程(图文+视频)。

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

专注 Java 一百年

关注我们国际站