Spring MVC学习教程之RequestMappingHandlerMapping匹配

前言

对于RequestMappingHandlerMapping,使用Spring的同学基本都不会陌生,该类的作用有两个:

  • 通过request查找对应的HandlerMethod,即当前request具体是由Controller中的哪个方法进行处理;
  • 查找当前系统中的Interceptor,将其与HandlerMethod封装为一个HandlerExecutionChain。

本文主要讲解RequestMappingHandlerMapping是如何获取HandlerMethod和Interceptor,并且将其封装为HandlerExecutionChain的。

下面话不多说了,来一起看看详细的介绍吧

1.整体封装结构

RequestMappingHandlerMapping实现了HandlerMapping接口,该接口的主要方法如下:

public interface HandlerMapping {
 // 通过request获取HandlerExecutionChain对象
 HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}

这里我们直接看RequestMappingHandlerMapping是如何实现该接口的:

@Override
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request)
 throws Exception {
 // 通过request获取具体的处理bean,这里handler可能有两种类型:HandlerMethod和String。
 // 如果是String类型,那么就在BeanFactory中查找该String类型的bean,需要注意的是,返回的
 // bean如果是需要使用RequestMappingHandlerAdapter处理,那么也必须是HandlerMethod类型的
 Object handler = getHandlerInternal(request);
 if (handler == null) {
  // 如果找不到处理方法,则获取自定义的默认handler
  handler = getDefaultHandler();
 }
 if (handler == null) {
  return null;
 }
 if (handler instanceof String) {
  // 如果获取的handler是String类型的,则在当前BeanFactory中获取该名称的bean,
  // 并将其作为handler返回
  String handlerName = (String) handler;
  handler = obtainApplicationContext().getBean(handlerName);
 }

 // 获取当前系统中配置的Interceptor,将其与handler一起封装为一个HandlerExecutionChain
 HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
 // 这里CorsUtils.isCorsRequest()方法判断的是当前请求是否为一个跨域的请求,如果是一个跨域的请求,
 // 则将跨域相关的配置也一并封装到HandlerExecutionChain中
 if (CorsUtils.isCorsRequest(request)) {
  CorsConfiguration globalConfig =
   this.globalCorsConfigSource.getCorsConfiguration(request);
  CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
  CorsConfiguration config = (globalConfig != null ?
   globalConfig.combine(handlerConfig) : handlerConfig);
  executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
 }
 return executionChain;
}

从上面的代码可以看出,对于HandlerExecutionChain的获取,RequestMappingHandlerMapping首先会获取当前request对应的handler,然后将其与Interceptor一起封装为一个HandlerExecutionChain对象。这里在进行封装的时候,Spring会对当前request是否为跨域请求进行判断,如果是跨域请求,则将相关的跨域配置封装到HandlerExecutionChain中,关于跨域请求,读者可以阅读跨域资源共享 CORS 详解

2. 获取HandlerMethod

关于RequestMappingHandlerMapping是如何获取handler的,其主要在getHandlerInternal()方法中,如下是该方法的源码:

@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
 // 获取当前request的URI
 String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
 if (logger.isDebugEnabled()) {
  logger.debug("Looking up handler method for path " + lookupPath);
 }
 // 获取注册的Mapping的读锁
 this.mappingRegistry.acquireReadLock();
 try {
  // 通过path和request查找具体的HandlerMethod
  HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
  if (logger.isDebugEnabled()) {
   if (handlerMethod != null) {
    logger.debug("Returning handler method [" + handlerMethod + "]");
   } else {
    logger.debug("Did not find handler method for [" + lookupPath + "]");
   }
  }
  // 如果获取到的bean是一个String类型的,则在BeanFactory中查找该bean,
  // 并将其封装为一个HandlerMethod对象
  return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
 } finally {
  // 释放当前注册的Mapping的读锁
  this.mappingRegistry.releaseReadLock();
 }
}

上述方法中,其首先会获取当前request的uri,然后通过uri查找HandlerMethod,并且在最后,会判断获取到的HandlerMethod中的bean是否为String类型的,如果是,则在当前BeanFactory中查找该名称的bean,并且将其封装为HandlerMethod对象。这里我们直接阅读lookupHandlerMethod()方法:

@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath,
  HttpServletRequest request) throws Exception {
 List<Match> matches = new ArrayList<>();
 // 通过uri直接在注册的RequestMapping中获取对应的RequestMappingInfo列表,需要注意的是,
 // 这里进行查找的方式只是通过url进行查找,但是具体哪些RequestMappingInfo是匹配的,还需要进一步过滤
 List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
 if (directPathMatches != null) {
  // 对获取到的RequestMappingInfo进行进一步过滤,并且将过滤结果封装为一个Match列表
  addMatchingMappings(directPathMatches, matches, request);
 }
 if (matches.isEmpty()) {
  // 如果无法通过uri进行直接匹配,则对所有的注册的RequestMapping进行匹配,这里无法通过uri
  // 匹配的情况主要有三种:
  // ①在RequestMapping中定义的是PathVariable,如/user/detail/{id};
  // ②在RequestMapping中定义了问号表达式,如/user/?etail;
  // ③在RequestMapping中定义了*或**匹配,如/user/detail/**
  addMatchingMappings(this.mappingRegistry.getMappings().keySet(),
   matches, request);
 }

 if (!matches.isEmpty()) {
  // 对匹配的结果进行排序,获取相似度最高的一个作为结果返回,这里对相似度的判断时,
  // 会判断前两个是否相似度是一样的,如果是一样的,则直接抛出异常,如果不相同,
  // 则直接返回最高的一个
  Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
  matches.sort(comparator);
  if (logger.isTraceEnabled()) {
   logger.trace("Found " + matches.size()
       + " matching mapping(s) for [" + lookupPath + "] : " + matches);
  }
  // 获取匹配程度最高的一个匹配结果
  Match bestMatch = matches.get(0);
  if (matches.size() > 1) {
   // 如果匹配结果不止一个,首先会判断是否是跨域请求,如果是,
   // 则返回PREFLIGHT_AMBIGUOUS_MATCH,如果不是,则会判断前两个匹配程度是否相同,
   // 如果相同则抛出异常
   if (CorsUtils.isPreFlightRequest(request)) {
    return PREFLIGHT_AMBIGUOUS_MATCH;
   }
   Match secondBestMatch = matches.get(1);
   if (comparator.compare(bestMatch, secondBestMatch) == 0) {
    Method m1 = bestMatch.handlerMethod.getMethod();
    Method m2 = secondBestMatch.handlerMethod.getMethod();
    throw new IllegalStateException("Ambiguous handler methods mapped for"
     + " HTTP path '" + request.getRequestURL() + "': {" + m1
     + ", " + m2 + "}");
   }
  }
  // 这里主要是对匹配结果的一个处理,主要包含对传入参数和返回的MediaType的处理
  handleMatch(bestMatch.mapping, lookupPath, request);
  return bestMatch.handlerMethod;
 } else {
  // 如果匹配结果是空的,则对所有注册的Mapping进行遍历,判断当前request具体是哪种情况导致
  // 的无法匹配:①RequestMethod无法匹配;②Consumes无法匹配;③Produces无法匹配;
  // ④Params无法匹配
  return handleNoMatch(this.mappingRegistry.getMappings().keySet(),
   lookupPath, request);
 }
}

这里对于结果的匹配,首先会通过uri进行直接匹配,如果能匹配到,则在匹配结果中尝试进行RequestMethod,Consumes和Produces等配置的匹配;如果通过uri不能匹配到,则直接对所有定义的RequestMapping进行匹配,这里主要是进行正则匹配,如果能匹配到。如果能够匹配到,则对匹配结果按照相似度进行排序,并且对前两个结果相似度进行比较,如果相似度一样,则抛出异常,如果不一样,则返回相似度最高的一个匹配结果。如果无法获取到匹配结果,则对所有的匹配结果进行遍历,判断当前request具体是哪一部分参数无法匹配到结果。对于匹配结果的获取,主要在addMatchingMappings()方法中,这里我们继续阅读该方法的源码:

private void addMatchingMappings(Collection<T> mappings, List<Match> matches,
  HttpServletRequest request) {
 for (T mapping : mappings) {
  T match = getMatchingMapping(mapping, request);
  if (match != null) {
   matches.add(new Match(match,
    this.mappingRegistry.getMappings().get(mapping)));
  }
 }
}

对于RequestMapping的匹配,这里逻辑比较简单,就是对所有的RequestMappingInfo进行遍历,然后将request分别于每个RequestMappingInfo进行匹配,如果匹配上了,其返回值就不为空,最后将所有的匹配结果返回。如下是getMatchingMapping()方法的源码(其最终调用的是RequestMappingInfo.getMatchingCondition()方法):

@Override
@Nullable
public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
 // 判断request请求的类型是否与当前RequestMethod匹配
 RequestMethodsRequestCondition methods =
  this.methodsCondition.getMatchingCondition(request);
 // 判断request请求的参数是否与RequestMapping中params参数配置的一致
 ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
 // 判断request请求的headers是否与RequestMapping中headers参数配置的一致
 HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
 // 判断request的请求体类型是否与RequestMapping中配置的consumes参数配置的一致
 ConsumesRequestCondition consumes =
  this.consumesCondition.getMatchingCondition(request);
 // 判断当前RequestMapping将要返回的请求体类型是否与request中Accept的header指定的一致
 ProducesRequestCondition produces =
  this.producesCondition.getMatchingCondition(request);

 // 对于上述几个判断,如果匹配上了,那么其返回值都不会为空,因而这里会对每个返回值都进行判断,
 // 如果有任意一个为空,则说明没匹配上,那么就返回null
 if (methods == null || params == null || headers == null
  || consumes == null || produces == null) {
  return null;
 }

 // 对于前面的匹配,都是一些静态属性的匹配,其中最重要的uri的匹配,主要是正则匹配,
 // 就是在下面这个方法中进行的
 PatternsRequestCondition patterns =
  this.patternsCondition.getMatchingCondition(request);
 // 如果URI没匹配上,则返回null
 if (patterns == null) {
  return null;
 }

 // 这里主要是对用户自定义的匹配条件进行匹配
 RequestConditionHolder custom =
  this.customConditionHolder.getMatchingCondition(request);
 if (custom == null) {
  return null;
 }

 // 如果上述所有条件都匹配上了,那么就将匹配结果封装为一个RequestMappingInfo返回
 return new RequestMappingInfo(this.name, patterns, methods, params, headers,
  consumes, produces, custom.getCondition());
}

可以看到,对于一个RequestMapping的匹配,主要包括:RequestMethod,Params,Headers,Consumes,Produces,Uri和自定义条件的匹配,如果这几个条件都匹配上了,才能表明当前RequestMapping与request匹配上了。

3. Interceptor的封装

关于Inteceptor的封装,由前述第一点可以看出,其主要在getHandlerExecutionChain()方法中,如下是该方法的源码:

protected HandlerExecutionChain getHandlerExecutionChain(Object handler,
  HttpServletRequest request) {
 // 将当前handler封装到HandlerExecutionChain对象中
 HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
  (HandlerExecutionChain) handler : new HandlerExecutionChain(handler));

 // 获取当前request的URI,用于MappedInterceptor的匹配
 String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
 // 对当前所有注册的Interceptor进行遍历,如果其是MappedInterceptor类型,则调用其matches()
 // 方法,判断当前Interceptor是否能够应用于该request,如果可以,则添加到HandlerExecutionChain中
 for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
  if (interceptor instanceof MappedInterceptor) {
   MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
   if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
    chain.addInterceptor(mappedInterceptor.getInterceptor());
   }
  } else {
   // 如果当前Interceptor不是MappedInterceptor类型,则直接将其添加到
   // HandlerExecutionChain中
   chain.addInterceptor(interceptor);
  }
 }
 return chain;
}

对于拦截器,理论上,Spring是会将所有的拦截器都进行一次调用,对于是否需要进行拦截,都是用户自定义实现的。这里如果对于URI有特殊的匹配,可以使用MappedInterceptor,然后实现其matches()方法,用于判断当前MappedInterceptor是否能够应用于当前request。

4. 小结

本文首先讲解了Spring是如何通过request进行匹配,从而找到具体处理当前请求的RequestMapping的,然后讲解了Spring是如何封装Interceptor,将HandlerMethod和Interceptor封装为一个HandlerExecutionChain的。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • 深入Android HandlerThread 使用及其源码完全解析

    关联篇:深入Android的消息机制源码详解-Handler,MessageQueue与Looper关系 关联篇:Handler内存泄漏及其解决方案 本篇我们将来给大家介绍HandlerThread这个类,以前我们在使用线程执行一个耗时任务时总会new一个Thread的线程去跑,当任务执行完后,线程就会自动被销毁掉,如果又由新的任务,我们又得新建线程.....我们假设这样的一个情景,我们通过listview去加载图文列表,当我们往下滑动时,这时需要不断去请求网络资源,也就是需要不断开线程去加载

  • Spring MVC学习教程之RequestMappingHandlerAdapter详解

    前言 RequestMappingHandlerAdapter实现了HandlerAdapter接口,顾名思义,表示handler的adapter,这里的handler指的是Spring处理具体请求的某个Controller的方法,也就是说HandlerAdapter指的是将当前请求适配到某个Handler的处理器.RequestMappingHandlerAdapter是HandlerAdapter的一个具体实现,主要用于将某个请求适配给@RequestMapping类型的Handler处理.

  • ASP.NET Core应用错误处理之ExceptionHandlerMiddleware中间件呈现“定制化错误页面”

    前言 DeveloperExceptionPageMiddleware中间件利用呈现出来的错误页面实现抛出异常和当前请求的详细信息以辅助开发人员更好地进行纠错诊断工作,而ExceptionHandlerMiddleware中间件则是面向最终用户的,我们可以利用它来显示一个友好的定制化的错误页面.按照惯例,我们还是先来看看ExceptionHandlerMiddleware的类型定义. public class ExceptionHandlerMiddleware { public Excepti

  • springmvc中RequestMappingHandlerAdapter与HttpMessageConverter的装配讲解

    一.DispatcherServlet 默认装配 RequestMappingHandlerAdapter ,而 RequestMappingHandlerAdapter 默认装配如下 HttpMessageConverter HttpMessageConverter: 2)加入jackson jar 包后,RequestMappingHandlerAdapter 装配的HttpMessageConverter 如下: 二. HttpMessageConverter 1)HttpMessageC

  • mybatis自定义类型处理器TypehHandler示例详解

    前言 当大家使用mybatis作为持久层框架时,在存储和查询数据时,只需要在mapper.xml文件中配置好对应字段的JdbcType和JavaType,mybatis就可以帮我们转化对应的类型.这背后是有mybatis内置的类型转换器做转换(可见源码TypeHandlerRegistry).但是有时候,我们会对某些字段做特殊处理,比如加密和解密.状态转换.类型转换等.这个时候我们需要自定义类型转换器. 类架构 从上面的图中可以看出MyBatis中整个类型处理器实现架构,TypeHandler接

  • Python中logging.NullHandler 的使用教程

    在使用 peewee 框架时,默认是不会出现日志消息的. from peewee import Model, CharField, DateTimeField, IntegerField from peewee_mssql import MssqlDatabase db = MssqlDatabase(database='test', host='.', user='sa', password='sa') class BaseModel(Model): class Meta: database

  • .Net WebApi消息拦截器之MessageHandler的示例

    消息拦截器是一个类,接收 HTTP request并返回 HTTP response,Message handler 继承自抽象类 HttpMessageHandler 可以自定义 MessageHandler,消息拦截器的作用如: 读取或更改请求头 request headers 添加 response headers 在到达 controller 之前,进行参数验证 自定义 Message Handlers 自定义 MessageHandler 需要继承 System.Net.Http.De

  • 深入Android Handler,MessageQueue与Looper关系

    关联篇:HandlerThread 使用及其源码完全解析 关联篇:Handler内存泄漏详解及其解决方案 一说到Android的消息机制,自然就会联想到Handler,我们知道Handler是Android消息机制的上层接口,因此我们在开发过程中也只需要和Handler交互即可,很多人认为Handler的作用就是更新UI,这也确实没错,但除了更新UI,Handler其实还有很多其他用途,比如我们需要在子线程进行耗时的I/O操作,可能是读取某些文件或者去访问网络等,当耗时操作完成后我们可能需要在U

  • MyBatis使用自定义TypeHandler转换类型的实现方法

    MyBatis虽然有很好的SQL执行性能,但毕竟不是完整的ORM框架,不同的数据库之间SQL执行还是有差异. 笔者最近在升级 Oracle 驱动至 ojdbc 7 ,就发现了处理DATE类型存在问题.还好MyBatis提供了使用自定义TypeHandler转换类型的功能. 本文介绍如下使用 TypeHandler 实现日期类型的转换. 问题背景 项目中有如下的字段,是采用的DATE类型: birthday = #{birthday, jdbcType=DATE}, 在更新 Oracle 驱动之前

  • 花样使用Handler与源码分析

    前几天在跟公司大佬讨论一个问题时,看到他使用Handler的一种方式,旁边的同事在说:以前不是这么用的啊.这个问题引发了我的好奇,虽然当时翻清楚道理了,但是还是想给大家分享一下. Handler在之前也说到过他的使用以及源码分析,而且相信大家都知道如何使用它,最常见的使用方法恐怕就是下面这种了: Handler handler = new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessa

随机推荐