Spring5源码解析-Spring中的处理器handlers

Spring Web应用程序的中心站是DispatcherServlet(具体请查看Spring5源码解析-论Spring DispatcherServlet的生命周期)。这是所有传入请求的中心入口。但如果没有但如果没有众多的handlers,我们依然不能做任何事情。

首先,在本文中,我们先解读handler到底是个啥。之后,我们将介绍一些Spring框架中handlers的两种处理类型。最后我们加点salt来让我们学的东西进行落地,我们将编写我们自己的handler。


Spring中的两种handler类型

首先,在Spring的世界中,这些handler到底做了些什么。简单的讲,这个就和我们听到一句话或者看到某个场景,然后有相关的反应是一样的,由很多处理最后转换到我们大脑皮层所能理解的东西。从机器语言的角度就是词法分析,语法分析,好咯,大家知道编译语言的重要性也就是基础的重要性了吧,回到框架中来,对于Spring来讲,这些处理程序就是一种将用户操作转换为Spring可以理解的元素。说到用户操作,我们可以考虑像http://xxx.com/login这样的URL类型。而我们的handler,在这里作为翻译处理,将尝试找到为此地址应该调用哪个控制器来处理。通常我们写Spring controller代码都知道,处理程序可以查找@RequestMapping的注解,并检查哪些映射与/login 这个URL匹配。由上一篇文章我们可以知道,这个处理程序将在DispatcherServlet的内被调用。

更准确地说,Spring中存在两种类型的handlers。第一种是handler mappings(处理程序映射)。它们的角色定位与前面所描述的功能完全相同。它们尝试将当前请求与相应的controller以及其中的方法相匹配。第二种是handler adapter(处理器适配器)。handler adapter从handler mappings中获取映射的controllers 和方法并调用它们。这种类型的适配器必须实现org.springframework.web.servlet.HandlerAdapter接口,它只有3种方法:
boolean supports(Object handler):检查传入参数的对象是否可以由此适配器处理
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) : 将请求翻译成视图。
long getLastModified(HttpServletRequest request, Object handler):返回给定HttpServletRequest的最后修改日期,以毫秒为单位。

但这里要注意的是,在Spring版本中有一些重要变化。作为DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapter或AnnotationMethodHandlerExceptionResolver的处理程序适配器自Spring 3.2版本以来已经废弃,在Spring4.x里还可以看到,在Spring5内已经删除掉了,替代品为RequestMappingHandlerMapping,RequestMappingHandlerAdapter和ExceptionHandlerExceptionResolver。通过这些新类以便于自定义映射。另外,通过在since 3.1 版本中org.springframework.web.method.HandlerMethod类中引入,来将所处理的对象转换为其方法表示。我们可以通过这个方法来判断对象返回的类型或者哪些参数是我们所期望的(看着拗口的话请打开源码查看此类注释)。


Spring框架中的handler

除了已经提供的处理程序适配器之外,Spring也有本地处理程序映射,最基本的处理程序映射器是org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping类。它将URL与相应的bean进行匹配。例如,请看下面的配置:

<bean name="/friends" class="com.waitingforcode.controller.FriendsController" />

正如你所看到的,这种配置在很多URL的情况下是很不实用的。一些更灵活的处理映射器是org.springframework.web.servlet.handler.SimpleUrlHandlerMapping。而不是为每个请求创建bean,我们可以创建一个映射文件,其中包含URL作为键和controller作为值,看下面的配置:

<bean id="simpleUrlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> 
  <property name="mappings">
    <props>
      <prop key="/friends.html">FriendsController</props>
  </property>
</bean>

但是,在SimpleUrlHandlerMapping中,处理稍微复杂URL也是一个头疼的问题。这也是为什么我们要用DefaultAnnotationHandlerMapping或者在最新的Spring版本中使用org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping的原因。

它们的映射检测是基于注解。这样,所有的逻辑都保留在Java代码这块,例如:

@Controller
public class FriendsController {

    @RequestMapping(value = "/friends.html", method = RequestMethod.GET)
    public String showFriendsList() {
        return "friendsListView";
    }

    @RequestMapping(value = "/friends/potential-friends.html" method = RequestMethod.GET)
    public String showPotentialFriends() {
        return "potentialFriendsView";
    }
}

与以前的处理程序不同的是,基于注解允许更灵活的配置。不仅不需要在XML进行各种繁琐的配置,一旦URL很多的情况下,想象一下XML,各种头大,现在通过注解,我们可以把一条路上不同岔口的URL在一个controller里进行接收处理就好。当配置文件中定义<mvc:annotation-driven/>时,此处理程序将被激活。另外,为了更细粒度的处理controller注解,我们可以通过添加<context:annotation-config />和<context:component-scan base-package =“path.with.my.services.and.controllers”/>来启用它们。

编写自定义的Spring handler程序

现在我们更深入了解一下Spring mapping handlers。我们来实现个我们自己的URL处理程序。其实很简单(因为只需要达到最基本的处理目的就可以了) ,我们将替换RequestMappingHandlerMapping,并使一个简单的映射器来处理URL地址。我们的映射器将只处理静态URL,如:/home.html。它无须也无法从方法签名中获取动态参数以及也无须知道@PathVariable元素。主要目标是让大家从中发现Spring处理一个请求所进行的步骤。

我们这个handler将扩展RequestMappingHandlerMapping并覆盖其方法(有些方法可以从RequestMappingInfoHandlerMapping找到,其实就是重写或实现AbstractHandlerMethodMapping里的几个抽象方法):

  • protected void registerHandlerMethod(Object handler,Method method,RequestMappingInfo mapping):

  • protected boolean isHandler(Class beanType): 检查bean是否符合给定处理程序的条件。

  • protected RequestMappingInfo getMappingForMethod(Method method,Class handlerType): 为给定的Method实例提供映射的方法,该方法表示处理的方法(例如,使用@RequestMapping注解的controller的方法上所对应的URL)。

  • protected HandlerMethod handleNoMatch(Set requestMappingInfos, String lookupPath, HttpServletRequest request) : 在给定的HttpServletRequest对象找不到匹配的处理方法时被调用。

  • protected void handleMatch(RequestMappingInfo info, String lookupPath, HttpServletRequest request) : 当为给定的HttpServletRequest对象找到匹配的处理方法时调用。

在写这个handler之前,让我们写个自定义的@RequestMapping的注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DumberRequestMapping {
    String value() default "";
}

唯一的属性是代表URL路径的值,与@RequestMapping注解中的value属性完全相同。现在我们可以传入我们的处理程序映射类。该课程在内部进行评论。这就是为什么它不会在通常的“文本模式”中包含任何补充评论。

public class DumberRequestHandlerMapping extends RequestMappingHandlerMapping {
    private static final Logger LOGGER = LoggerFactory.getLogger(DumberRequestHandlerMapping.class);

    /**
     * Checks if handler should be applied to given bean's class. The check is made through looking for DumberRequestMapping annotation.
     */
    @Override
    protected boolean isHandler(Class<?> beanType) {
        Method[] methods = ReflectionUtils.getAllDeclaredMethods(beanType);
        for (Method method : methods) {
            if (AnnotationUtils.findAnnotation(method, DumberRequestMapping.class) != null) {
                LOGGER.debug("[DumberRequestHandlerMapping] Method "+method+" supports @DumberRequestMapping ");
                return true;
            }
        }
        return false;
    }

    /**
     * Make some operations directly before returning HttpServletRequest instance into mapped controller's method. For example, if you add here some attributes to this object, those attributes will be reachable from controller's method which handles the request. 
     * RequestMappingInfoHandlerMapping does some of more complicated stuff here like exposing URI template variables or extracting 
     * "matrix variable".
     * NOTE : "matrix variables" are name-value pairs within path segments, separated with a semicolon (;). For example in this URL 
     * /clubs;country=France;division=Ligue 1, Ligue 2) we can find 2 matrix variables: country (France) and division (list composed by 
     * Ligue 1 and Ligue 2)
     */
    @Override
    protected void handleMatch(RequestMappingInfo info, String lookupPath, HttpServletRequest request) {
        LOGGER.debug("[DumberRequestHandlerMapping] handleMatch info "+info+  ", lookupPath ="+ lookupPath + ", request ="+request);
        request.setAttribute("isDumber", true);
        request.setAttribute("handledTime", System.nanoTime());
    }

    /**
     * Method invoked when given lookupPath doesn't match with this handler mapping.
     * Native RequestMappingInfoHandlerMapping uses this method to launch two exceptions : 
     * - HttpRequestMethodNotSupportedException - if some URLs match, but no theirs HTTP methods.
     * - HttpMediaTypeNotAcceptableException - if some URLs match, but no theirs content types. For example, a handler can match an URL 
     * like /my-page/test, but can expect that the request should be send as application/json. Or, the handler can match the URL but 
     * returns an inappropriate response type, for example: text/html instead of application/json.
     */
    @Override
    protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> requestMappingInfos, String lookupPath, HttpServletRequest request) throws ServletException {
        LOGGER.debug("[DumberRequestHandlerMapping] handleNoMatch info "+requestMappingInfos+  ", lookupPath ="+ lookupPath + ", request ="+request);
        return null;
    }

    /**
     * Here we constructs RequestMappingInfo instance for given method.
     * RequestMappingInfo - this object is used to encapsulate mapping conditions. For example, it contains an instance of 
     * PatternsRequestCondition which  is used in native Spring's RequestMappingInfoHandlerMapping  handleMatch() method to put URI 
     * variables into @RequestMapping pattern. 
     * Ie, it will take the following URL /test/1 and match it for URI template /test/{id}. In occurrence, it will found that 1 
     * corresponding to @PathVariable represented  by id variable ({id}) and will set its value to 1.
     */
    @Override
    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        LOGGER.debug("[DumberRequestHandlerMapping] getMappingForMethod method "+method+  ", handlerType ="+handlerType);
        RequestMappingInfo info = null;
        // look for @DumberRequestMapping annotation for the Method method from signature
        DumberRequestMapping methodAnnotation = AnnotationUtils.findAnnotation(method, DumberRequestMapping.class);
        if (methodAnnotation != null) {
            RequestCondition<?> methodCondition = getCustomMethodCondition(method);
            info = createRequestMappingInfo(methodAnnotation, methodCondition);
        }
        LOGGER.debug("[DumberRequestHandlerMapping] getMappingForMethod method; returns info mapping "+info);
        return info;
    }

    /**
     * Creates RequestMappingInfo object which encapsulates:
     * - PatternsRequestCondition: represents URI template to resolve. Resolving is helped by UrlPathHelper utility class from
     * package org.springframework.web.util.
     * - RequestMethodsRequestCondition: methods accepted by this handler. You can make a test and replace RequestMethod.GET by 
     * RequestMethod.POST. You will able to observe that our test won't work.
     * - ParamsRequestCondition: 
     * - HeadersRequestCondition: headers which should be send in request to given handler should handle this request. You can,
     * for exemple, put there an header value like "my-header:test" and observe the program behavior.
     * - ConsumesRequestCondition: this condition allows to specify the content-type of request. We can use it for, for example,
     * specify that a method can be handled only for application/json request.
     * - ProducesRequestCondition: this condition allows to specify the content-type of response. We can use it for, for example,
     * specify that a method can be applied only for text/plain response. 
     */
    protected RequestMappingInfo createRequestMappingInfo(DumberRequestMapping annotation, RequestCondition<?> customCondition) {
        return new RequestMappingInfo(
                new PatternsRequestCondition(new String[] {annotation.value()}),
                new RequestMethodsRequestCondition(new RequestMethod[]{RequestMethod.GET}),
                new ParamsRequestCondition(new String[]{}),
                new HeadersRequestCondition(new String[] {}),
                new ConsumesRequestCondition(new String[]{}, new String[]{}),
                new ProducesRequestCondition(new String[]{}, new String[]{}, getContentNegotiationManager()),
                customCondition);
    }

}

我们需要向我们的应用程序上下文添加新的HandlerMapping。请看下面这个基于XML的配置:

<bean class="com.mypackage.handler.DumberRequestHandlerMapping">//此处根据自己的包进行配置
  <property name="order" value="0" />
</bean>

请注意,order属性的存在确定了按顺序将请求由HandlerMapping处理。在这里,如果DumberRequestHandlerMapping可以应用于一个请求,Spring将立即使用它,而不需要寻找另一个可用的处理程序。

最后一件事是使用@DumberRequestMapping在方法上添加注解:

@Controller
public class TestController {
 private static final Logger LOGGER = LoggerFactory.getLogger(TestController.class);
    @DumberRequestMapping(value = "/test")
    public String testSession(HttpServletRequest request) {
        LOGGER.debug("Is dumber request ?"+request.getAttribute("isDumber"));
        LOGGER.debug("Handled time ?"+request.getAttribute("handledTime"));
        return "testTemplate";
    }

}

通过执行http://localhost:8084/test,您将看到在DumberRequestHandlerMapping的handleMatch方法中设置的请求的属性存在。如果您部署有应用程序的日志,您将看到有关controller执行流程的一些信息:

2017-08-05 23:31:00,027 [http-bio-8084-exec-1] [org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping]-[DEBUG] Looking up handler method for path /test   //先在RequestMappingHandlerMapping找的,也就是先找有@RequestMapping注解相应处理逻辑的方法来处理
2017-08-05 23:31:00,028 [http-bio-8084-exec-1] [org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping]-[DEBUG] Did not find handler method for [/test]   //在RequestMappingHandlerMapping中没找到相应的处理逻辑
2017-08-05 23:31:00,028 [http-bio-8084-exec-1] [com.migo.sso.DumberRequestHandlerMapping]-[DEBUG] Looking up handler method for path /test
  //从DumberRequestHandlerMapping里找,发现@DumberRequestMapping所注解的方法可以处理,那就处理咯
2017-08-05 23:31:00,029 [http-bio-8084-exec-1] [com.migo.sso.DumberRequestHandlerMapping]-[DEBUG] [DumberRequestHandlerMapping] handleMatch info {[/test],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}, lookupPath =/test, request =org.apache.catalina.connector.RequestFacade@24a7274b
2017-08-05 23:31:00,030 [http-bio-8084-exec-1] [com.migo.sso.DumberRequestHandlerMapping]-[DEBUG] Returning handler method [public java.lang.String com.migo.sso.controller.TestController.testSession(javax.servlet.http.HttpServletRequest)]
2017-08-05 23:31:00,030 [http-bio-8084-exec-1] [org.springframework.beans.factory.support.DefaultListableBeanFactory]-[DEBUG] Returning cached instance of singleton bean 'testController'
2017-08-05 23:31:00,030 [http-bio-8084-exec-1] [org.springframework.web.servlet.DispatcherServlet]-[DEBUG] Last-Modified value for [/test] is: -1
2017-08-05 23:31:00,040 [http-bio-8084-exec-1] [com.migo.sso.controller.TestController]-[DEBUG] Is dumber request ?true
2017-08-05 23:31:00,040 [http-bio-8084-exec-1] [com.migo.sso.controller.TestController]-[DEBUG] Handled time ?21230126522470
Handled time ?17452005683775

我们可以看到, handler mapping是Spring生态系统中的一个关键概念。所有的URL都由对应的处理程序处理,由此,Spring可以匹配传入的HTTP请求和所加注解配置的controller的方法。我们也看到了如何根据不同规则来过滤请求,例如:Content-Type,Accept或其他headers 或HTTP方法。我们还编写了一个poor版本的Spring的RequestMappingInfoHandlerMapping,它拦截一些URL处理并将结果通过视图输出给用户。
总结起来就是,通过一定的方式确定相应请求的处理位置(我们通常通过注解来确定),仅此而已,啰嗦了太多的东西,最后也就是如此的直白。

原文:Spring5源码解析-Spring中的处理器handlers

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,358评论 6 343
  • 什么是Spring Spring是一个开源的Java EE开发框架。Spring框架的核心功能可以应用在任何Jav...
    jemmm阅读 16,360评论 1 133
  • 1、Spring MVC请求流程 (1)初始化:(对DispatcherServlet和ContextLoderL...
    拾壹北阅读 1,916评论 0 12
  • 馒头学院、有书书院等知识付费账号的崛起像是雨后的春笋一般,三位好友扫码支持即可一分钱甚至免费领取课程,阅读书籍。面...
    余生_忆阅读 249评论 0 0