• 1.SpringMVC设计理念与DispatcherServlet


     

    SpringMVC作为Struts2之后异军突起的一个表现层框架,正越来越流行,相信javaee的开发者们就算没使用过SpringMVC,也应该对其略有耳闻。我试图通过对SpringMVC的设计思想和源码实现的剖析,从抽象意义上的设计层面和实现意义上的代码层面两个方面,逐一揭开SpringMVC神秘的面纱,本文的代码,都是基于Spring的 3.1.3RELEASE版本。

    任何一个框架,都有自己特定的适用领域,框架的设计和实现,必定是为了应付该领域内许多通用的,烦琐的、基础的工作而生。SpringMVC作为一个表现层框架,也必须直面Web开发领域中表现层中的几大课题,并给出自己的回答:

    • URL到框架的映射。

    • http请求参数绑定

    • http响应的生成和输出

    这三大课题,组成一个完整的web请求流程,每一个部分都具有非常广阔的外延。SpringMVC框架对这些课题的回答又是什么呢?

    学习一个框架,首要的是要先领会它的设计思想。从抽象、从全局上来审视这个框架。其中最具有参考价值的,就是这个框架所定义的核心接口。核心接口定义了框架的骨架,也在最抽象的意义上表达了框架的设计思想。

    下面我以一个web请求流程为载体,依次介绍SpringMVC的核心接口和类。

    用户在浏览器中,输入了http://www.xxxx.com/aaa/bbb.ccc的地址,回车后,浏览器发起一个http请求。请求到达你的服务器后,首先会被SpringMVC注册在web.xml中的前端转发器DispatcherServlet接收,DispatcherServlet是一个标准的Servlet,它的作用是接受和转发web请求到内部框架处理单元。

    下面看一下第一个出现在你面前的核心接口,它是在org.springframework.web.servlet包中定义的HandlerMapping接口:

    package org.springframework.web.servlet;

    import javax.servlet.http.HttpServletRequest;

    public interface HandlerMapping {

    String PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE = HandlerMapping.class.getName() + ".pathWithinHandlerMapping";

    String BEST_MATCHING_PATTERN_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingPattern";

    String INTROSPECT_TYPE_LEVEL_MAPPING = HandlerMapping.class.getName() + ".introspectTypeLevelMapping";

    String URI_TEMPLATE_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".uriTemplateVariables";

    String PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE = HandlerMapping.class.getName() + ".producibleMediaTypes";

    HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;

    }

    为了阅读方便,我去掉了源码中的注释,但是我强烈建议你一定要记得去阅读它,这样你才能从框架的设计者口中得到最准确的关于这个类或者接口的设计说明。类中定义的几个常量,我们先不去管它。关键在于这个接口中唯一的方法:

    HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;

    这个方法就算对于一个java初学者来说,也很容易理解:它只有一个类型为HttpServletRequest的参数,throws Exception的声明表示它不处理任何类型的异常,HandlerExecutionChain是它的返回类型。

    回到DispatcherServlet的处理流程,当DispatcherServlet接收到web请求后,由标准Servlet类处理方法doGet或者doPost,经过几次转发后,最终注册在DispatcherServlet类中的HandlerMapping实现类组成的一个List(有点拗口)会在一个循环中被遍历。以该web请求的HttpServletRequest对象为参数,依次调用其getHandler方法,第一个不为null的调用结果,将被返回。DispatcherServlet类中的这个遍历方法不长,贴一下,让大家有更直观的了解。

    /**
    * Return the HandlerExecutionChain for this request.
    * <p>Tries all handler mappings in order.
    * @param request current HTTP request
    * @return the HandlerExecutionChain, or <code>null</code> if no handler could be found
    */
    protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    //遍历处理器映射器
           for (HandlerMapping hm : this.handlerMappings) {
    if (logger.isTraceEnabled()) {
    logger.trace(
    "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
    }
               //获取HandlerExecutionChain(处理执行链),并返回
    HandlerExecutionChain handler = hm.getHandler(request);
    if (handler != null) {
    return handler;
    }
    }
    return null;
    }

    是的,第一步处理就这么简单的完成了。一个web请求经过处理后,会得到一个HandlerExecutionChain对象,这就是SpringMVC对URl映射给出的回答。需要留意的是,HandlerMapping接口的getHandler方法参数是HttpServletRequest,这意味着,HandlerMapping的实现类可以利用HttpServletRequest中的 所有信息来做出这个HandlerExecutionChain对象的生成”决策“。这包括,请求头、url路径、cookie、session、参数等等一切你从一个web请求中可以得到的任何东西(最常用的是url路径)。

    SpirngMVC的第一个扩展点,就出现在这里。我们可以编写任意的HandlerMapping实现类,依据任何策略来决定一个web请求到HandlerExecutionChain对象的生成。可以说,从第一个核心接口的声明开始,SpringMVC就把自己的灵活性和野心暴露无疑:哥玩的就是”Open-Closed“。

    HandlerExecutionChain这个类,就是我们下一个要了解的核心类。从名字可以直观的看得出,这个对象是一个执行链的封装。熟悉Struts2的都知道,Action对象也是被层层拦截器包装,这里可以做个类比,说明SpringMVC确实是吸收了Struts2的部分设计思想。

    HandlerExecutionChain类的代码不长,它定义在org.springframework.web.servlet包中,为了更直观的理解,先上代码。

    package org.springframework.web.servlet;

    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;

    import org.springframework.util.CollectionUtils;

    public class HandlerExecutionChain {

    private final Object handler;

    private HandlerInterceptor[] interceptors;

    private List<HandlerInterceptor> interceptorList;

    public HandlerExecutionChain(Object handler) {
    this(handler, null);
    }

    public HandlerExecutionChain(Object handler, HandlerInterceptor[] interceptors) {
    if (handler instanceof HandlerExecutionChain) {
    HandlerExecutionChain originalChain = (HandlerExecutionChain) handler;
    this.handler = originalChain.getHandler();
    this.interceptorList = new ArrayList<HandlerInterceptor>();
    CollectionUtils.mergeArrayIntoCollection(originalChain.getInterceptors(), this.interceptorList);
    CollectionUtils.mergeArrayIntoCollection(interceptors, this.interceptorList);
    }
    else {
    this.handler = handler;
    this.interceptors = interceptors;
    }
    }

    public Object getHandler() {
    return this.handler;
    }

    public void addInterceptor(HandlerInterceptor interceptor) {
    initInterceptorList();
    this.interceptorList.add(interceptor);
    }

    public void addInterceptors(HandlerInterceptor[] interceptors) {
    if (interceptors != null) {
    initInterceptorList();
    this.interceptorList.addAll(Arrays.asList(interceptors));
    }
    }

    private void initInterceptorList() {
    if (this.interceptorList == null) {
    this.interceptorList = new ArrayList<HandlerInterceptor>();
    }
    if (this.interceptors != null) {
    this.interceptorList.addAll(Arrays.asList(this.interceptors));
    this.interceptors = null;
    }
    }

    public HandlerInterceptor[] getInterceptors() {
    if (this.interceptors == null && this.interceptorList != null) {
    this.interceptors = this.interceptorList.toArray(new HandlerInterceptor[this.interceptorList.size()]);
    }
    return this.interceptors;
    }

    @Override
    public String toString() {
    if (this.handler == null) {
    return "HandlerExecutionChain with no handler";
    }
    StringBuilder sb = new StringBuilder();
    sb.append("HandlerExecutionChain with handler [").append(this.handler).append("]");
    if (!CollectionUtils.isEmpty(this.interceptorList)) {
    sb.append(" and ").append(this.interceptorList.size()).append(" interceptor");
    if (this.interceptorList.size() > 1) {
    sb.append("s");
    }
    }
    return sb.toString();
    }

    }

    乱七八糟一大堆,相信你也没全看完,也没必要全看。其实只需要看两行足矣。

    private final Object handler; //处理器,真正处理业务

    private HandlerInterceptor[] interceptors; //拦截器数组

    不出我们所料,一个实质执行对象,还有一堆拦截器。这不就是Struts2中的实现么,SpringMVC没有避嫌,还是采用了这种封装。得到HandlerExecutionChain这个执行链(execution chain)之后,下一步的处理将围绕其展开。

    HandlerInterceptor也是SpringMVC的核心接口,定义如下:

    package org.springframework.web.servlet;

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;

    public interface HandlerInterceptor {

    boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
       throws Exception;

    void postHandle(
    HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
    throws Exception;

    void afterCompletion(
    HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
    throws Exception;

    }

    至此,HandlerExecutionChain整个执行脉络也就清楚了:在真正调用其handler对象前,HandlerInterceptor接口实现类组成的数组将会被遍历,其preHandle方法会被依次调用,然后真正的handler对象将被调用。

    handler对象被调用后,就生成了需要的响应数据,在将处理结果写到HttpServletResponse对象之前(SpringMVC称为渲染视图),其postHandle方法会被依次调用。视图渲染完成后,最后afterCompletion方法会被依次调用,整个web请求的处理过程就结束了。

    在一个处理对象执行之前,之后利用拦截器做文章,这已经成为一种经典的框架设计套路。Struts2中的拦截器会做诸如参数绑定这类复杂的工作,那么SpringMVC的拦截器具体做些什么呢?我们暂且不关心,虽然这是很重要的细节,但细节毕竟是细节,我们先来理解更重要的东西。

    HandlerInterceptor,是SpringMVC的第二个扩展点的暴露,通过自定义拦截器,我们可以在一个请求被真正处理之前、请求被处理但还没输出到响应中、请求已经被输出到响应中之后这三个时间点去做任何我们想要做的事情。Struts2框架的成功,就是源于这种拦截器的设计,SpringMVC吸收了这种设计思想,并推陈出新,更合理的划分了三个不同的时间点,从而给web请求处理这个流程,提供了更大的扩展性。

    这个HandlerExecutionChain类中以Object引用所声明的handler对象,到底是个什么东东?它是怎么被调用的?

    回答这些问题之前,先看SpringMVC中的又一个核心接口,HandlerAdapter:

    package org.springframework.web.servlet;

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;

    public interface HandlerAdapter {

    boolean supports(Object handler);

    ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;

    long getLastModified(HttpServletRequest request, Object handler);

    }

    在DispatcherServlet中,除了HandlerMapping实现类的列表,同样也注册了一个HandlerAdapter实现类组成的列表,有代码为证。

    /** List of HandlerMappings used by this servlet */
    private List<HandlerMapping> handlerMappings;

    /** List of HandlerAdapters used by this servlet */
    private List<HandlerAdapter> handlerAdapters;

    接下来,我们再以DispatcherServlet类中另外一段代码来回答上述的问题:

    /**
    	 * Return the HandlerAdapter for this handler object.
    	 * @param handler the handler object to find an adapter for
    	 * @throws ServletException if no HandlerAdapter can be found for the handler. This is a fatal error.
    	 */
    	protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
    		for (HandlerAdapter ha : this.handlerAdapters) {
    			if (logger.isTraceEnabled()) {
    				logger.trace("Testing handler adapter [" + ha + "]");
    			}
    			if (ha.supports(handler)) {
    				return ha;
    			}
    		}
    		throw new ServletException("No adapter for handler [" + handler +
    				"]: Does your handler implement a supported interface like Controller?");
    	}
    
    

    这段代码已经很明显了,HandlerExecutionChain中的handler对象会被作为参数传递进去,在DispatcherServlet类中注册的HandlerAdapter实现类列表会被遍历,然后返回第一个supports方法返回true的HandlerAdapter对象,用这个HandlerAdapter实现类中的handle方法处理handler对象,并返回ModelAndView这个包含了视图和数据的对象。HandlerAdapter就是SpringMVC提供的第三个扩展点,你可以提供自己的实现类来处理handler对象。

    ModelAndView对象的代码就不贴了,它是SpringMVC中对视图和数据的一个聚合类。其中的视图,就是由SpringMVC的最后一个核心接口View所抽象:

    package org.springframework.web.servlet;
     
    import java.util.Map;
     
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
     
    public interface View {
     
    	String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";
     
    	String PATH_VARIABLES = View.class.getName() + ".pathVariables";
     
    	String getContentType();
     
    	void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
     
    }
    
    

    所有的数据,最后会作为一个Map对象传递到View实现类中的render方法,调用这个render方法,就完成了视图到响应的渲染。这个View实现类,就是来自HandlerAdapter中的handle方法的返回结果。当然从ModelAndView到真正的View实现类有一个解析的过程,ModelAndView中可以有真正的视图对象,也可以只是有一个视图的名字,SpringMVC会负责将视图名称解析为真正的视图对象。

    至此,我们了解了一个典型的完整的web请求在SpringMVC中的处理过程和其中涉及到的核心类和接口。

    在一个典型的SpringMVC调用中,HandlerExecutionChain中封装handler对象就是用@Controller注解标识的类的一个实例,根据类级别和方法级别的@RequestMapping注解,由默认注册的DefaultAnnotationHandlerMapping(3.1.3中更新为RequestMappingHandlerMapping类,但是为了向后兼容,DefaultAnnotationHandlerMapping也可以使用)生成HandlerExecutionChain对象,再由AnnotationMethodHandlerAdapter(3.1.3中更新为RequestMappingHandlerAdapter类,但是为了向后兼容,AnnotationMethodHandlerAdapter也可以使用)来执行这个HandlerExecutionChain对象,生成最终的ModelAndView对象后,再由具体的View对象的render方法渲染视图。

    可以看到,作为一个表现层框架,SpringMVC没有像Struts2那样激进,并没有采用和Web容器完全解耦的设计思想,而是以原生的Servlet框架对象为依托,通过合理的抽象,制定了严谨的的处理流程。这样做的结果是,执行效率比Struts2要高,灵活性也上升了一个层次。

    上一篇文章《SpringMVC源码剖析(一)- 从抽象和接口说起》中,我介绍了一次典型的SpringMVC请求处理过程中,相继粉墨登场的各种核心类和接口。我刻意忽略了源码中的处理细节,只列出最简单的类甚至是接口类,目的就是让大家先从最高层次的抽象意义上来审视SpringMVC这个框架;我也刻意将SpringMVC和Struts2做对比,目的是让大家看到,SpringMVC究竟吸取了Sturts2设计思想中的哪些精华,又弥补了它的哪些遗憾。

    DispatcherServlet作为SpringMVC的核心之中的核心类,再怎么强调它的重要性也不为过。SpringMVC所有的核心类和接口,都密集地出现在DispatcherServlet的源码中,SpringMVC源码剖析,很大程度上可以说也是在剖析DispatcherServlet这一个类。这一篇文章里,我先说几点关于DispatcherServlet的前世今生,希望能帮助你更好的理解它。

    1.对扩展开放,对修改封闭

    SpringMVC是一个基于著名的Open-Closed,即开闭原则进行设计的框架。在Spring官方文档里面关于SpringMVC的介绍开宗明义地进行了说明:

    A key design principle in Spring Web MVC and in Spring in general is the “Open for extension,closed for modification” principle.
    

    开闭原则是一个很宽泛的原则,具体体现到DispatcherServlet的源码中,我们可以大致摸得到一些线索:

    • 类中所有的变量声明,几乎都以接口的形式给出,并没有绑定在具体的实现类上。

    • 使用模版方法模式,在父类中对基础行为进行定义,让子类实现模版方法扩展行为。

    其中第一点,在一个框架的设计中尤为重要,也是贯彻开闭原则最重要的一点。因为当你通过一些高层次的接口或者抽象类,将一个类完成的逻辑或流程编写完成后(具体点说,是通过一个接口的引用调用接口方法),整个逻辑或流程的功能就被确实的在源码中固定下来了。可是这时,这些接口或抽象类的具体实现者是谁,还没有固定!这就给了你的系统或框架近乎无限的扩展性,因为你可以任意安排和实现这些类。

    我认为,面向对象设计的精髓,是对现实世界中“行为和契约”的描述。这个“行为和契约”,体现在接口和抽象类的方法声明中。软件设计师要用面向对象的眼光去观察和抽象这个世界中的事物,这里的事物可以是一些商业逻辑、可以是一些处理流程,然后用高层次的接口去描述这些行为和契约。当你在越抽象的层次上将这些行为和契约描述清楚后,你所设计的系统就是越符合开闭原则的。

    SpringMVC框架在面向对象设计上,做出了绝佳的示范。它通过高度抽象的接口,描述出了一次请求处理的流程,从而让整个框架从一开始就是符合开闭原则的。同时它也提供了这些接口的一系列默认实现类,让你不需要很复杂的配置,就能很好的使用SpringMVC进行开发。抽象的确是个利器,但是框架绝不能运行在空中楼阁中,SpringMVC提供的的这一系列默认实现类必须要有容身之所。聪明的你可能早已想到:Spring IOC容器。这就引出了我要说的第二点。

    2.配置元素的对象化

    所有的框架,都需要有这样一个功能,叫做:配置元素的对象化。因为几乎所有的框架,都将配置元素集中到外部的xml配置文件中,然后在框架的初始化流程中,对这些配置文件进行解析,再变成java世界中的一个个对象供框架使用,这整个过程,可以被称为配置元素的对象化。为什么要有配置文件呢?这个问题的回答也是很简单,因为没有人会想要使用一个配置散布在框架中各个java类源码里面的框架。框架也不允许使用者这样子做,因为框架在发布的时候,提供的是一个个jar包文件,jar包内是已经编译好的class文件。配置文件由使用者外部提供,框架对它进行解析,使用者能得到集中配置的好处,框架也乐于这样子,可以说是合情合理。

    那么作为Spring产品族的新成员,SpringMVC在设计的时候,相信设计者们不做它想,这一个“配置元素的对象化”功能既然不可避免,那么使用Spring IOC容器,通过bean配置文件来配置SpringMVC,绝对是不二之选。不可能像Struts2一样,内部再搞一个别的容器,因为Spring容器本身已经是被高度设计,而且已经在java世界获得巨大成功。从推广的角度上来说,如果对spring容器的所有知识,都可以完整的应用到SpringMVC,那么对于开发者无疑是一个极大的吸引力。

    剩下的问题就只有:到底该如何将Spring容器和SpringMVC的初始化过程整合起来呢?

    答案就是WebApplicationContext接口,更具体点说,是XmlWebApplicationContext这个Spring上下文实现类。SpringMVC也使用了这一个为了将Spring容器和Web环境整合而特意设计的Spring上下文类。我们打开WebApplicationContext的源码:

    package org.springframework.web.context;
    import javax.servlet.ServletContext;
    import org.springframework.context.ApplicationContext;
    public interface WebApplicationContext extends ApplicationContext {
     
    	String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
     
    	String SCOPE_REQUEST = "request";
     
    	String SCOPE_SESSION = "session";
     
    	String SCOPE_GLOBAL_SESSION = "globalSession";
     
    	String SCOPE_APPLICATION = "application";
     
    	String SERVLET_CONTEXT_BEAN_NAME = "servletContext";
     
    	String CONTEXT_PARAMETERS_BEAN_NAME = "contextParameters";
     
    	String CONTEXT_ATTRIBUTES_BEAN_NAME = "contextAttributes";
     
    	ServletContext getServletContext();
     
    }
    
    

    发现它是继承于ApplicationContext这个普通Spring容器所使用的上下文接口类,除了一些常量的声明,只多了一个可以获取到ServletContext的getServletContext()方法。回到上面提到的“行为和契约的描述”上,我们可以大胆的断言,Spring容器和Web环境的整合,是在ServletContext上做文章。

    打开所有使用了Spring的Web项目的web.xml文件,必定有这样一段配置:

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

    ContextLoaderListener实现了ServletContextListener接口,在Servlet容器启动的时候,会初始化一个WebApplicationContext的实现类,并将其作为ServletContext的一个属性设置到Servlet环境中,摘抄源码如下:

    servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
    

    WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE的值,在上面WebApplicationContext的源码中的第一个常量中就被声明,是WebApplicationContext.class.getName() + ".ROOT",更直接一点,它是“org.springframework.web.context.WebApplicationContext.ROOT”。ContextLoaderListener所初始化的这个Spring容器上下文,被称为根上下文。

    SpringMVC在DispatcherServlet的初始化过程中,同样会初始化一个WebApplicationContext的实现类,作为自己独有的上下文,这个独有的上下文,会将上面的根上下文作为自己的父上下文,来存放SpringMVC的配置元素,然后同样作为ServletContext的一个属性,被设置到ServletContext中,只不过它的key就稍微有点不同,key和具体的DispatcherServlet注册在web.xml文件中的名字有关,从这一点也决定了,我们可以在web.xml文件中注册多个DispatcherServlet,因为Servlet容器中注册的Servlet名字肯定不一样,设置到Servlet环境中的key也肯定不同。

    由于在Spring容器中,子上下文可以访问到所有父上下文中的信息,而父上下文访问不到子上下文的信息,这个根上下文,就很适合作为多个子上下文配置的集中点。以官方文档中的图来说明:

    3.前端控制器

    前端控制器,即所谓的Front Controller,体现的是设计模式中的前端控制器模式。前端控制器处理所有从用户过来的请求。所有用户的请求都要通过前端控制器。SpringMVC框架和其他请求驱动的表示层框架一样,也是围绕一个将请求分发到相应控制器的核心Servlet来设计的。DispatcherServlet和其他框架中的Servlet不一样的地方在于,它和Spring容器无缝整合在了一起,因此你可以在SpringMVC中使用Spring容器所有的特性。

    DispatcherServlet这个前端控制器,在SpringMVC中的作用,以官方文档中的配图来说明:

    整个流程可以被大致描述为:一个http请求到达服务器,被DispatcherServlet接收。DispatcherServlet将请求委派给合适的处理器Controller,此时处理控制权到达Controller对象。Controller内部完成请求的数据模型的创建和业务逻辑的处理,然后再将填充了数据后的模型即model和控制权一并交还给DispatcherServlet,委派DispatcherServlet来渲染响应。DispatcherServlet再将这些数据和适当的数据模版视图结合,向Response输出响应。

    可以看到Model-View-Controller这三样东西协同合作,共同体现出MVC的设计理念,三个层次可以分别独立演化,整个系统架构又清晰又简洁。这是SpringMVC为我们描述的美好愿景,后面我们也将看到,SpringMVC为了实现这一承诺,究竟做出了什么样的努力

  • 相关阅读:
    Git 多人协作开发的过程
    常见Http状态码大全
    网络请求之get post
    从前端面试过程中总结的一些经验
    HTML5新增标签
    前端小知识点--class命名规范
    前段工作第一天新知识点---handlebars.js和Seajs
    Javascript模式消息框--alert()、confirm()和prompt()的区别与用法
    用Margin还是用Padding
    Array实例对象的方法小结
  • 原文地址:https://www.cnblogs.com/itxiaok/p/10356691.html
Copyright © 2020-2023  润新知