Spring源码学习(六):Spring MVC的初始化过程

目录

1.ContextLoaderListener

1.1 创建WebApplicationContext

1.2 设置和刷新WebApplicationContext

2.DispatcherServlet

2.1 init方法

2.2 initServletBean方法

2.3 OnRefresh方法

3.九大组件的注册

3.1 文件上传解析器MultipartResolver

3.2 本地化解析器LocaleResolver

3.3 主题解析器ThemeResolver

3.4 处理器映射器HandlerMapping

3.4.1 initHandlerMappings

3.4.2 HandlerMethod初始化:afterPropertiesSet方法

3.4.3 拦截器初始化:initApplicationContext

3.5 处理器适配器HandlerAdapter

3.5.1 @ControllerAdvice 与 initControllerAdviceCache

3.5.2 参数解析器 HandlerMethodArgumentResolver

3.5.3 @InitBinder的初始化

3.5.4 返回值处理器 HandlerMethodReturnValueHandler

3.6 处理器异常解析器HandlerExceptionResolver

3.7 视图名翻译器RequestToViewNameTranslator

3.8 视图解析器ViewResolver

3.9 FlashMap管理器 FlashMapManager

4.CORS的初始化

4.1 CORS的使用

4.2 initCorsConfiguration方法


Spring最常用的场景就是Web后台开发,这就要使用到Spring MVC相关包:spring-web、spring-webmvc等。一个简单的Spring MVC项目如下:

首先是web.xml,它配置了首页、servlet、servlet-mapping、filter、listener等,Spring MVC通过加载该文件,获取配置的Servlet,来拦截URL。下面的配置中,指定了Spring配置文件的位置,设置了DispatcherServlet及启动级别,它将会在启动后尝试从WEB-INF下面加载 servletName-servlet.xml(斜粗体部分为servlet-name配置的内容),listener部分配置了上下文载入器,用来载入其它上下文配置文件,然后配置了servlet映射,“/”表示它拦截所有类型的URL:

<web-app version="2.5" 
    xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
    http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>
    <servlet>
        <servlet-name>hello</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>2</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>hello</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

Spring也支持编程式配置DispatcherServlet,只要实现WebApplicationInitializer的onStartup方法,在里面创建DispatcherServlet实例并注册到ServletContext即可(例子来源于官方文档):

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletCxt) {

        // Load Spring web application configuration
        AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
        ac.register(AppConfig.class);
        ac.refresh();

        // Create and register the DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(ac);
        ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
        registration.setLoadOnStartup(1);
        registration.addMapping("/app/*");
    }

然后是applicationContext.xml,它就是一个普通的Spring配置文件,一般会在这里配置ViewResolver,下面是一个JSP配置:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
          p:viewClass="org.springframework.web.servlet.view.JstlView"
          p:prefix="/WEB-INF/jsp/"
          p:suffix=".jsp"/>

hello-context.xml用来配置URL处理器的映射规则,也可以配置如下:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context-4.0.xsd">
    <context:component-scan base-package="com.test.controller"/>
</beans>

上述配置表示自动扫描com.test.controller包下,由stereotype类型注解标记的类,有四种:@Controller、@Component、@Repository、@Service。

然后我们就可以编写jsp文件和Controller,启动程序后就可以输入URL看到结果。

在上述配置中,有两个关键类:ContextLoaderListener和DispatcherServlet。

1.ContextLoaderListener

它自身的代码很简单,实现了来自ServletContextLoader接口的contextInitialized、contextDestroyed两个方法,不过主要实现在父类ContextLoader中。

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
    ContextLoaderListener() {
	}

	public ContextLoaderListener(WebApplicationContext context) {
		super(context);
	}

	@Override
	public void contextInitialized(ServletContextEvent event) {
		initWebApplicationContext(event.getServletContext());
	}

	@Override
	public void contextDestroyed(ServletContextEvent event) {
		closeWebApplicationContext(event.getServletContext());
		ContextCleanupListener.cleanupAttributes(event.getServletContext());
	}
}

实际上,Spring正是依靠ServletContextListener,才能被Tomcat容器加载的:

public boolean listenerStart() {
    ...
    for (int i = 0; i < instances.length; i++) {
       if (!(instances[i] instanceof ServletContextListener))
           continue;
       ServletContextListener listener =
           (ServletContextListener) instances[i];
       try {
           fireContainerEvent("beforeContextInitialized", listener);
           if (noPluggabilityListeners.contains(listener)) {
               listener.contextInitialized(tldEvent);
           } else {
               listener.contextInitialized(event);
           }
           fireContainerEvent("afterContextInitialized", listener);
       } catch (Throwable t) {
           ...
       }
   }
   return ok;
}

可见Tomcat启动Spring容器就是靠contextInitialized调用initWebApplicationContext方法来实现的,从名字不难看出,WebApplicationContext就是在ApplicationContext的基础上增加了一些Web操作及属性。下面来看看这个方法的源码。

首先判断了一次web.xml中是否重复定义了ContextLoader,从下面的代码可以看出,每当创建WebApplicationContext实例时,就会记录在ServletContext中以便全局调用,key就是ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,所以可以getAttribute来检查是否已经创建过WebApplicationContext:

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!");
}

1.1 创建WebApplicationContext

接下来,假如当前ContextLoader还没有管理任何WebApplicationContext实例,就创建一个,创建方法为createWebApplicationContext。最后的instantiateClass在阅读Spring源码时已经见过很多次了,作用是将Class对象实例化,因此,该方法的核心是determineContextClass方法:

protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
	Class<?> contextClass = determineContextClass(sc);
	if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
		throw new ApplicationContextException("Custom context class [" + contextClass.getName() + "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
	}
	return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}

determineContextClass基本逻辑如下(去除了try-catch),ClassUtil.forName很显然就是反射创建类

protected Class<?> determineContextClass(ServletContext servletContext) {
    String contextClassName = servletContext.getInitParameter("contextClass");
    if (contextClassName != null) {
        return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
    }
    else {
        contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
        return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
    }
}

如果是配合Tomcat使用,一般传入的是ApplicationContext(这个ApplicationContext是ServletContext的子类,而不是Spring容器),它的getInitParameter实现如下:

public String getInitParameter(final String name) {
    if ("org.apache.jasper.XML_VALIDATE_TLD".equals(name) &&
        context.getTldValidation()) {
            return "true";
    }
    if ("org.apache.jasper.XML_BLOCK_EXTERNAL".equals(name)) {
        if (!context.getXmlBlockExternal()) {
            return "false";
        }
    }
    return parameters.get(name);
}

这里将常量替换为对应的字面值,可以看到,最终是从一个Map中获取值。如果我们配置了自定义的WebApplicationContext实现,则加载自定义的,否则通过WebApplicationContext的全限定名查找需要加载的类名,并进行加载。在ContextLoader的静态块中,可以看到如下语句:

ClassPathResource resource = new ClassPathResource("ContextLoader.properties", ContextLoader.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);

也就是说,SpringMVC默认从classpath:org.springframework/web/context/ContextLoader.properties文件加载容器的类名,查询一下,果然如此:

# Default WebApplicationContext implementation class for ContextLoader.
# Used as fallback when no explicit context implementation has been specified as context-param.
# Not meant to be customized by application developers.

org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext

可见Spring Web容器的实现类为XmlWebApplicationContext。

1.2 设置和刷新WebApplicationContext

容器创建完毕后,根据经验来看,还需要一些设置和刷新,源码中通过configureAndRefreshWebApplicationContext方法实现。

该源码可分为设置和刷新两部分。首先看设置,代码检查了是否配置了contextId和contextConfigLocation,是则赋给新创建的容器,并且通过setServletContext将Web容器和Servlet上下文关联起来。然后获取Environment进行PropertySource的初始化,这一步中如果没有设置环境,会创建一个StandardServletEnvironment实例,获取servletContextInitParams和servletConfigInitParams,然后进行属性替换。

if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
	String idParam = sc.getInitParameter("contextId");
	if (idParam != null) {
		wac.setId(idParam);
	}
	else {
		wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
				ObjectUtils.getDisplayString(sc.getContextPath()));
	}
}
wac.setServletContext(sc);
String configLocationParam = sc.getInitParameter("contextConfigLocation");
if (configLocationParam != null) {
	wac.setConfigLocation(configLocationParam);
}
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
	((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}

接下来调用customizeContext方法对Web容器进行初始化,它会寻找配置的contextInitializerClasses或globalInitializerClasses,使用它们对Web容器进行初始化。

protected void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {
    List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> initializerClasses = determineContextInitializerClasses(sc);
	for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerClass : initializerClasses) {
		Class<?> initializerContextClass =
			GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class);
		if (initializerContextClass != null && !initializerContextClass.isInstance(wac)) {
			throw new ApplicationContextException(String.format(
				"Could not apply context initializer [%s] since its generic parameter [%s] " + "is not assignable from the type of application context used by this " + "context loader: [%s]", initializerClass.getName(), initializerContextClass.getName(),wac.getClass().getName()));
		}
		this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
	}
	AnnotationAwareOrderComparator.sort(this.contextInitializers);
	for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
		initializer.initialize(wac);
	}
}

接着调用refresh方法对容器进行刷新。使用过Spring一定不会对它陌生,该方法位于AbstractApplicationContext,绝大部分基本逻辑和Spring是一致的,但是在XmlWebApplicationContext中,对loadBeanDefinitions和postProcessBeanFactory进行了实现,因此又有些区别,首先是loadBeanDefinitions:

protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws IOException {
    String[] configLocations = getConfigLocations();
    if (configLocations != null) {
        for (String configLocation : configLocations) {
            reader.loadBeanDefinitions(configLocation);
        }
    }
}

protected String[] getDefaultConfigLocations() {
   return this.getNamespace() != null ? new String[]{"/WEB-INF/" + this.getNamespace() + ".xml"} : new String[]{"/WEB-INF/applicationContext.xml"};
}

这里读取了WEB-INF下的配置文件,要么由Namespace决定,要么默认读取applicationContext.xml。提到Namespace就不难想到Spring Schema,即通过META-INF下的spring.handlers文件配置命名空间解析器。

Spring MVC的默认命名空间解析器为MvcNamespaceHandler,它注册了一系列解析器,这些解析器方法又在parse方法中注册了一系列组件,例如常用的<mvc:annotation-driven/>配置,就会注册RequestMappingHandlerMapping、RequestMappingHandlerAdapter等:

context.registerComponent(new BeanComponentDefinition(handlerMappingDef, HANDLER_MAPPING_BEAN_NAME));
context.registerComponent(new BeanComponentDefinition(handlerAdapterDef, HANDLER_ADAPTER_BEAN_NAME));
context.registerComponent(new BeanComponentDefinition(uriContributorDef, uriContributorName));
context.registerComponent(new BeanComponentDefinition(mappedInterceptorDef, mappedInterceptorName));
context.registerComponent(new BeanComponentDefinition(methodExceptionResolver,
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值