tomcat + spring mvc原理(十):spring mvc如何将请求投送到Controller中的方法2

前言

    原理九介绍了HandlerMapping组件的接口标准和AbstractHandlerMapping抽象类的主要功能——加载相关的Interceptor。本期主要讲HandlerMethodMapping子类的实现细节,具体是如何获取Controller中的HandlerMethod的过程,从而完成对HandlerMapping组件@RequestMapping注解对应的请求投送。

AbstractHandlerMethodMapping和其子类

    所谓的HandlerMethodMapping指的就是将请求映射到处理该请求的Method,即使用@RequestMapping、@GetMapping或者@PostMapping注解的方法,在这里有专门的类型HandlerMethod与之对应,现在项目常用的都是这种方式。首先在看代码之前,自行想一下如果简单实现这样一种映射要如何做。一般是使用map,将匹配条件(比如url)和method组成键值对。有时候可能一个匹配条件可以匹配多个HandlerMethod(然后选出一个最优的),所以一对一的映射可能解决不了问题。实际上,AbstractHandlerMethodMapping使用的也是map的这种思路,接下来看它是如何处理这个问题的。

匹配条件

    在理解如何匹配到我们自定义的方法(由@RequestMapping、@GetMapping或者@PostMapping注解的)之前,先要了解一下匹配条件有哪些以及这些匹配条件是如何实现的。匹配条件的理解需要追溯到@RequestMapping这个注解。

public @interface RequestMapping {
    String name() default "";

    @AliasFor("path")
    String[] value() default {};

    @AliasFor("value")
    String[] path() default {};

    RequestMethod[] method() default {};

    String[] params() default {};

    String[] headers() default {};

    String[] consumes() default {};

    String[] produces() default {};
}

其中name、path、value都是指url,method是指请求的类型,GET、POST、PUT、DELETE等;params是指定request中必须包含某些参数值;headers是指request中必须包含某些指定的header值;consumes代表限定请求的提交内容类型(Content-Type),类型如application/json, text/html;produces指定返回内容的类型,需要在请求头中(Accept属性)包含指定类型。这些都是属于匹配条件。
    spring mvc为这些匹配条件定义了一个通用的接口RequestCondition。

public interface RequestCondition<T> {
  //将当前匹配条件和另一个匹配条件结合,比如url类型的和method类型
	T combine(T other);

 //获取匹配request的匹配条件,比如一个匹配条件是多个值的url匹配,
 /该方法会返回匹配传入request的匹配url条件。
	@Nullable
	T getMatchingCondition(HttpServletRequest request);

  //对比在当前传入request条件下两个匹配条件。该方法假设两个匹配
  //条件是通过getMatchingCondition产生的,和这个request有相
  //关关系。
	int compareTo(T other, HttpServletRequest request);

}

这个接口的抽象类实现AbstractRequestCondition主要重载了equals()、hashCode()和toString()方法,其子类代表了具体的6种类型的匹配条件。
tomcat + spring mvc原理(九):spring mvc如何将请求投送到Controller中的方法(二)-RequestCondition.png

    你应该发现了,实际上子类有8个。前六个:

类名功能
PatternsRequestConditionurl匹配
RequestMethodsRequestCondition请求类型匹配
HeadersRequestConditionheader属性匹配
ParamsRequestCondition参数值匹配
ProducesRequestCondition返回内容类型匹配
ConsumesRequestCondition请求提交类型匹配

最后CompositeRequestCondition代表是一个复合类型,可以将其他匹配类型封装到自己的一个类实例中。RequestConditionHolder是一个好东西,我记得在原理八

这些属性、配置都存储在当前环境中,可以通过Holder获取。也就是说,在处理请求的过程中RequestContextHolder能够获取所有和请求相关的内容,大家在业务代码编写中也可以使用LocaleContextHolder和RequestContextHolder这两个Holder。

RequestConditionHolder是一样的道理,也可以在编写业务代码时获取请求条件匹配的信息,算是框架提供的一个环境参数获取的工具。这些子类实现了接口的方法逻辑,可以保证在HandlerMethodMapping中被正常调用,代码不是非常复杂,大家有兴趣可以自己去看。

AbstractHandlerMethodMapping及其子类初始化

    AbstractHandlerMethodMapping是一个泛型类,定义了一个MappingRegistry内部类,并且内部初始化并持有了一个
MappingRegistry的类对象。顾名思义,MappingRegistry这个类的主要功能就是管理注册匹配条件的map。

class MappingRegistry {

  private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();

  private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();

  private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();

  private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();

  private final Map<T, MappingRegistration<T>> registry = new HashMap<>();

}

    首先理解T代表什么。T是AbstractHandlerMethodMapping中定义的泛型,统一用来代表匹配条件。urlLookupMap用来存储url和匹配条件的映射关系,url是pattern样式的,因此可以使用通配符。这里的map是MultiValue类型,一个键对应多个匹配条件。mappingLookup是用来存储匹配条件和HanlerMethod(Controller下被@RequestMapping注解方法的专用类型)的映射关系。nameLookup里的name是使用HandlerMethodMappingNamingStrategy策略的实现类从HandlerMethod中解析出来的,是用在mvc URI解析过程的变量。对于corsLookup,在这篇文章的上半部中(原理九)有提到跨站请求,这里就是HandlerMethod和跨站请求配置的映射,当然是在该请求为跨站请求的前提下才会用到该映射。registry会把除了corsLookup映射的其他所有映射统一到一起,这样registry的每一条记录中会包括匹配条件、对应HandlerMethod、完整url和映射名,其中匹配条件匹配条件和HandlerMethod不能为空。
    urlLookup和mappingLookup相互配合,先通过url获取匹配条件,再通过匹配条件获取HandlerMethod。有时候获取的匹配条件可能不止一个,这就需要选出最优的一个。
    AbstractHandlerMethodMapping初始化的方式是实现了InitializingBean接口,这个接口只有一个方法。

public interface InitializingBean {
	void afterPropertiesSet() throws Exception;
}

这个接口的神奇之处在于,当BeanFactory把一个bean内的所有属性都装备好之后,这个接口唯一的方法就会自动被调用。AbstractHandlerMethodMapping在自己实现的afterPropertiesSet方法中调用了initHandlerMethods。

protected void initHandlerMethods() {
  for (String beanName : getCandidateBeanNames()) {
    if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
      processCandidateBean(beanName);
    }
  }
  handlerMethodsInitialized(getHandlerMethods());
}

initHandlerMethods先筛选了beanName前缀为“scopedTarget”的bean,这个前缀一般是用来作为HandlerMethod的bean探测的筛选条件。接下来就对筛选下来的bean一个一个用processCandidateBean方法处理。先说后面的handlerMethodsInitialized方法,这个其实是一个模板方法,让子类有能力在所有HandlerMethod加载完后,可以对HandlerMethod做子类想要进行的操作。不过,子类并没有重载这个方法。
    processCandidateBean中使用isHandler方法对获取的bean的类型进行判断,从而筛选出Handler。逻辑也比较简单,就是通过是否注解有@Controller或者是@RequestMapping来判断。

@Override
protected boolean isHandler(Class<?> beanType) {
  return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
      AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}

因为@GetMapping和@PostMapping注解本质上还是通过@RequestMapping来实现的,所以这两个注解也在筛选的范围内。
    接着需要从被筛选的bean中提取出被注解的Method,并注册到各个map中,detectHandlerMethods做的就是这个工作。processCandidateBean方法在筛选出Handler之后,就调用了detectHandlerMethods方法。

protected void detectHandlerMethods(Object handler) {
  Class<?> handlerType = (handler instanceof String ?
      obtainApplicationContext().getType((String) handler) : handler.getClass());
  //提取和构建
  if (handlerType != null) {
    Class<?> userType = ClassUtils.getUserClass(handlerType);
    Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
        (MethodIntrospector.MetadataLookup<T>) method -> {
          try {
            return getMappingForMethod(method, userType);
          }
          catch (Throwable ex) {
            throw new IllegalStateException("Invalid mapping on handler class [" +
                userType.getName() + "]: " + method, ex);
          }
        });
    if (logger.isTraceEnabled()) {
      logger.trace(formatMappings(userType, methods));
    }

    //注册
    methods.forEach((method, mapping) -> {
      Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
      registerHandlerMethod(handler, invocableMethod, mapping);
    });
  }
}

根据需求,detectHandlerMethods方法可以分为两步,第一步提取Method并构建匹配条件,第二部将Method注册到三大map中。提取Method用的是spring的类方法遍历MethodIntrospector,获取HandlerMethod的匹配条件用的是getMappingForMethod()方法,这个方法是由子类RequestMappingHandlerMapping实现的。主要思路是获取@RequestMapping注解中的各个参数,构造为综合的匹配条件类RequestMappingInfo。RequestMappingInfo是RequestCondition的接口实现,内部包含了@RequestMapping的6种匹配条件。

public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {
    ······
	private final PatternsRequestCondition patternsCondition;

	private final RequestMethodsRequestCondition methodsCondition;

	private final ParamsRequestCondition paramsCondition;

	private final HeadersRequestCondition headersCondition;

	private final ConsumesRequestCondition consumesCondition;

	private final ProducesRequestCondition producesCondition;

    ······
}

detectHandlerMethod方法的第二步为注册。对单个HandlerMethod注册采用的是registerHandlerMethod方法:

protected void registerHandlerMethod(Object handler, Method method, T mapping) {
  this.mappingRegistry.register(mapping, handler, method);
}

public void register(T mapping, Object handler, Method method) {
  this.readWriteLock.writeLock().lock();
  try {
    HandlerMethod handlerMethod = createHandlerMethod(handler, method);
    assertUniqueMethodMapping(handlerMethod, mapping);
    this.mappingLookup.put(mapping, handlerMethod);

    List<String> directUrls = getDirectUrls(mapping);
    for (String url : directUrls) {
      this.urlLookup.add(url, mapping);
    }

    String name = null;
    if (getNamingStrategy() != null) {
      name = getNamingStrategy().getName(handlerMethod, mapping);
      addMappingName(name, handlerMethod);
    }

    CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
    if (corsConfig != null) {
      this.corsLookup.put(handlerMethod, corsConfig);
    }

    this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
  }
  finally {
    this.readWriteLock.writeLock().unlock();
  }
}

registerHandlerMethod方法中实际调用的是mappingRegistry(上文已经介绍过)内部对象的register方法。register先是根据Method构造了HandlerMethod,在判断mappingLookup不存在这种映射关系后,将这对映射添加到了mappingLookup中,然后根据匹配条件生成了url,将url和匹配条件的映射加入到了urlLookup中。后面还根据条件是否满足,填充了nameLookup和corsLookup。最后在registry的map中添加了完整的记录。这样,整个映射初始化构建就完成了。

AbstractHandlerMethodMapping匹配请求

    其实,构建初始化的映射条件是比较重要的,匹配请求就比较简单了。匹配请求的入口是

mapping.getHandler(request);

在AbstractHandlerMapping的实现中引导子类使用模板方法getHandlerInternal,这在原理九中已经介绍过了。这里来看下
AbstractHandlerMethodMapping对getHandlerInternal方法的代码实现。

@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
  String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
  this.mappingRegistry.acquireReadLock();
  try {
    HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
    return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
  }
  finally {
    this.mappingRegistry.releaseReadLock();
  }
}

protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
  List<Match> matches = new ArrayList<>();
  //先通过url获取所有所有匹配条件
  List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
  if (directPathMatches != null) {
    //通过匹配条件获取所有的HandlerMethod放入matches
    //(Match包含匹配条件和Handlermethod)
    addMatchingMappings(directPathMatches, matches, request);
  }
  if (matches.isEmpty()) {
    //如果没有任何匹配条件,将所有都放进去
    // No choice but to go through all mappings...
    addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
  }

  if (!matches.isEmpty()) {
    //通过Match来获取最佳的匹配条件
    Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
    //最佳匹配的排序
    matches.sort(comparator);
    Match bestMatch = matches.get(0);
    if (matches.size() > 1) {
      if (logger.isTraceEnabled()) {
        logger.trace(matches.size() + " matching mappings: " + matches);
      }
      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();
        String uri = request.getRequestURI();
        throw new IllegalStateException(
            "Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
      }
    }
    //设置最佳匹配的HandlerMethod
    request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
    //在请求体中设置了一些其他相关属性
    handleMatch(bestMatch.mapping, lookupPath, request);
    return bestMatch.handlerMethod;
  }
  else {
    return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
  }
}

正如我在源码中注释所展示的那样,整个匹配的过程匹配条件RequestCondition发挥了很大作用,包括其中重载compareTo,equals等方法,帮助进行排序。关于Match排序部分相关的代码还有挺有意思的,有兴趣可以读下。

这样获取了HandlerMethod之后,可以和在父类获取的Interceptor一起,构成一个HandlerExecutionChain返回给DispatcherServlet,这样整个HandlerMapping组件就实现了自己的生命价值。

tomcat + spring mvc原理(一):tomcat原理综述和静态架构
tomcat + spring mvc原理(二):tomcat容器初始化加载和启动
tomcat + spring mvc原理(三):tomcat网络请求的监控与处理1
tomcat + spring mvc原理(四):tomcat网络请求的监控与处理2
tomcat + spring mvc原理(五):tomcat Filter组件实现原理
tomcat + spring mvc原理(六):tomcat WAR包的部署与加载
tomcat + spring mvc原理(七):spring mvc的Servlet和九大标准组件的静态结构与初始化
tomcat + spring mvc原理(八):spring mvc对请求的处理流程
tomcat + spring mvc原理(九):spring mvc如何将请求投送到Controller中的方法 1

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值