Spring源码深度解析之Spring MVC
Spring框架提供了构建Web应用程序的全功能MVC模块。通过策略接口,Spring框架是高度可配置的,而且支持多种视图技术,例如JavaServer pages(JSP)技术、Velocity、Tiles、iText和POI。Spring MVC框架并不知道使用的视图,所以不会强迫您只使用JSP技术。Spring MVC分离了控制器、模型对象、分派器以及处理程序对象的角色,这种分离让它们更容易进行定制。
Spring的MVC是基于Servlet功能实现的。通过实现Servlet接口的DispatcherServlet来封装其核心功能实现,通过将请求分派给处理程序,同时带有可配置的处理程序映射、视图解析、本地语言、主题解析以及上载文件的支持。默认的处理程序是非常简单的Controller接口,只有一个方法ModelAndView handleRequest(request, response)。Spring提供了一个控制器层次结构,可以派生子类。如果应用程序需要处理用户输入表单,那么可以继承AbstractFormController。如果需要把多页输入处理到一个表单,那么可以继承AbstractWizardFormController。
Spring MVC或者其他比较成熟的MVC框架而言,解决的问题无外乎以下几点:
(1)将Web页面的请求传给服务器。
(2)根据不同的请求处理不同的逻辑单元。
(3)返回处理结果数据并跳转到响应的页面。
我们先通过一个简单示例来快速回顾Spring MVC的使用。
一、 Spring MVC快速体验
(一)配置web.xml
一个Web中可以没有web.xml文件,也就是说,web.xml文件并不是Web工厂必须的。web.xml文件用来初始化配置信息:比如Welcome页面、servlet、servlet-mapping、filter、listener、启动加载级别等。但是,Spring MVC的实现原理就是通过Servlet拦截所有URL来达到控制的目的,所以web.xml的配置是必须的。
1 <?xml version="1.0" encoding="UTF-8"?> 2 <web-app id="WebApp_ID" version="2.5" xmlns="http://java.sun.com/xml/ns/j2ee" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> 5 <display-name>Springmvc</display-name> 6 7 <!--使用ContextLoaderListener配置时,需要告诉他Spring配置文件的位置--> 8 <context-param> 9 <param-name>contextConfigLocation</param-name> 10 <param-value>classpath:applicationContext.xml</param-value> 11 </context-param> 12 13 <!--Spring MVC的前端控制器--> 14 <!--当DispatcherServlet载入后,它将从一个XML文件中载入Spring的应用上下文,该XMl文件的名字取决于<servlet-name>--> 15 <!--这里DispatcherServlet将试图从一个叫做Springmvc-servlet.xml的文件中载入应用上下文,其默认位于WEB-INF目录下--> 16 <servlet> 17 <servlet-name>Springmvc</servlet-name> 18 <servlet-class>org.Springframework.web.servlet.DispatcherServlet</servlet-class> 19 <load-on-startup>1</load-on-startup> 20 </servlet> 21 <servlet-mapping> 22 <servlet-name>Springmvc</servlet-name> 23 <url-pattern>*.htm</url-pattern> 24 </servlet-mapping> 25 26 <!--配置上下文载入器--> 27 <!--上下文载入器载入除DispatcherServlet载入的配置文件之外的其它上下文配置文件--> 28 <!--最常用的上下文载入器是一个Servlet监听器,其名称为ContextLoaderListener--> 29 <listener> 30 <listener-class>org.Springframework.web.context.ContextLoaderListener</listener-class> 31 </listener> 32 33 </web-app>
Spring的MVC之所以必须要配置web.xml,其实最关键的是要配置两个地方。
(1)contextConfigLocation:Spring的核心就是配置文件,可以说配置文件是Spring中必不可少的东西,而这个参数就是使Web与Spring的配置文件相结合的一个关键配置。
(2)DispatcherServlet:包含了SpringMVC的请求逻辑,Spring使用此类拦截Web请求并进行相应的逻辑处理。
(二)创建Spring配置文件applicationContext.xml
1 <?xml version="1.0" encoding="UTF-8" ?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmls:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns:tx="http://www.springframework.org/schema/tx" 5 xsi:schemaLocation="http://www.springframework.org/schema/beans 6 http://www.springframework.org/schema/beans/spring-beans-2.5.xsd 7 http://www.springframework.org/schema/tx 8 http://www.springframework.org/schema/tx/spring-tx-2.5.xsd"> 9 10 <bean id="viewResolver" class="org.springframework.web.servlet.view.Internal Resource ViewResolver"> 11 <property name="prefix" value="/WEB-INF/jsp/"/> 12 <property name="suffix" value=".jsp"/> 13 </bean> 14 15 </beans>
InternalResourceViewResolver是一个辅助Bean,会在ModelAndView返回的视图名前加上prefix指定的前缀,再在最后加上suffix指定的后缀,例如:由于XXController返回的ModelAndView中的视图名师testview。故该视图解析器将在/WEB-INF/jsp/testview.jsp处查找视图。
(三)创建model
模型对于Spring MVC来说是必不可少,如果处理程序非常简单,完全可以忽略。模型创建的主要目的就是承载数据,使数据传输更加方便。
1 public class User{ 2 private String username; 3 private Integer age; 4 5 public String getUsername(){ 6 return username; 7 } 8 public void setUsername(String username){ 9 this.username = username; 10 } 11 12 public Integer getAge() { 13 return age; 14 } 15 public void setAge(Integer age){ 16 this.age = age; 17 } 18 }
(四)创建Controller
控制器用于处理Web请求,每个控制器都对应着一个逻辑处理。
1 public class UserController extends AbstractController{ 2 @Override 3 protected ModelAndView handleRequestInternal(HttpServletRequest arg0, HttpServletResponse arg1) throws Exception{ 4 List<User> userList = new ArrayList<User>(); 5 User userA = new User(); 6 User userB = new User(); 7 userA.setUsername("张三"); 8 userA.setAge(27); 9 userB.setUsername("李四"); 10 userB.setAge(37); 11 userList.add(userA); 12 userList.add(userB); 13 14 return new ModelAndView("userlist", "users", userList); 15 } 16 }
在请求的最后返回了ModelAndView类型的实例。ModelAndView类在Spring MVC中占有很重要的地位。控制器执行方法都必须返回一个ModelAndView,ModelAndView对象保存了视图以及视图显示的模型数据,例如其中的参数如下:
第一个参数userlist:视图组件的逻辑名称。这里视图的逻辑名称就是userlist,视图解析器会使用该名称查找实际的View对象。
第二个参数users:传递给视图的,模型对象的名称。
第三个参数userList:传递给视图的,模型对象的值。
(五)创建视图文件userlist.jsp
1 <%@ page language="java" pageEncoding="UTF-8" %> 2 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 3 <h2>This is SpringMVC demo page</h2> 4 <c:forEach item="${users}" var="user"> 5 <c:out value="${user.username}"/><br/> 6 <c:out value="${usr.age}"/><br/> 7 </c:forEach>
视图文件用于展现请求处理结果,通过对JSTL的支持,可以很方便地展现在控制器中放入ModelAndView中处理结果数据。
(六)创建Servlet配置文件Spring-servlet.xml
1 <?xml version="1.0" encoding="UTF-8" ?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http//www.w3.org/2001/XMLSchema-instance" 4 xmlns:tx="http://www.springframework.org/schema/tx" 5 xsi:schemaLocation="http://www.springframework.org/schema/beans 6 http://www.springframework.org/schema/beans/spring-beans-2.5.xsd 7 http://www.springframework.org/schema/tx 8 http://www.springframework.org/schema/tx/spring-tx-2.5.xsd"> 9 10 <bean id="simpleUrlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> 11 <property name="mapping"> 12 <props> 13 <prop key="/usrerlist.htm">userController</prop> 14 </props> 15 </property> 16 </bean> 17 18 <!--这里的 id="userController对应的是<bean id="simpleUrlMapping">中的<prop>里面的value"--> 19 <bean id="userController" class="test.controller.UserController"/> 20 21 </beans>
因为Spring MVC是基于Servlet的实现,所以在Web启动的时候,服务器会首先尝试加载对应于Servlet的配置文件,而为了让项目更加模块化,通常我们讲Web部分的配置都存放于此配置文件中。
至此,已经完成了Spring MVC的搭建,启动服务器,输入网址http://localhost:8080/Springmvc/userlist.htm。会看到服务器器的返回界面。
二 、ContextLoaderListener
对于Spring MVC功能实现的分析,我们首先从web.xml开始,在web.xml文件中我们首先配置的就是contextLoaderListener,那么它所提供的功能有哪些又是如何实现的呢?
当使用编程方式的时候我们可以直接将Spring配置信息作为参数传入Spring容器中,如:
ApplicationContext ac = new ClassPathXmlApplicationContext(“applicationContext.xml”);
但是在Web下,我们需要更多的是与Web环境相互结合,通常的办法是将路径以context-param的方式注册并使用ContextLoaderListener进行监听读取。
ContextLoaderListener的作用就是启动Web容器时,自动装配ApplicationContext的配置信息。因为它实现了ServletContextListener这个接口,在web.xml配置这个监听器,启动容器时,就会默认执行它实现的方法,使用ServletContextListener接口,开发者能够在为客户端请求提供服务之前向ServletContext中添加任意的对象。这个对象在ServletContext启动的时候被初始化,然后在ServletContext整个运行期间都是可见的。
每一个Web应用都有一个ServletContext与之相关联。ServletContext对象在应用启动时被创建,在应用关闭的时候被销毁。ServletContext在全局范围内有效,类似于应用中一个全局变量。
在ServletContextListener中的核心逻辑便是初始化WebApplicationContext实例并存放至ServletContext中。
(一)ServletContextListener的使用
正式分析代码前我们同样还是先了解ServletContextListener的使用。
(1)创建自定义ServletContextListener。
首先我们创建ServletContextListener,目标是在系统启动时添加自定义的属性,以便于在全局范围内可以随时调用。系统启动的时候会调用ServletContextListener实现类的contextInitialized方法,所以需要在这个方法中实现我们的初始化逻辑。
1 public class MyDataContextListener implements ServletContextListener{ 2 3 private ServletContext context = null; 4 5 public MyDataContextListener(){ 6 7 } 8 9 //该方法在ServletContext启动之后被调用,并准备好处理客户端的请求 10 public void contextInitialized(ServletContextEvent event){ 11 this.context = event.getServletContext(); 12 //通过你可以实现自己的逻辑并将结果记录在属性中 13 context = setAttribut("myData", "this is myData"); 14 } 15 16 //这个方法在ServletContext将要关闭的时候调用 17 public void contextDestroyed(ServletContextEvent event){ 18 this.context = null; 19 } 20 }
(2)注册监听器。
在web.xml文件中需要注册自定义的监听器。
1 <listener> 2 com.test.MyDataContextListener 3 </listener>
(3)测试。
一旦Web应用启动的时候,我们就能在任意的Servlet或者JSP中通过下面的方式获取我们初始化参数,如下:String myData = (String) getServletContext().getAttribut(“myData”);
(二)Spring中的ContextLoaderListener
分析了ServletContextListener的使用方式后再来分析Spring中的ContextLoaderListener的实现方式就容易理解得多,虽然ContextLoaderListener实现的逻辑要复杂得多,但是大致的套路还是万变不离其宗。
ServletContext启动后会调用ServletContextListener的contextInitialized方法,那么,我们就从这个函数开始进行分析。
1 @Override 2 public void contextInitialized(ServletContextEvent event) { 3 //该方法在其父类ContextLoader中定义 4 initWebApplicationContext(event.getServletContext()); 5 }
这里涉及了一个常用类WebApplicationContext:在Web应用中,我们会用到WebApplicationContext,WebApplicationContext继承自ApplicationContext,在ApplicationContext的基础上又追加了一些特定于Web的操作及属性。非常类似于我们通过编程方式使用Spring时使用的ClassPathXmlApplicationContext类提供的功能。继续跟踪代码:
1 public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { 2 if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) { 3 //web.xml中存在多次ContextLoader的定义 4 throw new IllegalStateException( 5 "Cannot initialize context because there is already a root application context present - " + 6 "check whether you have multiple ContextLoader* definitions in your web.xml!"); 7 } 8 9 servletContext.log("Initializing Spring root WebApplicationContext"); 10 Log logger = LogFactory.getLog(ContextLoader.class); 11 if (logger.isInfoEnabled()) { 12 logger.info("Root WebApplicationContext: initialization started"); 13 } 14 long startTime = System.currentTimeMillis(); 15 16 try { 17 // Store context in local instance variable, to guarantee that 18 // it is available on ServletContext shutdown. 19 if (this.context == null) { 20 //初始化context 21 this.context = createWebApplicationContext(servletContext); 22 } 23 if (this.context instanceof ConfigurableWebApplicationContext) { 24 ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context; 25 if (!cwac.isActive()) { 26 // The context has not yet been refreshed -> provide services such as 27 // setting the parent context, setting the application context id, etc 28 if (cwac.getParent() == null) { 29 // The context instance was injected without an explicit parent -> 30 // determine parent for root web application context, if any. 31 ApplicationContext parent = loadParentContext(servletContext); 32 cwac.setParent(parent); 33 } 34 configureAndRefreshWebApplicationContext(cwac, servletContext); 35 } 36 } 37 //记录在servletContext中 38 servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); 39 40 ClassLoader ccl = Thread.currentThread().getContextClassLoader(); 41 if (ccl == ContextLoader.class.getClassLoader()) { 42 currentContext = this.context; 43 } 44 else if (ccl != null) { 45 currentContextPerThread.put(ccl, this.context); 46 } 47 48 if (logger.isInfoEnabled()) { 49 long elapsedTime = System.currentTimeMillis() - startTime; 50 logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms"); 51 } 52 53 return this.context; 54 } 55 catch (RuntimeException | Error ex) { 56 logger.error("Context initialization failed", ex); 57 servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex); 58 throw ex; 59 } 60 }
initWebApplicationContext函数主要是体现了创建WebApplicationContext实例的一个功能架构,从函数中我们看到了初始化的大致步骤。
(1)WebApplicationContext存在性的验证。
在配置中只允许申明一次ServletContextListener,多次申明会扰乱Spring的执行逻辑,所以这里首先要做的就是对比验证,在Spring中如果创建WebApplicationContext;实例会记录在ServletContext中以方便全局调用,而使用的key就是WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,所以验证的方式就是查看ServletContext实例中是否有对应key的属性。
(2)创建WebApplicationContext实例。
如果通过验证,则Spring将创建WebApplicationContext实例的工作委托给了createWebApplicationContext函数。
1 protected WebApplicationContext createWebApplicationContext(ServletContext sc) { 2 Class<?> contextClass = determineContextClass(sc); 3 if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { 4 throw new ApplicationContextException("Custom context class [" + contextClass.getName() + 5 "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]"); 6 } 7 return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass); 8 }
createWebApplicationContext函数又调用了determineContextClass方法,继续:
1 protected Class<?> determineContextClass(ServletContext servletContext) { 2 String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM); 3 if (contextClassName != null) { 4 try { 5 return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader()); 6 } 7 catch (ClassNotFoundException ex) { 8 throw new ApplicationContextException( 9 "Failed to load custom context class [" + contextClassName + "]", ex); 10 } 11 } 12 else { 13 contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName()); 14 try { 15 return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader()); 16 } 17 catch (ClassNotFoundException ex) { 18 throw new ApplicationContextException( 19 "Failed to load default context class [" + contextClassName + "]", ex); 20 } 21 } 22 }
其中,在ContextLoader类中有这样的静态代码块:
1 static { 2 // Load default strategy implementations from properties file. 3 // This is currently strictly internal and not meant to be customized 4 // by application developers. 5 try { 6 ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class); 7 defaultStrategies = PropertiesLoaderUtils.loadProperties(resource); 8 } 9 catch (IOException ex) { 10 throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage()); 11 } 12 }
根据以上静态的代码块内容,我们推断在当前类ContextLoader同样目录下必定会存在属性文件ContextLoader.properties(在..\spring-web\src\main\resources\org\springframework\web\context目录下),查看后果然存在,内容如下:
org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext
综合以上代码分析,在初始化过程中,程序首先会读取ContextLoader类的同目录下的属性文件ContextLoader.properties。并根据其中的配置提取将要实现WebApplicationContext接口的实现类,并根据这个实现类通过反射的方式进行实例的创建。
(3)将实例记录在servletContext中。
(4)映射当前的类加载器与创建的实例到全局变量currentContextPerThread中。
三、 DispatcherServlet
在Spring中,ContextLoaderListener只是辅助功能,用于创建WebApplicationContext类型实例,而真正的逻辑其实是在DispatcherServlet(package org.springframework.web.servlet)中进行的,DispatcherServlet是实现servlet的接口的实现类。
servlet是一个Java编写的程序,此程序是基于HTTP协议的,在服务器端运行的(如Tomcat),是按照servlet规范编写的一个Java类。主要是处理客户端的请求并将其结果发送到客户端。servlet的生命周期是由servlet的容器来控制的,它可以分为3个阶段:初始化、运行和销毁。
(1)初始化阶段
servlet容器加载servlet类,把servlet类的.class文件中的数据读到内存中。
servlet容器创建一个ServletConfig对象,ServletConfig对象包含了servlet的初始化配置信息。
servlet容器创建一个servlet对象。
servlet容器调用servlet对象的init方法机芯初始化化。
(2)运行阶段
当servelt容器接收到一个请求是,servelt容器会针对这个请求创建serveltRequest和ServletResponse对象,然后调用service方法。并把这两个参数传递给service方法。service方法通过servletRequest对象获得请求的信息。并处理请求。再通过servletResponse对象生成这个请求的响应结果。然后销毁servletRequest和ServletResponse对象。我们不管这个请求是post提交的还是get提交的,最终这个请求都会由service方法来处理。
(3)销毁阶段。
当Web应用被终止时,servlet容器会先调用servlet对象的destroy方法,然后在销毁servlet对象,同时也会销毁与servlet对象相关联的servletConfig对象。我们可以在destroy方法的实现中,释放servlet所占用的资源,如关闭数据库连接,关闭文件输入输出流等。
servlet的框架是由两个Java包组成:javax.servlet和javax.servlet.http。在javax.servlet包中定义了所有的servlet类都必须实现或拓展的通用接口和类。在javax.servlet.http包中定义了采用HTTP通信协议的HttpServlet类。
servlet类被设计成请求驱动,servlet的请求可能包含多个数据项,当Web容器接收到某个servlet请求是,servelt把请求封装成一个HTTPServletRequest对象,然后把对象传给servlet的对象的服务方法。
HTTP的请求方式包括delete、get、options、post、put和trace。在HttpServlet类中分别提供了相应的服务方法,它们是doDelete()、doGet()、doOptions()、doPost()、doPut()和doTrace()。
(一)servlet的使用
我们同样还是以最简单的servlet来快速体验其用法。
(1)建立servlet
1 public class Myservlet extends HttpServlet{ 2 public void init(){ 3 System.out.println("this is a method"); 4 } 5 6 public void doGet(HttpServletRequest request, HttpServletResponse response){ 7 handleLogic(request, response); 8 } 9 10 public void doPost(HttpServletRequest request, HttpServletResponse response){ 11 handleLogic(request, response); 12 } 13 14 public void handleLogic(HttpServletRequest request, HttpServletResponse response){ 15 System.out.println("handle myLogic"); 16 ServletContext sc = getServletContext(); 17 18 RequestDispatcher rd = null; 19 20 rd = sc.getRequestDispatcher("/index.jsp"); 21 try { 22 rd.forward(request, response); 23 } catch (ServletException | IOException e) { 24 e.printStackTrace(); 25 } 26 } 27 }
麻雀虽小,五脏俱全。实例中包含了对init方法和get/post方法的处理,init方法包装在servlet加载的时候能做一些逻辑操作,而HttpServlet类则会帮助我们根据方法类型的不同而将逻辑引入不同的函数。在子类中我们只需要重写对应的函数逻辑便可,如以上代码重写了doGet和doPost方法并将逻辑处理部分引导至handleLogic函数中,最后,又将页面跳转至index.jsp。
(2)添加配置
为了是servlet能够正常使用,需要在web.xml文件中添加以下配置:
1 <servlet> 2 <servlet-name>myservlet</servlet-name> 3 <servlet-name>test.servlet.MyServlet</servlet-name> 4 <load-on-startup>1</load-on-startup> 5 </servlet> 6 7 <servlet-mapping> 8 <servlet-name>myservlet</servlet-name> 9 <url-pattern>*.htm</url-pattern> 10 </servlet-mapping>
配置后便可以根据对应的配置访问响应的路径了。
(二)DispatcherServlet的初始化
通过上面的实例我们了解到,在servlet初始化阶段会调用器init方法,所以我们首先要查看在DispatcherServlet中是否重写了init方法。我们再其父类HttpServletBean(package org.springframework.web.servlet)中找到了该方法。
1 @Override 2 public final void init() throws ServletException { 3 4 // Set bean properties from init parameters. 5 //解析init-param并封装在pvs中 6 PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties); 7 if (!pvs.isEmpty()) { 8 try { 9 //将当前的这个Servlet类转化成一个BeanWrapper,从而能够以Spring的方式来对init-param的值进行注 10 BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); 11 ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext()); 12 //注册自定义属性编辑器,一旦遇到Resource类型的属性将会使用ResourceEditor进行解析 13 bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment())); 14 //空实现,留给子类覆盖 15 initBeanWrapper(bw); 16 //属性注入 17 bw.setPropertyValues(pvs, true); 18 } 19 catch (BeansException ex) { 20 if (logger.isErrorEnabled()) { 21 logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex); 22 } 23 throw ex; 24 } 25 } 26 27 // Let subclasses do whatever initialization they like. 28 //留给子类扩展 29 initServletBean(); 30 }
DispatchServlet的初始化过程主要是通过将当前的servlet类型实例转换成BeanWrapper类型实例,以便使用Spring中提供的注入功能进行对应属性的注入。这些属性如contextAttribut、contextClass、nameSpace、contextConfigLocation等,都可以在web.xml文件中以初始化参数的方式配置在servelt的声明中。DispatcherServlet继承自FrameworkServlet,FrameServlet类上包含对应的同名属性,Spring会保证这些参数被注入到对应的值中。属性注入主要包含以下几个步骤:
(1)封装及验证初始化参数
ServletConfigPropertyValues除了封装属性外还有对属性验证的功能。
1 public ServletConfigPropertyValues(ServletConfig config, Set<String> requiredProperties) 2 throws ServletException { 3 4 Set<String> missingProps = (!CollectionUtils.isEmpty(requiredProperties) ? 5 new HashSet<>(requiredProperties) : null); 6 7 Enumeration<String> paramNames = config.getInitParameterNames(); 8 while (paramNames.hasMoreElements()) { 9 String property = paramNames.nextElement(); 10 Object value = config.getInitParameter(property); 11 addPropertyValue(new PropertyValue(property, value)); 12 if (missingProps != null) { 13 missingProps.remove(property); 14 } 15 } 16 17 // Fail if we are still missing properties. 18 if (!CollectionUtils.isEmpty(missingProps)) { 19 throw new ServletException( 20 "Initialization from ServletConfig for servlet '" + config.getServletName() + 21 "' failed; the following required properties were missing: " + 22 StringUtils.collectionToDelimitedString(missingProps, ", ")); 23 } 24 }
从代码中得知,封装属性主要是对初始化的参数进行封装,也就是servlet中配置的<init-param>中配置的封装。当然,用户可以同对requiredProperties参数的初始化来强制验证某些属性的必要性。这样,在属性封装的过程中,一旦检测到requiredProperties中的属性没有指定初始值,就会抛出异常。
(2)将当前servlet实例转化成BeanWrapper实例
PropertyAccessorFactory.forBeanPropertyAccess是Spring中提供的工具方法,主要用于将指定实例转化成Spring中可以处理的BeanWrapper类型的实例。
(3)注册相对于Resource的属性编辑器
属性编辑器,我们再上文中已经介绍并分析过其原理。这里使用属性编辑器的目的是在对当前实例(DispatcherServlet)属性注入过程中一旦遇到Resource类型的属性就会使用ResourceEditor去解析。
(4)属性注入
BeanWrapper为Spring中的方法,支持Spring的自动注入。其实我们最常用的属性注入无非是contextAttribut、contextClass、nameSpace、contextConfigLocation等属性。
(5)servletBean的初始化
在ContextLoaderListener加载的时候已经创建了WebApplicationContext实例,而在这个函数中最重要的就是对这个实例进行进一步的补充初始化。
继续查看initServletBean()。父类FrameworkServlet覆盖了HttpServletBean中的initServletBean函数,如下:
1 protected final void initServletBean() throws ServletException { 2 getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'"); 3 if (logger.isInfoEnabled()) { 4 logger.info("Initializing Servlet '" + getServletName() + "'"); 5 } 6 long startTime = System.currentTimeMillis(); 7 8 try { 9 this.webApplicationContext = initWebApplicationContext(); 10 //设为子类覆盖 11 initFrameworkServlet(); 12 } 13 catch (ServletException | RuntimeException ex) { 14 logger.error("Context initialization failed", ex); 15 throw ex; 16 } 17 18 if (logger.isDebugEnabled()) { 19 String value = this.enableLoggingRequestDetails ? 20 "shown which may lead to unsafe logging of potentially sensitive data" : 21 "masked to prevent unsafe logging of potentially sensitive data"; 22 logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails + 23 "': request parameters and headers will be " + value); 24 } 25 26 if (logger.isInfoEnabled()) { 27 logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms"); 28 } 29 }
上面的函数设计了计时器来统计初始化的执行时间,而且提供了一个扩展方法initFrameworkServlet()用于子类的覆盖操作。而作为关键的初始化逻辑实现委托给了initWebApplicationContext()。
(三)WebApplicationContext的初始化
initWebApplicationContext函数的主要工作就是创建或者刷新WebApplicationContext实例并对servlet功能所使用额变量进行初始化。
1 protected WebApplicationContext initWebApplicationContext() { 2 WebApplicationContext rootContext = 3 WebApplicationContextUtils.getWebApplicationContext(getServletContext()); 4 WebApplicationContext wac = null; 5 6 if (this.webApplicationContext != null) { 7 // A context instance was injected at construction time -> use it 8 //context实例在构造函数中被注入 9 wac = this.webApplicationContext; 10 if (wac instanceof ConfigurableWebApplicationContext) { 11 ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac; 12 if (!cwac.isActive()) { 13 // The context has not yet been refreshed -> provide services such as 14 // setting the parent context, setting the application context id, etc 15 if (cwac.getParent() == null) { 16 // The context instance was injected without an explicit parent -> set 17 // the root application context (if any; may be null) as the parent 18 cwac.setParent(rootContext); 19 } 20 //刷新上下文环境 21 configureAndRefreshWebApplicationContext(cwac); 22 } 23 } 24 } 25 if (wac == null) { 26 // No context instance was injected at construction time -> see if one 27 // has been registered in the servlet context. If one exists, it is assumed 28 // that the parent context (if any) has already been set and that the 29 // user has performed any initialization such as setting the context id 30 //根据contextAttribute属性加载WebApplicationContext 31 wac = findWebApplicationContext(); 32 } 33 if (wac == null) { 34 // No context instance is defined for this servlet -> create a local one 35 wac = createWebApplicationContext(rootContext); 36 } 37 38 if (!this.refreshEventReceived) { 39 // Either the context is not a ConfigurableApplicationContext with refresh 40 // support or the context injected at construction time had already been 41 // refreshed -> trigger initial onRefresh manually here. 42 synchronized (this.onRefreshMonitor) { 43 onRefresh(wac); 44 } 45 } 46 47 if (this.publishContext) { 48 // Publish the context as a servlet context attribute. 49 String attrName = getServletContextAttributeName(); 50 getServletContext().setAttribute(attrName, wac); 51 } 52 53 return wac; 54 }
对应本函数中的初始化主要包含几个部分。
(1)寻找或创建对应的WebApplicationContext实例
WebApplicationContext的寻找和创建包括以下几个步骤。
(1.1)通过构造函数的注入进行初始化。
当进入initWebApplicationContext函数后通过判断this.webApplicationContext != null后,便可以确定this.webApplicationContext是否是通过构造函数来初始化的。可是有读者可能会有疑问,在initServletBean函数中明明是把创建好的实例记录在了this.webApplicationContext中:
this.webApplicationContext = initWebApplicationContext();
何以判断这个参数就是通过构造函数初始化,而不是通过上一次的函数返回值初始化的呢?如果存在这个问题,那么就是读者忽略了一个问题:在Web中包含SpringWeb的核心逻辑DispatcherServlet只可以被声明一次,在Spring中已经存在验证,所以这就确保了如果this.webApplicationContext != null,则可以直接判定this.webApplicationContext已经通过构造函数初始化了。
(1.2)通过contextAttribute进行初始化。
通过在web.xml文件中配置的servlet参数contextAttribut来 查找ServletContext中对应的属性,默认为WebApplicationContext.class.getName()+”.ROOT”,也就是在ContextLoaderListener加载时会创建WebApplicationContext实例,并将实例以WebApplicationContext.class.getName()+”.ROOT”为key放入ServletContext中,当然读者可以重写初始化逻辑使用自己创建的WebApplicationContext,并在servlet的配置中通过初始化参数contextAttribut指定可以key。
1 protected WebApplicationContext findWebApplicationContext() { 2 String attrName = getContextAttribute(); 3 if (attrName == null) { 4 return null; 5 } 6 WebApplicationContext wac = 7 WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName); 8 if (wac == null) { 9 throw new IllegalStateException("No WebApplicationContext found: initializer not registered?"); 10 } 11 return wac; 12 }
(1.3)重新创建WebApplicationContext实例。
如果通过以上两种方式并没有找到任何突破,那就没有办法了,只能在这里重新创建新的实例了。
1 protected WebApplicationContext createWebApplicationContext(@Nullable WebApplicationContext parent) { 2 return createWebApplicationContext((ApplicationContext) parent); 3 }
1 protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) { 2 //获取servlet的初始化参数contextClass,如果没有配置默认为XMLWebApplication.class 3 Class<?> contextClass = getContextClass(); 4 if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { 5 throw new ApplicationContextException( 6 "Fatal initialization error in servlet with name '" + getServletName() + 7 "': custom WebApplicationContext class [" + contextClass.getName() + 8 "] is not of type ConfigurableWebApplicationContext"); 9 } 10 //通过反射方式实例化contextClass 11 ConfigurableWebApplicationContext wac = 12 (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass); 13 14 wac.setEnvironment(getEnvironment()); 15 //parent为在ContextloaderListener中创建的实例,在ContextLoaderListener加载的时候初始化的WebApplicationContext类型实例 16 wac.setParent(parent); 17 String configLocation = getContextConfigLocation(); 18 if (configLocation != null) { 19 //获取contextConfigLocation属性,配置在servlet初始化参数中 20 wac.setConfigLocation(configLocation); 21 } 22 //初始化Spring环境包括加载配置文件等 23 configureAndRefreshWebApplicationContext(wac); 24 25 return wac; 26 }
(2)configureAndRefreshWebApplicationContext
无论是通过构造函数注入还是单独创建,都免不了会调用configureAndRefreshWebApplicationContext方法来对已经创建的WebApplicationContext实例进行配置及刷新,那么这个步骤又做了哪些工作呢?
1 protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) { 2 if (ObjectUtils.identityToString(wac).equals(wac.getId())) { 3 // The application context id is still set to its original default value 4 // -> assign a more useful id based on available information 5 if (this.contextId != null) { 6 wac.setId(this.contextId); 7 } 8 else { 9 // Generate default id... 10 wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + 11 ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName()); 12 } 13 } 14 15 wac.setServletContext(getServletContext()); 16 wac.setServletConfig(getServletConfig()); 17 wac.setNamespace(getNamespace()); 18 wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener())); 19 20 // The wac environment's #initPropertySources will be called in any case when the context 21 // is refreshed; do it eagerly here to ensure servlet property sources are in place for 22 // use in any post-processing or initialization that occurs below prior to #refresh 23 ConfigurableEnvironment env = wac.getEnvironment(); 24 if (env instanceof ConfigurableWebEnvironment) { 25 ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig()); 26 } 27 28 postProcessWebApplicationContext(wac); 29 applyInitializers(wac); 30 //加载配置文件及整合parent到wac 31 wac.refresh(); 32 }
(3)刷新
onRefresh是FrameworkServlet类中提供的模板方法,在其子类DispatcherServlet中进行了重写,主要用于刷新Spring在Web功能实现中所必须使用的全局变量。下面我们会介绍它们的初始化过程以及使用场景,而至于具体的使用细节会在稍后的章节中再做详细的介绍。
1 /** 2 * This implementation calls {@link #initStrategies}. 3 */ 4 @Override 5 protected void onRefresh(ApplicationContext context) { 6 initStrategies(context); 7 } 8 9 /** 10 * Initialize the strategy objects that this servlet uses. 11 * <p>May be overridden in subclasses in order to initialize further strategy objects. 12 */ 13 protected void initStrategies(ApplicationContext context) { 14 //(1)初始化MultipartResolver 15 initMultipartResolver(context); 16 //(2)初始化LocaleResolver 17 initLocaleResolver(context); 18 //(3)初始化ThemeResolver 19 initThemeResolver(context); 20 //(4)初始化HandlerMappings 21 initHandlerMappings(context); 22 //(5)初始化HandlerAdapters 23 initHandlerAdapters(context); 24 //(6)初始化HandlerExceptionResolvers 25 initHandlerExceptionResolvers(context); 26 //(7)初始化RequestToViewNameTranslator 27 initRequestToViewNameTranslator(context); 28 //(8)初始化ViewResolvers 29 initViewResolvers(context); 30 //(9)初始化FlashMapManager 31 initFlashMapManager(context); 32 }
(3.1)初始化MultipartResolver
在Spring中,MultipartResolver主要用来处理文件上传。默认情况下,Spring是没有Multipart处理的,因为一些开发者想要自己处理它们。如果想使用Spring的multipart,则需要在Web应用的上下文中添加multipart解析器。这样,每个请求就会被检查是否包含multipart。然而,如果请求中包含multipart,那么上下文中定义的MultipartResolver就会解析它,这样请求的multipart属性就会像其他属性一样被处理。常用配置如下:
1 <bean id="multipartResolver" class="org.Springframework.web.multipart.commons.CommonsMultipartResolver"> 2 <!--该属性用来配置可上传文件的最大byte数--> 3 <proterty name="maximumFileSize"><value>100000</value></proterty> 4 </bean>
当然,CommonsMultipartResolver还提供了其他功能用于帮助用户完成上传功能,有兴趣的读者可以进一步查看。
那么MultipartResolver就是在initMultipartResolver中被加入到DispatcherServlet中的。
1 private void initMultipartResolver(ApplicationContext context) { 2 try { 3 this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class); 4 if (logger.isTraceEnabled()) { 5 logger.trace("Detected " + this.multipartResolver); 6 } 7 else if (logger.isDebugEnabled()) { 8 logger.debug("Detected " + this.multipartResolver.getClass().getSimpleName()); 9 } 10 } 11 catch (NoSuchBeanDefinitionException ex) { 12 // Default is no multipart resolver. 13 this.multipartResolver = null; 14 if (logger.isTraceEnabled()) { 15 logger.trace("No MultipartResolver '" + MULTIPART_RESOLVER_BEAN_NAME + "' declared"); 16 } 17 } 18 }
因为之前的步骤已经完成了Spring中配置文件的解析,所以在这里只要在配置文件注册过都可以通过ApplicationContext提供的getBean方法来直接获取对应的bean,进而初始化MultipartResolver中的MultipartResolver变量。
(3.2)初始化LocalResolver
在Spring国际化配置中一共有3种使用方式。
1. 基于URL参数的配置
通过URL参数来控制国际化,比如你在页面上加一句<a href=”?locale=zh_CN”>简体中文</a>来控制项目中使用的国际化参数。而提供这个功能的就是AcceptHeaderLocalResolver,默认的参数名为local,注意大小写。里面放的就是你的提交参数,比如en_US、zh_CN之类的,具体配置如下:
<bean id=”localResolver” class=”org.Springframework.web.servlet.i18n.AcceptHeaderLocalResolver”/>
2. 基于session的配置
它通过检验用户会话中国预置的属性来解析区域。最常用的是根据用户本次会话过程中的语言设定决定语言种类(例如,用户登录时选择语言种类,则此次登录周期内统一使用此语言设定),如果会话属性不存在,它会根据accept-language HTTP头部确定默认区域。
<bean id=”localResolver” class=”org.Springframework.web.servlet.i18n.SessionLocaleResolver”>
3. 基于Cookie的国际化配置。
CookieLocalResolver用于通过浏览器的cookie设置取得Locale对象。这种策略再应用程序不支持会话或者状态必须保存在客户端时有用,配置如下:
<bean id=”localResolver” class=”org.Springframework.web.servlet.i18n.CookieLocalResolver”>
这3种方式都可以解决国际化的问题,但是,对于LocalResolver的使用基础是在DispatcherServlet中初始化的。
1 private void initLocaleResolver(ApplicationContext context) { 2 try { 3 this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class); 4 if (logger.isTraceEnabled()) { 5 logger.trace("Detected " + this.localeResolver); 6 } 7 else if (logger.isDebugEnabled()) { 8 logger.debug("Detected " + this.localeResolver.getClass().getSimpleName()); 9 } 10 } 11 catch (NoSuchBeanDefinitionException ex) { 12 // We need to use the default. 13 this.localeResolver = getDefaultStrategy(context, LocaleResolver.class); 14 if (logger.isTraceEnabled()) { 15 logger.trace("No LocaleResolver '" + LOCALE_RESOLVER_BEAN_NAME + 16 "': using default [" + this.localeResolver.getClass().getSimpleName() + "]"); 17 } 18 } 19 }
提取配置文件中设置的LocalResolver来初始化DispatcherServlet中的localeResolver属性。
(3.3)初始化ThemeResolver。
在Web开发中经常会遇到通过主题Theme来控制网页风格。这将进一步改善用户体验。简单的说,一个主题就是一组静态资源(比如样式表和图片)。它们可以影响程序的视觉效果。Spring中的主题功能和国际化功能非常类似。构成Spring主题功能主要包括如下内容。
1. 主题资源
org.Springframework.ui.context.ThemeSource是Spring中主题资源的接口。Spring的主题需要通过ThemeSource接口来实现存放主题信息的资源。
org.Springframework.ui.context.support.ResourceBundleThemeSource是ThemeSource接口默认实现类(也就是通过ResourceBundle资源的方式定义主题。)在Spring中的配置如下:
1 <bean id="themeSource" class="org.Springframework.ui.context.support.ResourceBundleThemeSource"> 2 <proterty name="basenamePrefix" value="com.text."></proterty> 3 </bean>
默认状态下是在类路径根目录下查找相应的资源文件。也可以通过basenamePrefix来制定。这样,DispatcherServlet就会在com.test包下查找资源文件。
2. 主题解析器。
ThemeSource定义了一些主题资源,那么不同的用户使用什么主题资源由谁定义呢?org.Springframework.web.servlet.ThemeResolver是主题解析器的接口,主题解析器的工作便是由它的子类来完成的。
对于主题解析器的子类主要有3个比较常用的实现。以主题文件summer.properties为例。
①FixedThemeResolver用于选择一个固定的主题。
1 <bean id="themeSource" class="org.Springframework.web.servlet.theme.FixedThemeResolver"> 2 <proterty name="defaultThemeName" value="summer"/> 3 </bean>
以上配置的作用是设置主题文件为summer.properties,在整个项目内固定不变。
②CookieThemeResolver用于实现用户所选的主题,以cookie的形式存放在客户端的机器上,配置如下:
1 <bean id="themeSource" class="org.Springframework.web.servlet.theme. CookieThemeResolver"> 2 <proterty name="defaultThemeName" value="summer"/> 3 </bean>
③SessionThemeResolver用于主题保存在用户的HTTP session中。
1 <bean id="themeSource" class="org.Springframework.web.servlet.theme. SessionThemeResolver"> 2 <proterty name="defaultThemeName" value="summer"/> 3 </bean>
以上配置用于设置主题名称,并且将该名称保存在用户的HttpSession中。
④AbstractThemeResolver是一个抽象类被SessionThemeResolver和FixedThemeResolver继承,用户也可以继承它来自定义主题解析器。
3. 拦截器。
如果需要根据用户请求来改变主题,那么Spring提供了一个已经实现的拦截器ThemeChangeInterceptor拦截器了,配置如下:
1 <bean id="themeChangeInterceptor" class="org.Springframework.web.servlet.theme.ThemeChangeInterceptor"> 2 <proterty name="paramName" value="themeName"/> 3 </bean>
其中设置用户请求参数名为themeName,即URL为?themeName=具体的主题名称。此外,还需要在handlerMapping中配置拦截器。当然需要在handleMapping中添加拦截器。
1 <property name="interceptors"> 2 <list> 3 <ref local="themeChangeInterceptor"/> 4 </list> 5 </property>
了解了主题文件的简单使用方式后,再来查看解析器的初始化工作。与其他变量的初始化工作相同,主题文件解析器的初始化工作并没有任何特别需要说明的地方。
1 private void initThemeResolver(ApplicationContext context) { 2 try { 3 this.themeResolver = context.getBean(THEME_RESOLVER_BEAN_NAME, ThemeResolver.class); 4 if (logger.isTraceEnabled()) { 5 logger.trace("Detected " + this.themeResolver); 6 } 7 else if (logger.isDebugEnabled()) { 8 logger.debug("Detected " + this.themeResolver.getClass().getSimpleName()); 9 } 10 } 11 catch (NoSuchBeanDefinitionException ex) { 12 // We need to use the default. 13 this.themeResolver = getDefaultStrategy(context, ThemeResolver.class); 14 if (logger.isTraceEnabled()) { 15 logger.trace("No ThemeResolver '" + THEME_RESOLVER_BEAN_NAME + 16 "': using default [" + this.themeResolver.getClass().getSimpleName() + "]"); 17 } 18 } 19 }
(3.4)初始化HandlerMapping
当客户端发出Request时DispatcherServlet会将Request提交给HandlerMapping,然后HandlerMapping根据WebApplicationContext的配置来回传给DispatcherServlet响应的Controller。
在基于SpringMVC的Web应用程序中,我们可以为DispatcherServlet提供多个HandlerMapping供其使用。DispatcherServlet在选用HandlerMapping的过程中,将根据我们所指定的一系列HandlerMapping的优先级进行排序,然后优先使用优先级在签名的HandlerMapping。如果当前的HandlerMapping能够返回可用的Handler,DispatcherServlet则使用当前返回的Handler进行Web请求的处理,而不再继续询问其他的HandlerMapping。否则,DispatcherServlet将继续按照各个HandlerMapping的优先级进行询问,直到获取一个可用的Handler为止。初始化配置如下:
1 private void initHandlerMappings(ApplicationContext context) { 2 this.handlerMappings = null; 3 4 if (this.detectAllHandlerMappings) { 5 // Find all HandlerMappings in the ApplicationContext, including ancestor contexts. 6 Map<String, HandlerMapping> matchingBeans = 7 BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); 8 if (!matchingBeans.isEmpty()) { 9 this.handlerMappings = new ArrayList<>(matchingBeans.values()); 10 // We keep HandlerMappings in sorted order. 11 AnnotationAwareOrderComparator.sort(this.handlerMappings); 12 } 13 } 14 else { 15 try { 16 HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class); 17 this.handlerMappings = Collections.singletonList(hm); 18 } 19 catch (NoSuchBeanDefinitionException ex) { 20 // Ignore, we'll add a default HandlerMapping later. 21 } 22 } 23 24 // Ensure we have at least one HandlerMapping, by registering 25 // a default HandlerMapping if no other mappings are found. 26 if (this.handlerMappings == null) { 27 this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class); 28 if (logger.isTraceEnabled()) { 29 logger.trace("No HandlerMappings declared for servlet '" + getServletName() + 30 "': using default strategies from DispatcherServlet.properties"); 31 } 32 } 33 }
默认情况下,SpringMVC将加载当前系统中所有实现了HandlerMapping接口的bean。如果只期望SpringMVC加载指定的handlerMapping时,可用修改web.xml中的DispatcherServlet的初始参数,将detectAllHandlerMappings的值设置为false:
1 <init-param> 2 <param-name>detectAllHandlerMappings</param-name> 3 <param-value>false</param-value> 4 </init-param>
此时,SpringMVC将查找名为“HandlerMapping”的bean,并作为当前系统中唯一的handlerMapping。如果没有定义HandlerMapping的话,则SpringMVC将按照org.Springframework.web.servlet.DispatcherServlet所在目录下的DispatcherServlet.properties中所定义的org.Springframework.web.servlet.HandlerMapping的内容来加载默认的handlerMapping(用户没有自定义Strategies的情况下)。
(3.5)初始化HandlerAdapters
从名字也能联想到这是一个典型的适配器模式的使用,在计算机编程中,适配器模式将一个类的接口适配成用户所期待的。使用适配器,可以使接口不兼容而无法在一起工作的类协同工作。做饭是将类自己的接口包裹在一个已存在的类中。那么在处理handler时为什么会使用适配器模式呢?回答这个问题我们首先要分析它的初始化逻辑。
1 private void initHandlerAdapters(ApplicationContext context) { 2 this.handlerAdapters = null; 3 4 if (this.detectAllHandlerAdapters) { 5 // Find all HandlerAdapters in the ApplicationContext, including ancestor contexts. 6 Map<String, HandlerAdapter> matchingBeans = 7 BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false); 8 if (!matchingBeans.isEmpty()) { 9 this.handlerAdapters = new ArrayList<>(matchingBeans.values()); 10 // We keep HandlerAdapters in sorted order. 11 AnnotationAwareOrderComparator.sort(this.handlerAdapters); 12 } 13 } 14 else { 15 try { 16 HandlerAdapter ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class); 17 this.handlerAdapters = Collections.singletonList(ha); 18 } 19 catch (NoSuchBeanDefinitionException ex) { 20 // Ignore, we'll add a default HandlerAdapter later. 21 } 22 } 23 24 // Ensure we have at least some HandlerAdapters, by registering 25 // default HandlerAdapters if no other adapters are found. 26 if (this.handlerAdapters == null) { 27 this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class); 28 if (logger.isTraceEnabled()) { 29 logger.trace("No HandlerAdapters declared for servlet '" + getServletName() + 30 "': using default strategies from DispatcherServlet.properties"); 31 } 32 } 33 }
同样在初始化的过程中涉及了一个变量detectAllHandlerAdapters,detectAllHandlerAdapters的作用和detectAllHandlerMapping类似,只不过作用对象为handlerAdapter。亦可通过如下配置来强制系统只加载bean name为“handlerAdapter”的handlerAdapter。
1 <init-param> 2 <param-name>detectAllHandlerAdapters</param-name> 3 <param-value>false</param-value> 4 </init-param>
如果无法找到对应的bean,那么系统会尝试加载默认的适配器。
1 @SuppressWarnings("unchecked") 2 protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) { 3 String key = strategyInterface.getName(); 4 String value = defaultStrategies.getProperty(key); 5 if (value != null) { 6 String[] classNames = StringUtils.commaDelimitedListToStringArray(value); 7 List<T> strategies = new ArrayList<>(classNames.length); 8 for (String className : classNames) { 9 try { 10 Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader()); 11 Object strategy = createDefaultStrategy(context, clazz); 12 strategies.add((T) strategy); 13 } 14 catch (ClassNotFoundException ex) { 15 throw new BeanInitializationException( 16 "Could not find DispatcherServlet's default strategy class [" + className + 17 "] for interface [" + key + "]", ex); 18 } 19 catch (LinkageError err) { 20 throw new BeanInitializationException( 21 "Unresolvable class definition for DispatcherServlet's default strategy class [" + 22 className + "] for interface [" + key + "]", err); 23 } 24 } 25 return strategies; 26 } 27 else { 28 return new LinkedList<>(); 29 } 30 }
在getDefaultStrategies函数中,Spring会尝试从defaultStrategies中加载对应的HandlerAdapter的属性。那么defaultStrategies是如何初始化的呢?
在当期类DispatcherServlet中存在这样一段初始化代码块:
1 static { 2 // Load default strategy implementations from properties file. 3 // This is currently strictly internal and not meant to be customized 4 // by application developers. 5 try { 6 ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class); 7 defaultStrategies = PropertiesLoaderUtils.loadProperties(resource); 8 } 9 catch (IOException ex) { 10 throw new IllegalStateException("Could not load '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage()); 11 } 12 }
在系统加载的时候,defaultStrategies根据当前路径DispatcherServlet.properties来初始化本身,查看DispatcherServlet.properties中对应于HandlerAdapter的属性:
1 org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\ 2 org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\ 3 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\ 4 org.springframework.web.servlet.function.support.HandlerFunctionAdapter
由此得知,如果程序开发人员没有再配置文件中定义自己的适配器,那么Spring会默认加载配置文件中的适配器。
作为总控制器的派遣器servlet通过处理器映射得到处理器后,会轮询处理器适配器模块,查找能够处理当前HTTP请求的处理器适配器的实现,处理器适配器模块根据处理器映射发挥的处理器类型,例如简单的控制器类型、注解控制器类型或者远程调用处理器类型,来选择某一个适当的处理器适配器的实现,从而适配当前的HTTP请求。
1. HTTP请求处理器适配器(HTTPRequestHandlerAdapter)。
HTTP请求处理器适配器仅仅支持对HTTP请求处理器的适配。它简单的将HTTP请求对象和响应对象传递给HTTP请求处理器的实现,它并不需要返回值。它主要应用在基于HTTP的远程调用的实现上。
2. 简单控制器适配器(SimpleControllerHandlerAdapter)。
这个实现类将HTTP请求适配到一个控制器的实现进行处理。这里控制器的实现是一个简单的控制器接口的实现。简单控制器处理器适配器被设计成一个框架类的实现,不需要被改写,客户化的业务逻辑通常是在控制器接口的实现类中实现的。
3. 注解方法处理器适配器(AnnotationMethodHandlerAdapte)。
这个类的实现是基于注解的实现,它需要结合注解方法映射和注解方法处理器协同工作。它通过解析声明在注解控制器的请求映射信息来解析相应的处理器方法来处理当前的HTTP请求。在处理的过程中,它通过反射来发现探测处理器方法的参数,调用处理器方法,并且映射返回值到模型和控制器对象,最后返回模型和控制器对象给作为主控制器的派遣器Servlet.
所以我们现在基本可以回答之前的问题了,Spring找那个所使用的Handler并没有任何特殊的联系,但是为了统一处理,Spring提供了不同情况下的适配器。
(3.6)初始化HandlerExceptionResolver。
基于HandlerExceptionResolver接口的异常处理,使用这种方式只需要实现resolveException方法,该方法返回一个ModelAndView对象,在方法内部对异常的类型进行判断,然后尝试生成对应的ModelAndView对象,如果该方法返回了null,则Spring会继续寻找其他的实现了HandlerExceptionResolver接口的bean。换句话说,Spring会搜索所有注册在其环境中的实现了HandlerExceptionResolver接口的bean,逐个执行,直到返回了一个ModelAndView对象。
1 import javax.servlet.http.HttpServletRequest; 2 import javax.servlet.http.HttpServletResponse; 3 4 import org.apache.commons.logging.Log; 5 import org.apache.commons.logging.LogFactory; 6 import org.Springframework.stereotype.Componet; 7 import org.Springframwwork.web.servlet.HandlerExceptionResolver; 8 import org.Springframework.web.servlet.ModelAndView; 9 10 @component 11 public class ExceptionHandler implements HandlerExceptionResolver{ 12 private static final Log = logs = LogFactory.getLog(ExceptionHandler.class); 13 14 @override 15 public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object obj, Exception exception){ 16 request.setAttribute("exception", exception.toString()); 17 request.setAttribute("exceptionStack", exception); 18 logs.error(exception.toString(), exception); 19 return new ModelAndView("error/exception"); 20 } 21 }
这个类必须声明到Spring中去,让Spring管理它,在Spring的配置文件applicationContext.xml中增加以下内容:
<bean id="exceptionHandler" class="com.test.exception.MyExceptionHandler"/>
初始化代码如下:
1 private void initHandlerExceptionResolvers(ApplicationContext context) { 2 this.handlerExceptionResolvers = null; 3 4 if (this.detectAllHandlerExceptionResolvers) { 5 // Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts. 6 Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils 7 .beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false); 8 if (!matchingBeans.isEmpty()) { 9 this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values()); 10 // We keep HandlerExceptionResolvers in sorted order. 11 AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers); 12 } 13 } 14 else { 15 try { 16 HandlerExceptionResolver her = 17 context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class); 18 this.handlerExceptionResolvers = Collections.singletonList(her); 19 } 20 catch (NoSuchBeanDefinitionException ex) { 21 // Ignore, no HandlerExceptionResolver is fine too. 22 } 23 } 24 25 // Ensure we have at least some HandlerExceptionResolvers, by registering 26 // default HandlerExceptionResolvers if no other resolvers are found. 27 if (this.handlerExceptionResolvers == null) { 28 this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class); 29 if (logger.isTraceEnabled()) { 30 logger.trace("No HandlerExceptionResolvers declared in servlet '" + getServletName() + 31 "': using default strategies from DispatcherServlet.properties"); 32 } 33 } 34 }
(3.7)初始化RequestToViewNameTranslator。
当Controller处理器方法没有返回一个View对象或逻辑视图名称,并且在该方法中没有直接往response的输出流里面写数据的时候,Spring就会采用约定好的方式提供一个逻辑视图名称。这个逻辑视图名称是通过Spring定义的org.Springframework.web.servlet.RequestToViewNameTranslator接口的getViewName方法来实现的,我们可以实现自己RequestToViewNameTranslator接口来约定好没有返回视图名称的时候如何确定视图名称。Spring已经给我们提供了一个它自己的实现,那就是org.Springframework.web.servlet.view.DefaultRequestToViewNameTranslator。
在介绍DefaultRequestToViewNameTranslator是如何约定视图名称之前,先来看一下它支持用户定义的属性。
prefix:前缀,表示约定好的视图名称需要加上的前缀,默认是空串。
suffix:后缀,表示约定好的视图名称需要加上的后缀,默认是空串。
separator:分隔符,默认是斜杠“/”。
stripLeadingSlash:如果首字符是分隔符,是否要去除,默认是true。
stripTrailingSlash:如果最后一个字符是分隔符,是否要去除,默认是true。
stripExtension:如果请求路径包含扩展名是否要去除,默认是true。
urlDecode:是否需要对URL编码,默认是ture。它会采用request指定的编码或者ISO-8859-1编码对URL进行解码。
当我们没有再SpringMVC的配置文件中手动的定义一个名为viewNameTranlator的Bean的时候,Spring就会为我们提供一个默认的viewNameTranslator,即DefaultRequestToViewNameTranslator。
接下来看一下,当Controller处理器方法没有返回逻辑视图名称时,DefaultRequestToViewNameTranslator是如何约定视图名称的。DefaultRequestToViewNameTranslator会获取到请求的URL,然后根据提供的属性做一些改造,把改造之后的结果作为视图名称返回。这里以请求路径http://localhost/app/test/index.html为例,来说明一下DefaultRequestToViewNameTranslator是如何工作的。该请求路径对应的请求URI为/test/index.html,我们来看以下几种情况,它分别对应的逻辑视图名称是什么。
prefix和suffix如果都存在,其他为默认值,那么对应返回的逻辑视图名称应该是prefixtest/indexsuffix。
stripLeadingSlash和stripExtension都为false,其他为默认,这时候对应的逻辑视图名称是/product/index.html。
都采用默认配置时,返回的逻辑视图名称应该是product/index。
如果逻辑视图名称跟请求路径相同或者相关关系都是一样的,那么我们就可以采用Spring为我们事先约定好的逻辑视图名称返回,这可以大大简化我们的开发工作,而以上功能实现的关键属性viewNameTranslator,则是在initRequestToViewNameTranslator中完成的。
1 private void initRequestToViewNameTranslator(ApplicationContext context) { 2 try { 3 this.viewNameTranslator = 4 context.getBean(REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME, RequestToViewNameTranslator.class); 5 if (logger.isTraceEnabled()) { 6 logger.trace("Detected " + this.viewNameTranslator.getClass().getSimpleName()); 7 } 8 else if (logger.isDebugEnabled()) { 9 logger.debug("Detected " + this.viewNameTranslator); 10 } 11 } 12 catch (NoSuchBeanDefinitionException ex) { 13 // We need to use the default. 14 this.viewNameTranslator = getDefaultStrategy(context, RequestToViewNameTranslator.class); 15 if (logger.isTraceEnabled()) { 16 logger.trace("No RequestToViewNameTranslator '" + REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME + 17 "': using default [" + this.viewNameTranslator.getClass().getSimpleName() + "]"); 18 } 19 } 20 }
(3.8)初始化ViewResolvers。
在SpringMVC中,当Controller将请求处理结果放入到ModelAndView中以后,DispatcherServlet会根据ModelAndView选择合适的视图进行渲染。那么在SpringMVC中是如何选择合适的View的呢?View对象是如何创建的呢?答案就在ViewResolver中。ViewResolver接口定义了resolverViewName方法,根据ViewName创建合适类型的View实现。
那么如何配置ViewResolver呢?在Spring中,ViewResolver作为Spring Bean存在,可以在Spring配置文件中进行配置,例如下面的代码,配置了JSP相关的viewResolver。
1 <bean class="org.Springframework.web.servlet.view.InternalResourceViewResolver"> 2 <property name="prefix" value="/WEB-INF/views/"/> 3 <property name="suffix" value=".jsp"/> 4 </bean>
ViewResolver属性的初始化工作在initViewResolver中完成。
1 private void initViewResolvers(ApplicationContext context) { 2 this.viewResolvers = null; 3 4 if (this.detectAllViewResolvers) { 5 // Find all ViewResolvers in the ApplicationContext, including ancestor contexts. 6 Map<String, ViewResolver> matchingBeans = 7 BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false); 8 if (!matchingBeans.isEmpty()) { 9 this.viewResolvers = new ArrayList<>(matchingBeans.values()); 10 // We keep ViewResolvers in sorted order. 11 AnnotationAwareOrderComparator.sort(this.viewResolvers); 12 } 13 } 14 else { 15 try { 16 ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class); 17 this.viewResolvers = Collections.singletonList(vr); 18 } 19 catch (NoSuchBeanDefinitionException ex) { 20 // Ignore, we'll add a default ViewResolver later. 21 } 22 } 23 24 // Ensure we have at least one ViewResolver, by registering 25 // a default ViewResolver if no other resolvers are found. 26 if (this.viewResolvers == null) { 27 this.viewResolvers = getDefaultStrategies(context, ViewResolver.class); 28 if (logger.isTraceEnabled()) { 29 logger.trace("No ViewResolvers declared for servlet '" + getServletName() + 30 "': using default strategies from DispatcherServlet.properties"); 31 } 32 } 33 }
(3.9)初始化FlashMapManager。
SpringMVC Flash attributes提供了一个请求存储属性,可供其他请求使用。在使用重定向时候非常必要,例如Post/Redirect/Get模式。Flash attribute在重定向之前暂存(就像存在session中)以便重定向之后还能使用,并立即删除。
SpringMVC有两个主要的抽象来支持Flash attributes。FlashMap用于保持Flash attributes,而FlashMapManager用于存储、检索、管理FlashMap实例。
Flash attributes支持默认开启(“on”)并不需要显示启用,它永远不会导致HTTP Session的创建。这两个FlashMap实例都可以通过静态方法RequestContextUtils从SpringMVC的任何位置访问。
FlashMapManager的初始化在initFlashMapManager中完成。
1 private void initFlashMapManager(ApplicationContext context) { 2 try { 3 this.flashMapManager = context.getBean(FLASH_MAP_MANAGER_BEAN_NAME, FlashMapManager.class); 4 if (logger.isTraceEnabled()) { 5 logger.trace("Detected " + this.flashMapManager.getClass().getSimpleName()); 6 } 7 else if (logger.isDebugEnabled()) { 8 logger.debug("Detected " + this.flashMapManager); 9 } 10 } 11 catch (NoSuchBeanDefinitionException ex) { 12 // We need to use the default. 13 this.flashMapManager = getDefaultStrategy(context, FlashMapManager.class); 14 if (logger.isTraceEnabled()) { 15 logger.trace("No FlashMapManager '" + FLASH_MAP_MANAGER_BEAN_NAME + 16 "': using default [" + this.flashMapManager.getClass().getSimpleName() + "]"); 17 } 18 } 19 }
四、 DispatcherServlet的处理逻辑
根据之前的示例,我们知道在HttpServlet类中分别提供了相应的服务方法,他们是doDelete()、doGet()、doOptions()、doGet()、doPut()和doTrace(),它会根据请求的不同形式将程序引导至对应的函数进行处理。这几个函数中最常用的函数无非就是doGet()和doGet(),那么我们就直接查看DispatcherServlet中对于这两个函数的实现逻辑(在其父类FrameworkServlet中实现)。
1 protected final void doGet(HttpServletRequest request, HttpServletResponse response) 2 throws ServletException, IOException { 3 4 processRequest(request, response); 5 }
1 protected final void doPost(HttpServletRequest request, HttpServletResponse response) 2 throws ServletException, IOException { 3 4 processRequest(request, response); 5 }
对于不同的方法,Spring并没有做特殊的处理,而是统一将程序再一次地引导至processRequest(request, response)中。
1 protected final void processRequest(HttpServletRequest request, HttpServletResponse response) 2 throws ServletException, IOException { 3 4 //记录当前时间,用于计算web请求的处理时间 5 long startTime = System.currentTimeMillis(); 6 Throwable failureCause = null; 7 8 LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext(); 9 LocaleContext localeContext = buildLocaleContext(request); 10 11 RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes(); 12 ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes); 13 14 WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); 15 asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor()); 16 17 initContextHolders(request, localeContext, requestAttributes); 18 19 try { 20 doService(request, response); 21 } 22 catch (ServletException | IOException ex) { 23 failureCause = ex; 24 throw ex; 25 } 26 catch (Throwable ex) { 27 failureCause = ex; 28 throw new NestedServletException("Request processing failed", ex); 29 } 30 31 finally { 32 resetContextHolders(request, previousLocaleContext, previousAttributes); 33 if (requestAttributes != null) { 34 requestAttributes.requestCompleted(); 35 } 36 logResult(request, response, failureCause, asyncManager); 37 publishRequestHandledEvent(request, response, startTime, failureCause); 38 } 39 }
函数中已经开始了对请求的处理,虽然吧细节转移到了doService函数中实现,但是我们不难看出处理请求前后所做的准备与处理工作。
(1)为了保证当前线程的LocalContext以及RequestAttribute可以在当前请求后还能恢复,提取当前线程的两个属性。
(2)根据当前request创建对应的LocalContext和RequestAttributes,并绑定到当前线程。
(3)委托给doService方法进一步处理。
(4)请求处理结束后恢复线程到原始状态。
(5)请求处理结束后无论成功与否发布事件通知。
继续查看doService方法。
1 protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception { 2 logRequest(request); 3 4 // Keep a snapshot of the request attributes in case of an include, 5 // to be able to restore the original attributes after the include. 6 Map<String, Object> attributesSnapshot = null; 7 if (WebUtils.isIncludeRequest(request)) { 8 attributesSnapshot = new HashMap<>(); 9 Enumeration<?> attrNames = request.getAttributeNames(); 10 while (attrNames.hasMoreElements()) { 11 String attrName = (String) attrNames.nextElement(); 12 if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) { 13 attributesSnapshot.put(attrName, request.getAttribute(attrName)); 14 } 15 } 16 } 17 18 // Make framework objects available to handlers and view objects. 19 request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext()); 20 request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver); 21 request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver); 22 request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource()); 23 24 if (this.flashMapManager != null) { 25 FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response); 26 if (inputFlashMap != null) { 27 request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap)); 28 } 29 request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap()); 30 request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager); 31 } 32 33 try { 34 doDispatch(request, response); 35 } 36 finally { 37 if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { 38 // Restore the original attribute snapshot, in case of an include. 39 if (attributesSnapshot != null) { 40 restoreAttributesAfterInclude(request, attributesSnapshot); 41 } 42 } 43 } 44 }
我们猜想对请求处理至少应该包括一些诸如寻找Handler并页面跳转之类的逻辑处理,但是,在doService中我们并没有看到想看到的逻辑,相反却同样是一些准备工作,但是这些准备工作确实必不可少的。Spring将已经初始化的功能辅助工具变量,比如localeResolver、ThemeResolver等设置在request属性中,而这些属性会在接下来额处理中派上用场。
经过层层的准备工作,终于在doDispatch函数中看到了完整的请求处理过程。
1 protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { 2 HttpServletRequest processedRequest = request; 3 HandlerExecutionChain mappedHandler = null; 4 boolean multipartRequestParsed = false; 5 6 WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); 7 8 try { 9 ModelAndView mv = null; 10 Exception dispatchException = null; 11 12 try { 13 //如果是MultipartContent类型的request则转换request为MultipartHttpServletRequest类型的request 14 processedRequest = checkMultipart(request); 15 multipartRequestParsed = (processedRequest != request); 16 17 // Determine handler for the current request. 18 //根据request信息寻找对应的Handler 19 mappedHandler = getHandler(processedRequest); 20 if (mappedHandler == null) { 21 //如果没有找到对应的handler则通过response反馈错误信息 22 noHandlerFound(processedRequest, response); 23 return; 24 } 25 26 // Determine handler adapter for the current request. 27 //根据当前的handler寻找对应的HandlerAdapter 28 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); 29 30 // Process last-modified header, if supported by the handler. 31 //如果当前handler支持last-modified头处理 32 String method = request.getMethod(); 33 boolean isGet = "GET".equals(method); 34 if (isGet || "HEAD".equals(method)) { 35 long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); 36 if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { 37 return; 38 } 39 } 40 41 if (!mappedHandler.applyPreHandle(processedRequest, response)) { 42 return; 43 } 44 45 // Actually invoke the handler. 46 //真正的激活handler并返回视图 47 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); 48 49 if (asyncManager.isConcurrentHandlingStarted()) { 50 return; 51 } 52 53 applyDefaultViewName(processedRequest, mv); 54 mappedHandler.applyPostHandle(processedRequest, response, mv); 55 } 56 catch (Exception ex) { 57 dispatchException = ex; 58 } 59 catch (Throwable err) { 60 // As of 4.3, we're processing Errors thrown from handler methods as well, 61 // making them available for @ExceptionHandler methods and other scenarios. 62 dispatchException = new NestedServletException("Handler dispatch failed", err); 63 } 64 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); 65 } 66 catch (Exception ex) { 67 triggerAfterCompletion(processedRequest, response, mappedHandler, ex); 68 } 69 catch (Throwable err) { 70 triggerAfterCompletion(processedRequest, response, mappedHandler, 71 new NestedServletException("Handler processing failed", err)); 72 } 73 finally { 74 if (asyncManager.isConcurrentHandlingStarted()) { 75 // Instead of postHandle and afterCompletion 76 if (mappedHandler != null) { 77 mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); 78 } 79 } 80 else { 81 // Clean up any resources used by a multipart request. 82 if (multipartRequestParsed) { 83 cleanupMultipart(processedRequest); 84 } 85 } 86 } 87 }
doDispatch函数中展示了Spring请求处理所涉及的主要逻辑,而我们之前设置在request中的各种辅助属性也都有被派上了用场。下面回顾一下逻辑处理的全过程。
(一)MultipartContent类型的request处理
对于请求的处理,Spring首先要考虑的是对于Multipart的处理,如果是MultipartContent类型的request,则转换request为MultipartHttpServletRequest类型的request。
1 protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException { 2 if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) { 3 if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) { 4 if (request.getDispatcherType().equals(DispatcherType.REQUEST)) { 5 logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter"); 6 } 7 } 8 else if (hasMultipartException(request)) { 9 logger.debug("Multipart resolution previously failed for current request - " + 10 "skipping re-resolution for undisturbed error rendering"); 11 } 12 else { 13 try { 14 return this.multipartResolver.resolveMultipart(request); 15 } 16 catch (MultipartException ex) { 17 if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) { 18 logger.debug("Multipart resolution failed for error dispatch", ex); 19 // Keep processing error dispatch with regular request handle below 20 } 21 else { 22 throw ex; 23 } 24 } 25 } 26 } 27 // If not returned before: return original request. 28 return request; 29 }
(二) 根据request信息寻找对应的handler
在Spring中最简单的映射处理器配置如下:
1 <bean id="simpleUrlMapping" class="org.Springframework.web.servlet.handler.SimpleUrlHandlerMapping"> 2 <property name="mapping"> 3 <props> 4 <prop key="/userlist.htm">userController</prop> 5 </props> 6 </property> 7 </bean>
在Spring的加载过程中,Spring会将类型为SimpleUrlHandlerMapping的实例加载到this.handlerMappings中,按照常理推断,根据request提取对应的Handler,无非就是提取当前实例中的userController,但是userController为继承自AbstractController类型实例,与HandlerExecutionChain并无任何关联,那么进一步是如何封装的呢?
1 protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { 2 if (this.handlerMappings != null) { 3 for (HandlerMapping mapping : this.handlerMappings) { 4 HandlerExecutionChain handler = mapping.getHandler(request); 5 if (handler != null) { 6 return handler; 7 } 8 } 9 } 10 return null; 11 }
在之前的内容中我们提过,在系统启动时Spring会将所有的映射类型的bean注册到this.handlerMappings变量中,所以此函数的目的就是遍历所有的HandlerMapping,并调用其getHandler方法进行封装处理。以SimpleUrlHandlerMapping为例查看其getHandler方法。
函数中首先会使用getHandlerInternal方法根据request信息获取到对应的Handler,如果以SimpleUrlHandlerMapping为例分析,那么我们推断此步骤提供的功能很可能就是根据URL找到匹配的Controller并返回,当然如果没有找到对应的Controller处理器那么程序会尝试去查找配置中的默认处理器,当然,当查找的Controller为String类型时,那就意味着返回的是配置的bean名称,需要根据bean名称查找对应的bean,最后,还要通过getHandlerExecutionChain方法对返回的Handler进行封装,以保证满足返回类型的匹配。下面详细分析这个过程。
1. 根据request查找对应的Handler
首先从根据request查找对应的Handler开始分析。
1 protected Object getHandlerInternal(HttpServletRequest request) throws Exception { 2 //截取用于匹配的url有效路径 3 String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); 4 request.setAttribute(LOOKUP_PATH, lookupPath); 5 //根据路径寻找Handler 6 Object handler = lookupHandler(lookupPath, request); 7 if (handler == null) { 8 // We need to care for the default handler directly, since we need to 9 // expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well. 10 Object rawHandler = null; 11 if ("/".equals(lookupPath)) { 12 //如果请求的路径仅仅是“/”,那么使用RootHandler进行处理 13 rawHandler = getRootHandler(); 14 } 15 if (rawHandler == null) { 16 //无法找到handler则使用默认的handler 17 rawHandler = getDefaultHandler(); 18 } 19 if (rawHandler != null) { 20 //根据beanName获取对应的bean 21 // Bean name or resolved handler? 22 if (rawHandler instanceof String) { 23 String handlerName = (String) rawHandler; 24 rawHandler = obtainApplicationContext().getBean(handlerName); 25 } 26 //模板方法 27 validateHandler(rawHandler, request); 28 handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null); 29 } 30 } 31 return handler; 32 }
1 protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception { 2 // Direct match? 3 //直接匹配情况的处理 4 Object handler = this.handlerMap.get(urlPath); 5 if (handler != null) { 6 // Bean name or resolved handler? 7 if (handler instanceof String) { 8 String handlerName = (String) handler; 9 handler = obtainApplicationContext().getBean(handlerName); 10 } 11 validateHandler(handler, request); 12 return buildPathExposingHandler(handler, urlPath, urlPath, null); 13 } 14 15 // Pattern match? 16 //通配符匹配的处理 17 List<String> matchingPatterns = new ArrayList<>(); 18 for (String registeredPattern : this.handlerMap.keySet()) { 19 if (getPathMatcher().match(registeredPattern, urlPath)) { 20 matchingPatterns.add(registeredPattern); 21 } 22 else if (useTrailingSlashMatch()) { 23 if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern + "/", urlPath)) { 24 matchingPatterns.add(registeredPattern + "/"); 25 } 26 } 27 } 28 29 String bestMatch = null; 30 Comparator<String> patternComparator = getPathMatcher().getPatternComparator(urlPath); 31 if (!matchingPatterns.isEmpty()) { 32 matchingPatterns.sort(patternComparator); 33 if (logger.isTraceEnabled() && matchingPatterns.size() > 1) { 34 logger.trace("Matching patterns " + matchingPatterns); 35 } 36 bestMatch = matchingPatterns.get(0); 37 } 38 if (bestMatch != null) { 39 handler = this.handlerMap.get(bestMatch); 40 if (handler == null) { 41 if (bestMatch.endsWith("/")) { 42 handler = this.handlerMap.get(bestMatch.substring(0, bestMatch.length() - 1)); 43 } 44 if (handler == null) { 45 throw new IllegalStateException( 46 "Could not find handler for best pattern match [" + bestMatch + "]"); 47 } 48 } 49 // Bean name or resolved handler? 50 if (handler instanceof String) { 51 String handlerName = (String) handler; 52 handler = obtainApplicationContext().getBean(handlerName); 53 } 54 validateHandler(handler, request); 55 String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestMatch, urlPath); 56 57 // There might be multiple 'best patterns', let's make sure we have the correct URI template variables 58 // for all of them 59 Map<String, String> uriTemplateVariables = new LinkedHashMap<>(); 60 for (String matchingPattern : matchingPatterns) { 61 if (patternComparator.compare(bestMatch, matchingPattern) == 0) { 62 Map<String, String> vars = getPathMatcher().extractUriTemplateVariables(matchingPattern, urlPath); 63 Map<String, String> decodedVars = getUrlPathHelper().decodePathVariables(request, vars); 64 uriTemplateVariables.putAll(decodedVars); 65 } 66 } 67 if (logger.isTraceEnabled() && uriTemplateVariables.size() > 0) { 68 logger.trace("URI variables " + uriTemplateVariables); 69 } 70 return buildPathExposingHandler(handler, bestMatch, pathWithinMapping, uriTemplateVariables); 71 } 72 73 // No handler found... 74 return null; 75 }
根据URL获取对应Handler的匹配规则代码实现起来虽然很长,但是并不难理解,考虑了直接匹配与通配符两种情况。其中要提及的是buildPathExposingHandler函数,它将Handler封装成了HandlerExecutionChain类型。
1 protected Object buildPathExposingHandler(Object rawHandler, String bestMatchingPattern, 2 String pathWithinMapping, @Nullable Map<String, String> uriTemplateVariables) { 3 4 HandlerExecutionChain chain = new HandlerExecutionChain(rawHandler); 5 chain.addInterceptor(new PathExposingHandlerInterceptor(bestMatchingPattern, pathWithinMapping)); 6 if (!CollectionUtils.isEmpty(uriTemplateVariables)) { 7 chain.addInterceptor(new UriTemplateVariablesHandlerInterceptor(uriTemplateVariables)); 8 } 9 return chain; 10 }
在函数中我们看到了通过将Handler以参数形式传入,并构建HandlerExcutionChain类型实例,加入了两个拦截器。此时我们似乎已经了解了Spring这样大费周折的目的。链处理机制,是Spring中非常常用的处理方式,是AOP中的重要组成部分,可以方便地对目标对象进行扩展及拦截,这是非常优秀的设计。
2. 加入拦截器到执行链
将配置中的对应拦截器加入到执行链找中,以保证这些拦截器可以有效的作用于目标对象。
1 protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { 2 if (this.handlerMappings != null) { 3 for (HandlerMapping mapping : this.handlerMappings) { 4 HandlerExecutionChain handler = mapping.getHandler(request); 5 if (handler != null) { 6 return handler; 7 } 8 } 9 } 10 return null; 11 }
(三)没找到对应的Handler的错误处理
每个请求都应该对应着一个Handler,因为每个请求都会在后台有相应的逻辑对应,而逻辑的实现就是在Handler中,所以一旦遇到没有找到Handler的情况(正常情况下如果没有URL匹配的Handler,开发人员可以设置默认的Handler来处理请求,但是如果默认请求也未设置就会出现Handler为空的情况),就只能通过response向用户返回错误信息。
1 protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception { 2 if (pageNotFoundLogger.isWarnEnabled()) { 3 pageNotFoundLogger.warn("No mapping for " + request.getMethod() + " " + getRequestUri(request)); 4 } 5 if (this.throwExceptionIfNoHandlerFound) { 6 throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request), 7 new ServletServerHttpRequest(request).getHeaders()); 8 } 9 else { 10 response.sendError(HttpServletResponse.SC_NOT_FOUND); 11 } 12 }
(四)根据当前的Handler寻找对应的HandlerAdapter
在WebApplicationContext的初始化过程中我们讨论了HandlerAdapter的初始化,了解了在默认的情况下普通的Web请求会交给SimpleControllerHandlerAdapter去处理。下面我们以SimpleControllerHandlerAdapter为例来分析获取适配器的逻辑。
1 protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { 2 if (this.handlerAdapters != null) { 3 for (HandlerAdapter adapter : this.handlerAdapters) { 4 if (adapter.supports(handler)) { 5 return adapter; 6 } 7 } 8 } 9 throw new ServletException("No adapter for handler [" + handler + 10 "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler"); 11 }
通过上面的函数我们了解到,对于获取适配器的逻辑无非就是遍历所有的适配器来选择合适的适配器并返回它,而某个适配器是否适用于当前的Handler逻辑被封装在具体的适配器中。进一步查看SimpleControllerHandlerAdapter的supports方法。
1 @Override 2 public boolean supports(Object handler) { 3 return (handler instanceof Controller); 4 }
分析到这里,一切已经明了,SimpleControllerHandlerAdapter就是用于处理普通的Web请求的,而且对于SpringMVC来说,我们会把逻辑封装至Controller的子类中,例如我们之前的引导示例UserController就是继承自AbstractController,而AbstractController实现了Controller接口。
(五)缓存处理
在研究Spring对缓存处理的功能支持前,我们先来了解一个概念:Last-Modified缓存机制。
(1)在客户端第一次输入URL时,服务器会返回内容和状态码200,表示请求成功,同时会添加一个“Last-Modified”的响应头,表示此文件在服务器上的最后更新时间,例如,“Last-Modified:Wed, 14 Mar 2012 10:22:42 GMT”表示最后更新时间为(2012-03-14 10:22)。
(2)客户端第二次请求此URL时,客户端会向服务器发送请求头”If-Modified-Since”。询问服务器该时间之后当前请求内容是否有被修改过,如” If-Modified-Since: Wed, 14 Mar 2012 10:22:42 GMT”,如果服务器端的内容没有变化,则自动返回HTTP 304状态码(只要响应头,内容为空,这样就节省了网络带宽)。
Spring提供的对Last-Modified机制的支持,只需要实现LastModified接口,如下所示:
1 public class HelloWorldLastModifiedCacheController extends AbstractController implements LastModified{ 2 private long lastModified; 3 protected ModelAndView handleRequestInternal(HttpServletRequest req, HttpServletResponse resp) throws Exception{ 4 //点击后再次请求当前页面 5 resp.getWrite().write("<a href=''>this</a>"); 6 return null; 7 } 8 9 public long getLastModified(HttpServletRequest request){ 10 if(lastModified == 0L){ 11 //第一次或者逻辑有变化的时候,应该重新返回内容最新修改的时间戳 12 lastModified = System.currentTimeMillis(); 13 } 14 return lastModified; 15 } 16 }
HelloWorldLastModifiedCacheController只需要实现LastModified接口的getLastModified方法,保证当内容发生改变时返回最新的修改时间即可。
Spring判断是否过期,通过判断请求”If-Modified-Since”是否大于等于当前的getLastModified方法的时间戳。如果是,则认为没有修改。上面的Controller与普通的Controller并无太大的差别,声明如下:
<bean name="/helloLastModified" class="com.test.controller.HelloWorldLastModifiedCacheController"/>
(六)HandlerInterceptor的处理
Servlet API定义的servlet过滤器可以在servelt处理每个Web请求的前后分别对它进行前置处理和后置处理。此外,有些时候,你可能只想处理由某些SpringMVC处理程序处理的Web请求,并在这些处理程序返回的模型属性被传递到视图之前,对它们进行一些操作。
SpringMVC允许你通过处理拦截Web请求,进行前置处理和后置处理。处理拦截是在Spring的Web应用程序上下文中配置的,因此它们可以利用各种容器特性,并引用容器中声明的任何bean。处理拦截是针对特殊的处理程序映射进行注册的,因此它只拦截通过这些处理程序映射的请求。每个处理拦截都必须实现HandlerInterceptor接口,它包含三个需要你实现的回调方法:preHandle()、postHandle()和afterCompletion()。第一个和第二个方法分别是在处理程序处理请求之前和之后被调用的。第二个方法还允许访问返回的ModelAndView对象,因此可以在里面操作之前和之后被调用。第二个方法还允许访问返回的ModelAndView对象,因此可以在它里面操作模型属性。最后一个方法是在所有请求处理完成之后被调用的(如视图呈现之后),以下是HandlerInterceptor的简单实现:
1 public class MyTestInterceptor implements HandlerInterceptor{ 2 public boolean preHandler(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{ 3 long startTime = System.currentTimeMillis(); 4 request.setAttribute("startTime", startTime); 5 return true; 6 } 7 8 public void postHandler(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception{ 9 long startTime = (Long)request.getAtribute("startTime"); 10 request.removeAttribute("startTime"); 11 long endTime = System.currentTimeMillis(); 12 modelAndView.addObject("handlingTime", endTime-startTime); 13 } 14 15 public void afterCompletion((HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception{ 16 } 17 }
在这个拦截器的preHandler()方法中,你记录了起始时间,并将它保存到请求属性中。这个方法应该返回true,运行DispatcherServlet继续处理请求。否则DispatcherServlet会认为这个方法已经处理了请求,直接将响应返回给用户。然后,在postHandler()方法中,从请求属性中加载起始时间,并将它与当前时间进行比较。你可以计算总的持续时间,然后把这个时间添加到模型中,传递给视图。最后afterCompletion()方法无事可做,空着就可以了。
(七)逻辑处理
对于逻辑处理其实是通过适配器中转调用Handler并返回视图的,对应代码:
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
同样,还是以引导示例为基础进行处理逻辑分析,之前分析过,对于普通的Web请求,Spring默认使用SimpleControllerHandlerAdapter类进行处理,我们进入SimpleControllerHandlerAdapter类的handle方法如下:
1 public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) 2 throws Exception { 3 4 return ((Controller) handler).handleRequest(request, response); 5 }
但是回顾引导示例中的UserController,我们的逻辑是写在handleRequestInternal函数中而不是handleRequest函数,所以我们还需要进一步分析这期间所包含的处理流程。
(八)异常视图的处理
有时候系统运行过程中出现异常,而我们并不希望就此中断对用户的服务。而是至少告知客户当前系统在处理逻辑的过程中出现了异常,甚至告知他们因为什么原因导致的。Spring中的异常处理机制会帮我们完成这个工作。其实,这里Spring主要的工作就是将逻辑引导至HandlerExceptionResolver类的resolveException方法,而HandlerExceptionResolver的使用。我们再讲解WebApplicationContext的初始化的时候已经介绍过了。
1 protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception{ 2 //Check resistered handlerExceptionResolvers... 3 ModelAndView exMv = null; 4 for (HandlerExceptionResolver handlerExceptionResolver:this.handlerExceptionResolvers){ 5 exMv = handlerExceptionResolver.resolveException(request, response, handler, ex); 6 if (exMv != null){ 7 break; 8 } 9 } 10 if (exMv != null) { 11 if (exMv.isEmpty()){ 12 return null; 13 } 14 //We might still need view name translation for a plain error model.. 15 if (!exMv.hasView()){ 16 exMv.setViewName(getDefaultViewName(request)); 17 } 18 if (logger.isDebugEnabled()){ 19 logger.debug("Handler execution resulted in exception - forwarding to resolved error view:" + exMv, ex); 20 } 21 WebUtils.exposeErrorRequestAttributes(request, ex, getServletName()); 22 return exMv; 23 } 24 throw ex; 25 }
(九)根据视图跳转页面
无论一个系统还是一个站点,最重要的工作都是和用户进行交互,用户操作系统后无论下发的命令是否成功都需要给用户一个反馈,以便于用户进行下一步的判断。所以,在逻辑处理的最后一定会涉及一个页面的跳转的问题。
1 protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { 2 // Determine locale for request and apply it to the response 3 Local local = this.localResolver.resolveLocale(request); 4 response.setLocale(locale); 5 6 View view; 7 if (mv.isReference()){ 8 //We need to resolve the view name 9 view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request); 10 if (view == null) { 11 throw new ServletException("Could not resolve view with name'" + mv.getViewName() + "'in servlet with name '" + getServletName() + "'"); 12 } 13 } 14 else { 15 //No need to lookup:the ModelAndView object contains the actual View objects. 16 view = mv.getView(); 17 if (view == null) { 18 throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " + "View object in servlet with name '" + getServletName() + "'" ); 19 } 20 } 21 22 // Delegate to the View object for rendering 23 if (logger.isDebugEnable()){ 24 logger.debug("Rendering view [" + view "] in DispatcherServlet with name '" + getServletName() + "'"); 25 } 26 view.render(mv.getModelInternal(), request, response); 27 }
1. 解析视图名称
在上文中我们提到DispatcherServlet会根据ModelAndView选择合适的视图来进行渲染,而这一个功能是在resolveViewName函数中完成的。
1 protected View resolveViewName(String viewName, @Nullable Map<String, Object> model, 2 Locale locale, HttpServletRequest request) throws Exception { 3 4 if (this.viewResolvers != null) { 5 for (ViewResolver viewResolver : this.viewResolvers) { 6 View view = viewResolver.resolveViewName(viewName, locale); 7 if (view != null) { 8 return view; 9 } 10 } 11 } 12 return null; 13 }
我们以org.Springframework.web.servlet.view.InternalResourceViewResolver为例分析ViewResolver逻辑的解析过程,其中resolveViewName是在其父类AbstractCachingViewResolver(package org.springframework.web.servlet.view)中完成的。
1 public View resolveViewName(String viewName, Locale locale) throws Exception { 2 if (!isCache()) { 3 //不存在缓存的情况下直接创建视图 4 return createView(viewName, locale); 5 } 6 else { 7 //直接从缓存中提取 8 Object cacheKey = getCacheKey(viewName, locale); 9 View view = this.viewAccessCache.get(cacheKey); 10 if (view == null) { 11 synchronized (this.viewCreationCache) { 12 view = this.viewCreationCache.get(cacheKey); 13 if (view == null) { 14 // Ask the subclass to create the View object. 15 view = createView(viewName, locale); 16 if (view == null && this.cacheUnresolved) { 17 view = UNRESOLVED_VIEW; 18 } 19 if (view != null && this.cacheFilter.filter(view, viewName, locale)) { 20 this.viewAccessCache.put(cacheKey, view); 21 this.viewCreationCache.put(cacheKey, view); 22 } 23 } 24 } 25 } 26 else { 27 if (logger.isTraceEnabled()) { 28 logger.trace(formatKey(cacheKey) + "served from cache"); 29 } 30 } 31 return (view != UNRESOLVED_VIEW ? view : null); 32 } 33 }
在父类URLBasedViewResolver中重写了createView函数。
1 protected View createView(String viewName, Locale locale) throws Exception { 2 // If this resolver is not supposed to handle the given view, 3 // return null to pass on to the next resolver in the chain. 4 //如果当前解析器不支持当前给定的view,如viewName为空等情况 5 if (!canHandle(viewName, locale)) { 6 return null; 7 } 8 9 // Check for special "redirect:" prefix. 10 //处理前缀为redirect:xx的情况 11 if (viewName.startsWith(REDIRECT_URL_PREFIX)) { 12 String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length()); 13 RedirectView view = new RedirectView(redirectUrl, 14 isRedirectContextRelative(), isRedirectHttp10Compatible()); 15 String[] hosts = getRedirectHosts(); 16 if (hosts != null) { 17 view.setHosts(hosts); 18 } 19 return applyLifecycleMethods(REDIRECT_URL_PREFIX, view); 20 } 21 22 // Check for special "forward:" prefix. 23 //处理前缀为forward:xx的情况 24 if (viewName.startsWith(FORWARD_URL_PREFIX)) { 25 String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length()); 26 InternalResourceView view = new InternalResourceView(forwardUrl); 27 return applyLifecycleMethods(FORWARD_URL_PREFIX, view); 28 } 29 30 // Else fall back to superclass implementation: calling loadView. 31 return super.createView(viewName, locale); 32 }
1 protected AbstractUrlBasedView buildView(String viewName) throws Exception { 2 Class<?> viewClass = getViewClass(); 3 Assert.state(viewClass != null, "No view class"); 4 5 AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass); 6 //添加前缀和后缀 7 view.setUrl(getPrefix() + viewName + getSuffix()); 8 view.setAttributesMap(getAttributesMap()); 9 10 String contentType = getContentType(); 11 if (contentType != null) { 12 //设置ContenTye 13 view.setContentType(contentType); 14 } 15 16 String requestContextAttribute = getRequestContextAttribute(); 17 if (requestContextAttribute != null) { 18 view.setRequestContextAttribute(requestContextAttribute); 19 } 20 21 Boolean exposePathVariables = getExposePathVariables(); 22 if (exposePathVariables != null) { 23 view.setExposePathVariables(exposePathVariables); 24 } 25 Boolean exposeContextBeansAsAttributes = getExposeContextBeansAsAttributes(); 26 if (exposeContextBeansAsAttributes != null) { 27 view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes); 28 } 29 String[] exposedContextBeanNames = getExposedContextBeanNames(); 30 if (exposedContextBeanNames != null) { 31 view.setExposedContextBeanNames(exposedContextBeanNames); 32 } 33 34 return view; 35 }
通读以上代码,我们发现对于InternalResourceViewResolver所提供的解析功能主要考虑到了以下几个方面的处理:
基于效率的考虑,提供了缓存的支持。
提供了对redirect:xx和forward:xx前缀的支持。
添加了前缀及后缀,并向View中加入了必须的属性设置。
2. 页面跳转
当通过viewName解析到对应的View后,就可以进一步处理跳转逻辑了。
1 protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { 2 // Determine locale for request and apply it to the response. 3 Locale locale = 4 (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale()); 5 response.setLocale(locale); 6 7 View view; 8 String viewName = mv.getViewName(); 9 if (viewName != null) { 10 // We need to resolve the view name. 11 view = resolveViewName(viewName, mv.getModelInternal(), locale, request); 12 if (view == null) { 13 throw new ServletException("Could not resolve view with name '" + mv.getViewName() + 14 "' in servlet with name '" + getServletName() + "'"); 15 } 16 } 17 else { 18 // No need to lookup: the ModelAndView object contains the actual View object. 19 view = mv.getView(); 20 if (view == null) { 21 throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " + 22 "View object in servlet with name '" + getServletName() + "'"); 23 } 24 } 25 26 // Delegate to the View object for rendering. 27 if (logger.isTraceEnabled()) { 28 logger.trace("Rendering view [" + view + "] "); 29 } 30 try { 31 if (mv.getStatus() != null) { 32 response.setStatus(mv.getStatus().value()); 33 } 34 view.render(mv.getModelInternal(), request, response); 35 } 36 catch (Exception ex) { 37 if (logger.isDebugEnabled()) { 38 logger.debug("Error rendering view [" + view + "]", ex); 39 } 40 throw ex; 41 } 42 }
本文摘自《Spring源码深度解析》SpringMVC,作者:郝佳。本文代码基于的Spring版本为5.2.4.BUILD-SNAPSHOT,和原书代码部分会略有不同。
拓展阅读:
Spring框架之beans源码完全解析
Spring框架之AOP源码完全解析
Spring框架之jms源码完全解析
Spring框架之spring-web http源码完全解析
Spring框架之spring-web web源码完全解析