赞
踩
目录
1.2 设置和刷新WebApplicationContext
3.4.2 HandlerMethod初始化:afterPropertiesSet方法
3.4.3 拦截器初始化:initApplicationContext
3.5.1 @ControllerAdvice 与 initControllerAdviceCache
3.5.2 参数解析器 HandlerMethodArgumentResolver
3.5.4 返回值处理器 HandlerMethodReturnValueHandler
3.6 处理器异常解析器HandlerExceptionResolver
3.7 视图名翻译器RequestToViewNameTranslator
3.9 FlashMap管理器 FlashMapManager
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。
它自身的代码很简单,实现了来自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!");
- }
接下来,假如当前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。
容器创建完毕后,根据经验来看,还需要一些设置和刷新,源码中通过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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。