看透SpringMVC源代码分析与实践(三)

一、SpringMVC之初体验

  1. SpringMVC最简单的配置

       (1)在web.xml中配置Servlet

         (2)   创建Controller和view

         (3)   创建SpringMVC的xml配置文件

 

二、创建SpringMVC之器

SpringMVC中核心Servlet的继承结构如下图所示:

看透SpringMVC源代码分析与实践(三)_第1张图片

GenericServlet和HttpServlet在JAVA中,剩下的HttpServletBean、FrameworkServlet和DispatcherServlet是SpringMVC中的。

SpringMVC中Servlet一共三个层次,分别是HttpServletBean、FrameworkServlet和DispatcherServlet。HttpServletBean直接继承Java中的HttpServlet,其作用是Servlet中配置的参数设置到相应的属性;FrameworkServlet初始化了webApplicationContext;DispatcherServlet初始化了自身的九个组件。

 

三、SpringMVC之用

1.HttpServletBean

HttpServletBean主要参与了创建工作,并没有涉及请求的处理。

2.FrameworkServlet

Servlet的处理过程:首先是从Servlet接口的service方法开始,然后再HttpServlet的service方法中根据请求的类型不同将请求路由到了doGet、doHead、doPost、doPut、doDelete、doOptions和doTrace七个方法,并且做了doHead、doOptions和doTrace的默认实现,其中doHead调用doGet,然后返回只有header没有body的response。

在FrameworkServlet中重写了service、doGet、doPost、doPut、doDelete、doOptions、doTrace方法(除了doHead的所有处理请求的方法)。在service方法中添加了对PATCH类型请求的处理,其他类型的请求直接交给了父类进行处理;doOptions和doTrace方法可以通过设置dispatchOptionsRequest和dispatchTraceRequest参数决定是由自己处理还是由父类处理(默认都是交给父类处理,doOptions会在父类的处理结果中添加PATCH类型);doGet,doPost,doPut和doDelete都是自己处理。所有需要自己处理的请求都交给processRequest方法进行统一处理。

ProcessRequest是FrameworkServlet类在处理请求中最核心的方法。processRequest中的核心语句是doService,这是一个模板方法,在DispatcherServlet中具体实现。除了异步请求和调用doService方法具体处理请求,processRequest自己主要做了两件事:

  • 对LocaleContext和RequestAttributes的设置和恢复。
  • 处理完后发布了ServletRequestHandlerEvent的消息。

首先看下LocaleContext和RequestAttributes。LocaleContext里面存放着Locale(也就是本地化信息,如zh-cn等),RequestAttributes是spring的一个接口,通过它可以get/set/removeAttribute,根据scope参数判断操作request还是session。这里具体使用的是ServletRequestAttributes类,在ServletRequestAttributes里面还封装了request、response和session,而且都提供了get方法,可以直接获取。

当publishEvents设置为true时,请求处理结束后就会发出这个消息,无论请求处理成功与否都会发布。publishEvents可以在web.xml文件中配置SpringMVC的Servlet时配置,默认为true。我们可以通过监听这个事件来做一些事情,如记录日志。

3.DispatcherServlet

DispatcherServlet是SpringMVC最核心的类,整个处理过程的顶层设计都在这里面。

DispatcherServlet里面执行处理的入口方法应该是doService,不过doService并没有直接进行处理,而是交给了doDispatch进行具体的处理,在调用doDispatch之前,doService做了一些事:首先判断是不是include请求,如果是则对request的Attribute做个快照备份,等doDispatch处理完之后进行还原,在昨晚快照后又对request设置了一些属性。

对request设置的属性中,前面4个属性,webApplicationContext、localeResolver、themeResolver和themeSource在之后介绍的handler和view中需要使用。后面三个属性都和flashMap相关,主要用于Redirect转发时参数的传递,比如,为了避免重复提交表单,可以在处理完post请求后redirect到一个get的请求,这样即使用户刷新也不会有重复提交的问题。不过这里有个问题,前面的post请求时提交订单,提交完后redirect到一个现实订单的页面,显然在显示订单的页面需要知道订单的一些信息,但redirect本身是没有传递参数的功能的,按普通的模式如果想传递参数,只能将其写入url中,但是url有长度限制,另外有些场景中我们想传递的参数还不想暴露在url里,这时就可以用flashMap来进行传递了,我们只需要在redirect之前将需要传递的参数写入OUTPUT_FLASH_MAP_ATTRIBUTE,如下:

((FlashMap)((ServletRequestAttributes) (RequestContextHolder.getRequestAttributes())).getRequest.getAttribute(DispatcherServlet.OUTPUT_FLASH_MAP_ATTRIBUTE)).put(“name”,”张三丰”);

这样在redirect之后的handler中spring就会自动将其设置到model里(先设置到INPUT_FLASH_MAP_ATTRIBUTE属性里,然后再放到model里)。当然这样操作还是有点麻烦,spring还给我们提供了更加简单的操作方法,我们只需要在handler方法的参数中定义RedirectAttributes类型的变量,然后把需要保存的属性设置到里面就行,之后的事情spring自动完成。RedirectAttributes有两种设置参数的方法addAttribute(key,value)和addFlashAttribute(key,value),用第一个方法设置的参数会拼接到url中,第二个方法设置的参数就是用我们刚才所讲的flashMap保存的。比如,一个提交订单的Controller可以这样写:

@RequestMapping(Value=”/submit”,method=RequestMethod.POST)

public String submit(RedirectAttributes attr) throws IOException{

((FlashMap)((ServletRequestAttributes) (RequestContextHolder.getRequestAttributes())).getRequest.getAttribute(DispatcherServlet.OUTPUT_FLASH_MAP_ATTRIBUTE)).put(“name”,”张三丰”);

attr.addFlashAttribute(“ordersId”,”xxx”);

attr.addAttribute(“local”,”zh-cn”);

return “redirect:showorders”;

}

 

@RequestMapping(value=”/showorders”,method=RequestMethod.POST)

public String showOrders(Model model) throws IOExcepion{

doSomething……

return “orders”;

}

这里分别使用三种方法来传递redirect参数:

  • 使用前面讲过的RequestContextHolder获取到request,并从其属性中拿到outputFlashMap,然后将属性放进去,当然request可以直接写到参数里让SpringMVC给设置进来。
  • 通过传入的attr参数的addFlashAttribute方法设置,这样也可以保存到outputFlashMap中,和第一中方法效果一样。
  • 通过传入的attr参数的addAttribute方法设置,这样设置的参数不会保存到FlashMap,而是会拼接到url中。

从Request获取outputFlashMap除了直接获取DispatcherServlet.OUTPUT_FLASH_MAP

_ATTRIBUTE属性,还可以使用RequestContextUtils来操作:RequestContextUtils.getOutputF

lashMap(request),这样也可以得到outputFlashMap,其实它内部还是从Request的属性获取的。

当用户提交http://xxx/submit请求后浏览器地址栏会自动跳转到http://xxx/showorders?

Local=zh-cn链接,而在showOrders的model里会存在[“name”,”张三丰”]和[“ordersId”,”xxx”]两个属性,而且对客户端是透明的,用户并不知道。

这就是flashMap的用法,inputFlashMap用于保存上次请求中转发过来的属性,outputFlashMap用于保存本次请求需要转发的属性,FlashMapManager用于管理他们。

doDispatch方法也非常整洁,从顶层设计了整个请求处理过程。doDispatch中最核心的代码只要4句,他们的任务分别是:一、根据request找到Handler;二、根据Hanler找到对应的HandlerAdapter;三、用HandlerAdapter处理Handler;四、调用processDispatchResult方法处理上面处理之后的结果(包含找到View并渲染输出给用户),对应的代码如下:

mappedHandler = this.getHandler(processedRequest);

HandlerAdapter err1 = this.getHandlerAdapter(mappedHandler.getHandler());

err = err1.handle(processedRequest, response, mappedHandler.getHandler());

this.processDispatchResult(processedRequest, response, mappedHandler, err,

(Exception)dispatchException);

这里有三个概念:HandlerMapping、Handler和HandlerAdapter。

Handler:也就是处理器,它直接对应着MVC的C也就是Controller层,它的具体表现形式有很多,可以是类,也可以是方法。我们标注了@RequestMapping的所有方法都可以看成一个Handler。只要实际处理请求就可以是Handler。

HandlerMapping:是用来查找Handler的,在SpringMVC中会处理很多请求,每个请求都需要一个Handler来处理,具体接收到一个请求后使用哪个Handler来处理呢?这就是HandlerMapping要做的事。

HandlerAdapter:就是一个适配器。因为SpringMVC的Handler可以是任意的形式,只要能够处理请求就OK,但是Servlet需要的处理方法的结构确实固定的,都是以request和response为参数的方法。HandlerAdapter要做的事就是让固定的Servlet处理方法调用灵活的Handler来进行处理。

通俗的解释就是Handler是用来干活的工具,HandlerMapping用于根据需要干的活找到相应的工具,HandlerAdapter就是使用工具干活的人。SpringMVC中也一样,在九大组件中HandlerAdapter也是最复杂的。

另外View和ViewResolver的原理与Handler和HandlerMapping的原理类似。View是用来展示数据的,而ViewResolver用来查找View。通俗地讲就是干完活后需要写报告,写报告又需要模板,View就是所需要的模板,模板就像公文里边的格式,内容就是Model里边的数据,ViewResolver就是用来选择使用哪个模板的。

使用HandlerMapping找到干活的Handler,找到使用Handler的HandlerAdapter,让HandlerAdapter使用Handler干活。干完活将结果通过View展示给用户。

 

4.doDispatch结构

doDispatch大体可以分为两部分:处理请求和渲染页面。开头部分先定义了几个变量,如下:

  • HttpServletRequest processedRequest:实际处理时所用的request,如果不是上传请求则直接使用接收到的请求,如果是上传请求则封装为上传的请求。
  • HandlerExecutionChain mappedHandler:处理请求的处理器链(包含处理器和对应的Interceptor)。
  • boolean multipartRequestParsed:是不是上传请求的标志。
  • ModelAndView mv:封装Model和View的容器,此变量在整个SpringMVC处理的过程中承担着非常重要角色。
  • Exception dispatchException:处理请求过程中抛出的异常。需要注意的是它并不包含渲染过程抛出的异常。

doDispatch中首先检查是不是上传请求,如果是上传请求,则将request请求转换为MultipartHttpServletRequest,并将multipartRequestParsed标志设置为true。其中使用到了MultipartResolver。

然后通过getHandler方法获取Handler处理器链,其中使用到了HandlerMapping,返回值为HandlerExecutionChain类型,其中包含着与当前request相匹配的Interceptor和Handler。

方法结构非常简单,HandlerMapping在后面详解,HandlerExecutionChain的类型类似于前面Tomcat讲过的Pipeline,Interceptor和Handler相当于那里边的Value和BaseValue,执行时先依次执行Interceptor的preHandler方法,最后执行Handler,返回的时候按相反的顺序执行Interceptor的postHandler方法。就好像要去一个地方,Interceptor要经过的收费站,Handler是目的地,去的时候和返回的时候都要经过加油站,但两次所经过的顺序是相反的。

接下来是处理GET、HEAD请求的Last-Modified。当浏览器第一次跟服务器请求资源(GET、Head请求)时,服务器在返回的请求头里面会包含一个Last-Modified的属性,代表本资源最后是什么时候修改的。在浏览器以后发送请求时会同时发送之前接收到的Last-Modified,服务器接收到待Last-Modified的请求后会用其值和自己实际资源的最后修改时间做对比,如果资源过期了则返回新的资源(同时返回新的Last-Modified),否则直接返回304状态码表示资源未过期,浏览器直接使用之前缓存的结果。

接下来依次调用相应Interceptor的preHandler。

处理完Interceptor的preHandler后就到了此方法最关键的地方——让HandlerAdapter使用Handler处理请求,Controller就是在这个地方执行的。

Handler处理完请求后,如果需要异步处理,则直接返回,如果不需要异步处理,当view为空时(如Handler返回值为void),设置默认view,然后执行相应Interceptor的postHandle。

设置默认view的过程中使用到了ViewNameTranslator。

到这里请求处理的内容就完成了,接下来使用processDispatchResult方法处理前面返回的结果,其中包括处理异常、渲染页面、触发Interceptor的afterCompletion方法三部分内容。

doDispatch有两层异常捕获,内层是捕获在对请求进行处理的过程中抛出的异常,外层主要是在处理渲染页面时抛出的。内层的异常,也就是执行请求处理时的异常会设置到dispatchException变量,然后在processDispatchResult方法中进行处理,外层则是处理processDispatchResult方法抛出的异常。

看到processDispatchResult处理异常的方式其实就是将相应的错误页面设置到View,在其中的processHandlerException方法中用到了HandlerExceptionResolver。

渲染页面具体在render方法中执行,render中首先对response设置了Local,过程中使用了LocaleResolver,然后判断View如果是String类型则调用resolveViewName方法使用ViewResolver得到实际的View,最后调用View的render方法对页面进行具体渲染,渲染的过程中使用到了ThemeResolver。

最后通过mappedHandler的triggerAfterConpletion方法触发Interceptor的

afterCompletion方法,这里的Interceptor也是按反方向执行的。到这里processDispatchResult方法就执行完了。

再返回doDispatch方法中,在最后的finally中判断是否请求启动了异步处理,如果启动了则调用相应异步处理的拦截器,否则如果是上传请求则删除上传请求过程中产生的临时资源。

doDispatch方法就分析完了。可以看到SpringMVC的处理方式是先在顶层设计好整体架构,然后将具体的处理交给不同的组件具体去实现的。doDispatch的流程图如下图所示,中间是doDispatcher的处理流程图,左边是Interceptor相关处理方法的调用位置,右边是doDisPatcher方法处理过程中涉及的组件。图中上半部分的处理请求对应着MVC中的Controller也就是C层,下半部分的processDispatchResult主要对应了MVC中的View也就是V层,M层也就是Model贯穿于整个过程中。

看透SpringMVC源代码分析与实践(三)_第2张图片

三个Servlet的处理过程大致功能如下:

HttpServletBean:没有参与实际请求的处理。

FrameworkServlet:将不同类型的请求合并到processRequest方法统一处理,

processRequest方法中做了三件事:

调用了doService模板方法具体处理请求。

将当前请求的LocaleContext和ServletRequestAttributes在处理请求前设置到了LocaleContextHolder和RequestContextHolder,并在请求处理完成后恢复。

请求处理完成后发布了ServletRequestHandledEvent消息。

DispatcherServlet:doService方法给request设置了一些属性并将请求交给了doDispatch 方法具体处理。

DispatcherServlet中的doDispatch方法完成SpringMVC中请求处理过程的顶层设计,它使用DispatcherServlet中的九大组件完成了具体的请求处理。

 

四、SpringMVC组件概览

1.HandlerMapping

HandlerMapping的作用是根据request找到相应的处理器Handler和Interceptors,HandlerMapping接口里面只有一个方法。

HandlerExecutionChain getHandler(HttpServletRequest var1) throws Exception;

方法的实现非常简单,只要使用Request返回HandlerExecutionChain就可以了。我们也可以自己定义一个HandlerMapping,然后自己实现getHandler方法。比如,定义一个Tudou-

HandlerMapping,将tudou开头的请求对应到digua的处理器去处理,只需要判断请求的url:

如果是以tudo开头那么就返回地瓜的Handler。如果想更进一步还可以再细分,如将tudou开头的Get请求对应到maidigua(卖地瓜),而Post请求对应到shoudigua(收地瓜),其他类型全部交给digua处理器,代码如下:

public class DiguaHandlerMapping implements HandlerMapping {

    @Override

    public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {

        String url = request.getRequestURI().toString();

        String method = request.getMethod();

        if(url.startsWith("/tudou")){

            if(method.equalsIgnoreCase("GET")){

                return "maidigua"对应的Handler;

            }

            else if(method.equalsIgnoreCase("POST")){

                return "SHOUDIGUA"对应的Handler;

            }

            else {

                return "digua"对应的handler;

            }

        }

        return null;

    }

}

这里的HandlerMapping只能查找”tudou”相关的Handler,而更一般的做法是维护一个对应多个请求的map,其实SimpleUrlHandlerMapping的基本原理就是这样。HandlerMapping编写出来后需要注册到SpingMVC的容器里面才可以使用,注册也非常简单,只需要在配置文件里配置一个Bean就可以了,SpringMVC会按照类型将它注册到HandlerMapping中。

如果有个专门处理tudoupian(土豆片)的处理器(处理以tudoupian开头的url),而且是在另外的HandlerMapping中进行的映射,这就涉及有限使用哪个HandlerMapping来查找处理器的问题,如果先使用TudouHandlerMapping就会将tudoupian的请求也交给tudou的处理器,这样就出问题了!这时候使用HandlerMapping的顺序就非常重要了,这里的顺序可以通过order属性来定义(当然HandlerMapping需要实现Order接口),order越小越先使用,比如:

查找Handler是按顺序便利所有的HandlerMapping,当找到一个HandlerMapping后立即停止查找并返回。

 

2.HandlerAdapter

HandlerAdapter里面一共有三个方法,supports(Object handler)判断是否可以使用某个Handler;Handler方法是用来具体使用Handler干活;getLastModified是获取资源的Last-Modified,Last-Modified是资源最后一次修改的时间。

之所以使用HandlerAdapter是因为SpringMVC并没有对处理器做任何限制,处理器可以以任何合理的方式来表现,可以是一个类,也可以是一个方法,还可以是别的合理的方式,从handle方法可以看出它是Object的类型。

接着前面的例子写一个HandlerAdapter,首先写一个MaiDiguaController处理器,然后再写对应的HandlerAdapter,代码如下:

public class MaiDiguaController {

    public ModelAndView maidigua(HttpServletRequest request, HttpServletResponse response){

    }

}

public class MaiDiguaHandlerAdapter implements HandlerAdapter {

    @Override

    public boolean supports(Object o) {

        return o instanceof MaiDiguaController;

    }

    @Override

    public ModelAndView handle(HttpServletRequest httpServletRequest,

HttpServletResponse httpServletResponse, Object o) throws Exception {

        return ((MaiDiguaController)o).maidigua(httpServletRequest,httpServletResponse);

    }

    @Override

    public long getLastModified(HttpServletRequest httpServletRequest, Object o) {

        return -1L;

    }

}

这里写了一个卖地瓜的Handler——MaiDiguaController,用来具体处理卖地瓜这件事,MaiDiguaHandlerAdapter使用MaiDiguaController完成卖地瓜这件事。

下面看一个SpingMVC自己的HandlerAdapter——SimpleControllerHandlerAdapter,代码如下:

public class SimpleControllerHandlerAdapter implements HandlerAdapter {

    public SimpleControllerHandlerAdapter() {

    }

 

    public boolean supports(Object handler) {

        return handler instanceof Controller;

    }

 

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

        return ((Controller)handler).handleRequest(request, response);

    }

 

    public long getLastModified(HttpServletRequest request, Object handler) {

        return handler instanceof LastModified?((LastModified)handler).getLastModified(request):-1L;

    }

}

可以看到这个Adapter也非常简单,是用来使用实现Controller接口的处理器干活的,干活的方法是直接调用处理器的handleRequest方法。

选择使用哪个HandlerAdapter的过程在getHandlerAdapter方法中,它的逻辑是遍历所有的Adapter,然后检查哪个可以处理当前的Handler,找到第一个可以处理Handler的Adapter后就停止查找并将其返回。既然需要挨个检查,那就需要有一个顺序,这里的顺序同样是通过Order属性来设置的。

HandlerAdapter需要注册到SpringMVC的容器里,注册方法和HandlerMapping一样,只要配置一个Bean就可以了。Handler是从HandlerMapping里返回的。

3.HandlerExceptionResolver

别的组件都是在正常情况下用来干活的,不过干活的过程中难免会出现问题,出现问题后怎么办呢?这就需要有一个专门的角色对异常情况进行处理,在SpringMVC中就是HandlerExceptionResolver。具体来说,此组件的作用是根据异常设置ModelAndView,之后再交给render方法进行渲染。render只负责将ModelAndView渲染成页面,具体ModelAndView是怎么来的render并不关心。这也是SpringMVC设计优秀的一个表现——分工明确互不干涉。通过前面doDispatcher的分析可以知道HandlerExceptionResolver只是用于解析对请求做处理的过程中产生的异常,而渲染环节产生的异常不归它管,现在我们就知道原因了:它是在render之前工作的,先解析出ModelAndView之后render才去渲染的,所以它就不能处理render过程中的异常了。

HandlerExceptionResolver结构非常简单,只有一个方法,只需要从异常解析出ModelAndView就可以了。具体实现可以维护一个异常为key、value为view的Map,解析是直接从Map里获取View,如果在Map里没有相应的异常可以返回默认的view。

 

4.ViewResolver

ViewResolver用来将String类型的视图名和Locale解析为View类型的视图,ViewResolver接口也非常简单,只有一个方法。

public interface ViewResolver {

    View resolveViewName(String var1, Locale var2) throws Exception;

}

从接口方法的定义可以看出解析视图所需的参数是视图名和Locale,不过一般情况下我们只需要根据视图名找到对应的视图,然后渲染就行,并不需要对不同的区域使用不同的视图进行显示,如果需要国际化支持也只要将显示的内容或者主题使用国际化支持,不过SpringMVC确实有这个功能,可以让不同的区域使用不同的视图进行显示。ResourceBundleViewResolver就需要同时使用视图名和Locale来解析视图。ResourceBundleViewResolver需要将每一个视图名和对应的视图类型配置到相应的properties文件中,默认使用classpath下的views为baseName的配置文件,如views.properties、views_zh_CN.properties等,baseName和文件位置都可以设置。不同的Locale会使用不同的配置文件,而且它们之间没有任何关系,这样就可以让不同的区域使用不同的View来进行渲染了。

View是用来渲染页面的,通俗点来说就是要将程序返回的参数填入模板里,生成html文件。这里有两个问题:使用哪个模板?用什么技术填入参数?

这其实就是ViewResolver主要要做的工作,ViewResolver需要找到渲染所用的模板和所用的技术(也就是视图的类型)进行渲染,具体的渲染过程则交给不同的视图自己完成。我们最常使用的UrlBasedViewResolver系列的解析器都是针对单一视图类型进行解析的。只需要找到使用的模板就可以了,比如,InternalResourceViewResolver只针对jsp类型的视图,FreeMarkerViewResolver只针对FreeMarker,VelocityViewResolver只针对Velocity。而ResourceBundleViewResolver、XmlViewResolver、BeanNameViewResolver等解析器可以同时解析多种类型的视图。如前面说过的ResourceBundleViewResolver,它是根据properties配置文件来解析的,配置文件里就需要同时配置class和url两项内容,比如:

hello.(class)=org.springframework.Web.servlet.view.InternalResourceView

hello.url=/WEB-INF/go.jsp

这样就将hello配置到/WEB-INF/go.jsp模板的jsp类型的视图了,当Controller返回hello时会使用这个视图来进行渲染。这里的jsp类型并不是根据go.jsp的后缀来确定的,这个后缀只是个文件名,可以把它改成任意别的格式,视图的类型是根据.class的配置项来确定的。

XmlViewResolver与ResourceBundleViewResolver类似,只不过它是使用xml文件来配置的。BeanNameViewResolver是根据ViewName从ApplicationContext容器中查找相应的bean做View的,这个实现比较简单,下面是BeanNameViewResolver的源码。

public class BeanNameViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered {

    private int order = 2147483647;

    public BeanNameViewResolver() {}

    public void setOrder(int order) {this.order = order;}

    public int getOrder() {return this.order;}

    public View resolveViewName(String viewName, Locale locale) throws BeansException {

        ApplicationContext context = this.getApplicationContext();

        if(!context.containsBean(viewName)) {

            if(this.logger.isDebugEnabled()) {

                this.logger.debug("No matching bean found for view name \'" + viewName + "\'");

            }

            return null;

        } else if(!context.isTypeMatch(viewName, View.class)) {

            if(this.logger.isDebugEnabled()) {

                this.logger.debug("Found matching bean for view name \'" + viewName + "\' - to be ignored since it does not implement View");

            }

            return null;

        } else {

            return (View)context.getBean(viewName, View.class);

        }

    }

}

可以看到其原理就是根据viewName从spring容器中查找Bean,如果查找不到或者查到后不是View类型则返回null,否则返回容器中的bean。

ViewResolver的使用需要注册到SpringMVC的容器里,默认使用的是org.spring

-framewoek.web.serlvet.view.InternalResourceViewResolver。

 

5.RequestToViewNameTranslator

ViewResovler是根据ViewName查找View,但有的Handler处理完后并没有设置View,也没有设置viewName,这时就需要从request获取viewName了,而如何从request获取viewName就是RequestToViewNameTranslator要做的事情。RequestToViewNameTranslator接口定义如下:

public interface RequestToViewNameTranslator {

     String getViewName(HttpServletRequest var1) throws Exception;

}

其中只有一个getViewName方法,只要通过request获取到viewName就可以了。我们自己定义一个Translator,其中判断如果是tudou的Get请求则返回“maidigua”否则返回“404”

作为viewName。

public class MaiDiguaRequestToViewNameTranslator implements RequestToViewName

-Translator {

    @Override

    public String getViewName(HttpServletRequest request) throws Exception {

        if(request.getRequestURI().toString().startsWith("/tudou")&&

                request.getMethod().equalsIgnoreCase("GET")){

            return "maidigua";

        }

        return "404";

    }

}

实际使用时应该设置规则而不是将某个具体请求与ViewName的对应关系硬编码到程序里面,那样才可以具备更好的通用性。

RequestToViewNameTranslator在SpringMVC容器里只可以配置一个,所以所有request到ViewName的转换规则都要在一个Translator里面全部实现。

 

6.LocaleResolver

解析视图需要两个参数:一个是视图名,另一个是Locale。视图名是处理器返回的(或者使用RequestToViewNameTranslator解析的默认视图名),Locale是从哪里来的呢?这就是LocaleResolver要做的事情。

LocaleResolver用于从request解析出Locale。Locale就是zh-cn之类,表示一个区域。有了这个就可以对不同区域的用户显示不同的结果,这就是il8n(国际化)的基本原理,LocaleResolver是il8n的基础。LocaleResolver接口定义如下:

public interface LocaleResolver {

    Locale resolveLocale(HttpServletRequest var1);

    void setLocale(HttpServletRequest var1, HttpServletResponse var2, Locale var3);

}

接口定义非常简单,只有2个方法,分别表示:从request解析出Locale和将特定的Locale设置给某个request。之前doService方法中,容器会将LocaleResolver设置到request的attribute中,代码如下:

request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);

这样就让我们再需要使用Locale的时候可以直接从request拿到localeResolver,然后解析出Locale。

SpringMVC中主要在两个地方用到了Locale:

ViewResolver解析视图的时候;

使用国际化资源或者主题的时候。

国际化资源和主题主要使用RequestContext的getMessage和getThemeMessage方法。

标签内部其实就是使用的RequestContext,只不过没有直接调用getMessage而是先调用了getMessageSource然后在内部有调用了getMessage,详细代码可以看org.springframework.web.servlet.tags.MessageTag。

LocaleResolver的作用就是选择区域,有时候需要提供人为设置区域的功能,比如很多网站可以选择显示什么语言,这就需要提供人为修改Locale的机制。在SpringMVC中非常简单,只需要调用LocaleResolver的setLocale方法即可。我们可以使用Interceptor来做到这一点,这就是org.springframework.web.servlet.i18n.LocaleChangeInterceptor,配置方法如下:

 

      

           

LocaleChangeInterceptor"/>

 

这样就可以通过locale参数来修改Locale了,比如:

http://localhost:8080?locale=zh_CN

http://localhost:8080?locale=en

这里的“locale”也可以通过paramName设置为别的名称,如设置为“lang”。

 

      

           

LocaleChangeInterceptor" p:paramName=”lang”/>

 

 

7.ThemeResolver

ThemeResolver是解析主题用的。ThemeResolver的接口定义如下:

public interface ThemeResolver {

    String resolveThemeName(HttpServletRequest var1);

    void setThemeName(HttpServletRequest var1, HttpServletResponse var2, String var3);

}

不同的主题就是换了一套图片、显示效果以及样式等。SpringMVC中一套主题对应一个properties文件,里面存放着跟当前主题相关的所有资源,如图片、css样式表等,例如:

#theme.properties

logo.pic=/images/default/logo.jpg

logo.word=excelib

style=/css/default/style.css

将上面的文件命名为theme.properties,放到classpath下面就可以在页面中使用了,如果在jsp页面中,使用就可以得到excelib了(需要在文件开头引入spring自己的标签库<%@taglibprefix=”spring” uri=”http://www.springframework.org-

/tags”%>)。现在所用的主题名就叫theme,主题名也就是文件名,所以创建主题非常简单,只需要准备好资源,然后新建一个以主题名为文件名的properties文件并将资源设置进去就可以了。另外,SpringMVC的主题也支持国际化,也就是说同一个主题不同的区域也可以显示不同的风格,比如,可以定义以为主题文件

theme.properties

theme_zh_CN.properties

Theme_en_US.properties

这样即使同样使用theme的主题,不同的区域也会调用不同主题文件里的资源进行显示。

SpringMVC中跟主题相关的类主要有ThemeResolver、ThemeSource和Theme。从上面的接口可以看出,ThemeResolver的作用是从request解析出主题名;ThemeSource则是根据主题名找到具体的主题;Theme是ThemeSource找出的一个具体的主题,可以通过它获取主题里具体的资源。获取主题的资源依然在RequestContext中,代码如下:

//org.springframework.web.servlet.support.RequestContext

public String getThemeMessage(String code, Object[] args, String defaultMessage) {

      return this.getTheme().getMessageSource().getMessage(code, args, defaultMessage, this.locale);

}

public Theme getTheme() {

      if(this.theme == null) {

          this.theme = RequestContextUtils.getTheme(this.request);

          if(this.theme == null) {

             this.theme = this.getFallbackTheme();

          }

        }

        return this.theme;

 }

可以看到这里首先通过RequestContextUtils获取到Theme,然后获取对应的资源,再看一下RequestContextUtils是怎么获取Theme的:

 

//org.springframework.web.servlet.support.RequestContextUtils

    public static Theme getTheme(HttpServletRequest request) {

        ThemeResolver themeResolver = getThemeResolver(request);

        ThemeSource themeSource = getThemeSource(request);

        if(themeResolver != null && themeSource != null) {

            String themeName = themeResolver.resolveThemeName(request);

            return themeSource.getTheme(themeName);

        } else {

            return null;

        }

}

从这里就可以清楚地看到ThemeResolver和ThemeSource的作用。ThemeResolver的默认实现是org.springframework.web.servlet.theme.FixedThemeResolver,这里边使用的默认主题名就叫“theme”,这也就是前面使用theme主题时不用配置也可以使用的原因。从DispatcherServlet中可以看到ThemeResolver默认使用的是WebApplicationContext。

在讲SpringMVC容器创建时介绍过WebApplicationContext是在FrameworkServlet中创建的,默认使用的是XmlWebApplicationContext,其父类是AbstractRefreshableWebApplicaion-

Context,这个类实现了ThemeSource接口,其实现方式是在内部封装了一个ThemeSource属性,然后将具体工作交给它去干。

现在我们就把整个原理弄明白了:主题是通过一系列资源来具体体现的,要得到一个主题的资源,首先要得到资源的名称,这个是ThemeResolver的工作,然后用资源名称找到主题(可以理解为一个配置文件),这个ThemeSource的工作,最后使用主题获取里面具体的资源就可以了。ThemeResolver默认使用的是FixedThemeResolver,ThemeSource默认使用的是WebApplicationContext(其实是AbstractRefreshableWebApplicationContext里的ThemeSource),不过我们也可以自己来配置,例如:

ResourceBundleThemeSource”

p:basenamePrefix=”com.excelib.themes.”/>

CookieThemeResolver”

p:defaultThemeName=”default”/>

这里不仅配置了themeResolver和themeSource,而且还配置了默认主题名为“default”,以及配置文件的位置在com.excelib.themes包下(配置时最后要有点“.”)。

SpringMVC中主题的切换和Locale的切换使用相同的模式,也是使用Interceptor。配置如下:

     

          

          ThemeChangeInterceptor"

                p:paramName="theme"/>

     

可以通过paramName设置修改主题的参数名,默认使用“theme”。下面的请求可以切换为summer主题:

http://localhost:8080?theme=summer

 

8.MultipartResolver

MultipartResolver用于处理上传请求,处理方法是将普通的request包装成MultipartHttpServletRequest,后者可以直接调用getFile方法获取到File,如果上传多个文件,还可以调用getFileMap得到FileName->File结构的Map,这样就使得上传请求的处理变得非常简单。当然,这里做的其实是锦上添花的事情,如果上传的请求不用MultipartResolver封装成MultipartHttpServletRequest,直接用原来的request也是可以的,所以在SpringMVC中此组件没有提供默认值。MultipartResolver定义如下:

public interface MultipartResolver {

    boolean isMultipart(HttpServletRequest var1);

    MultipartHttpServletRequest resolveMultipart(HttpServletRequest var1) throws

MultipartException;

    void cleanupMultipart(MultipartHttpServletRequest var1);

}

这里一共三个方法,作用分别是判断是不是上传请求、将request包装成MultipartHttp-

ServletRequest、处理完后清理上传过程中产生的临时资源。

 

9.FlashMapManager

FlashMap相关的内容在前面已经介绍过了,主要用在redirect中传递参数。而FlashMapManager是用来管理FlashMap的,定义如下:

public interface FlashMapManager {

    FlashMap retrieveAndUpdate(HttpServletRequest var1, HttpServletResponse var2);

    void saveOutputFlashMap(FlashMap var1, HttpServletRequest var2,

HttpServletResponse var3);

}

retrieveAndUpdate方法用于恢复参数,并将恢复过的和超时的参数从保存介质中删除;

saveOutputFlashMap用于将参数保存起来。

默认实现是org.springframework.web.servlet.support.SessionFlashMapManager,它是将参数保存到session中。

整个redirect的参数通过FlashMap传递的过程分三步:

  • 在处理器中将需要传递的参数设置到outputFlashMap中,设置方法在分析Dispatcher

-Servlet的时候已经介绍过了,可以直接使用request.getAttribute(DispatcherServlet.

OUTPUT_FLASH_MAP_ATTRIBUTE)拿到outputFlashMap,然后将参数put进去,也可以将需要传递的参数设置到处理器的RedirectAttributes类型的参数中,当处理器处理完请求时,如果是redirect类型的返回值RequestMappingHandlerAdapter会将其设置到outputFlashMap中。

  • 在RedirectView的renderMergedOutputModel方法中调用FlashMapManager的saveOut

-putFlashMap方法,将outputFlashMap中的参数设置到Session中。

  • 请求redirect后DispatcherServlet的doService会调用FlashMapManager的retrieveAnd

-Update方法从session中获取inoutFlashMap并设置到Request的属性中备用,同时从session中删除。

SpringMVC的九大组件分别是HandlerMapping、HandlerAdapter、HandlerExceptionRe

-solver、ViewResolver、RequestToViewNameTranslator、LocaleResolver、ThemeResolver、

MultipartResolver、FlashMapManager。

你可能感兴趣的:(看透SpringMVC源代码分析与实践(三))