Spring MVC RequestMappingInfo路由条件匹配

前言

我们已经知道,被@RequestMapping标注的方法会被解析为 HandlerMethod,它也是 Spring MVC 中最常用的 Handler 类型。现在的问题是,HTTP 请求是如何路由到对应的 HandlerMethod?你可能脱口而出:根据请求的 Url 匹配啊!的确,Url 匹配是最简单一种规则,但事实上 Spring MVC 的功能之丰富超乎你想象。

比如,你可以在两个方法上定义相同的请求路径,根据请求参数的不同路由到不同的方法上处理:

java 复制代码
@GetMapping(path = "api", params = "id")
public Object apiV1(@RequestParam("id") String id) {
    return "apiV1:" + id;
}

@GetMapping(path = "api", params = "name")
public Object apiV2(@RequestParam("name") String name) {
    return "apiV2:" + name;
}

除此之外,你还可以根据请求方法、请求头、Content-Type、Accept、甚至自定义更复杂的路由规则。Spring MVC 处理这套路由机制是很复杂的,来看下是怎么实现的吧。

RequestCondition

首先我们重新认识一下@RequestMapping注解,先了解它支持配置哪些路由条件。

java 复制代码
public @interface RequestMapping {
  String[] path() default {};
  RequestMethod[] method() default {};
  String[] params() default {};
  String[] headers() default {};
  String[] consumes() default {};
  String[] produces() default {};
}
  • path:请求的路径数组,可以使用 Ant 模式匹配
  • method:请求的方法数组
  • params:请求的参数数组,可以指定键或键值
  • headers:请求头数组,可以指定键或键值
  • consumes:指定只接收哪些媒体类型参数的请求,匹配 Content-Type 请求头,例如:application/json
  • produces:指定返回的媒体类型,匹配 Accept 请求头,例如:text/html

这里举一个典型示例:

java 复制代码
@RequestMapping(
        path = "get/{userId}",
        method = {RequestMethod.GET},
        headers = {"token"},
        params = {"name"},
        consumes = {"application/json"},
        produces = {"text/html"}
)
public Object getUser(@PathVariable("userId") String userId,
                      @RequestParam("name") String name) {

    return null;
}

在这个示例中,请求的 Url 必须匹配"get/{userId}"、请求方法必须是 GET、请求头必须携带 token、必须有一个 name 参数、Content-Type 必须是"application/json"、Accept 必须包含"text/html",满足以上所有条件请求才会路由到该方法。

因为要匹配的条件特别多,Spring MVC 专门为此抽象了一个接口:RequestCondition,它代表 HTTP 请求要匹配的条件。

java 复制代码
public interface RequestCondition<T> {
  T combine(T other);
  
  T getMatchingCondition(HttpServletRequest request);
   
  int compareTo(T other, HttpServletRequest request);
}
  • combine:条件合并,例如合并方法和类上的@RequestMapping注解
  • getMatchingCondition:获得匹配的条件,例如有多个 path,返回匹配上的 path,返回 null 表示不匹配
  • compareTo:条件比较,匹配到多个候选项时选出一个最佳项

RequestCondition 有很多实现类,@RequestMapping里的每一项规则都对应一个具体的实现类:

如图所示,请求路径匹配对应 PathPatternsRequestCondition、请求方法条件匹配对应 RequestMethodsRequestCondition、请求头条件匹配对应 HeadersRequestCondition 等等。CompositeRequestCondition 组合了多个 RequestCondition,本身不具备条件匹配的能力,委托给内部的 requestConditions。

所有的实现类就不一一分析了,我们看几个常用的。

RequestMethodsRequestCondition

RequestMethodsRequestCondition 是请求方法条件类,内部用一个 Set 记录支持的请求方法,通过构造函数传入:

java 复制代码
private RequestMethodsRequestCondition(Set<RequestMethod> methods) {
	this.methods = methods;
}

匹配规则也很简单,就是看 methods 是否包含了:

java 复制代码
private RequestMethodsRequestCondition matchRequestMethod(String httpMethodValue) {
	RequestMethod requestMethod;
	try {
		requestMethod = RequestMethod.valueOf(httpMethodValue);
		if (getMethods().contains(requestMethod)) {
			return requestMethodConditionCache.get(httpMethodValue);
		}
		if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.GET)) {
			return requestMethodConditionCache.get(HttpMethod.GET.name());
		}
	}
	catch (IllegalArgumentException ex) {
		// Custom request method
	}
	return null;
}

HeadersRequestCondition

HeadersRequestCondition 是请求头条件类,支持多种匹配方式:

  • key:请求头必须有 key
  • !key:请求头必须没有 key
  • key=value:请求头必须有 key、且值必须是 value

配置通过构造函数传入,因为匹配的复杂性,Spring MVC 会把配置的字符串解析成表达式 HeaderExpression。

java 复制代码
public HeadersRequestCondition(String... headers) {
	// 解析表达式
	this.expressions = parseExpressions(headers);
}

解析过程并不复杂,内部用三个变量记录键值、以及是否取反:

java 复制代码
AbstractNameValueExpression(String expression) {
	int separator = expression.indexOf('=');
	if (separator == -1) {
		this.isNegated = expression.startsWith("!");
		this.name = (this.isNegated ? expression.substring(1) : expression);
		this.value = null;
	}
	else {
		this.isNegated = (separator > 0) && (expression.charAt(separator - 1) == '!');
		this.name = (this.isNegated ? expression.substring(0, separator - 1) : expression.substring(0, separator));
		this.value = parseValue(expression.substring(separator + 1));
	}
}

匹配的过程也不复杂,挨个和所有 HeaderExpression 匹配,必须所有都匹配通过才行:

java 复制代码
public HeadersRequestCondition getMatchingCondition(HttpServletRequest request) {
	if (CorsUtils.isPreFlightRequest(request)) {
		return PRE_FLIGHT_MATCH;
	}
	for (HeaderExpression expression : this.expressions) {
		if (!expression.match(request)) {
			return null;
		}
	}
	return this;
}

RequestMappingInfo

在 RequestCondition 众多实现类中,有一个最为关键,也是我们要重点分析的------RequestMappingInfo,它可以看作是@RequestMapping的封装类。

它聚合了@RequestMapping注解的所有条件对象,内部用七个变量来表示,匹配时必须所有的条件都匹配通过才算,任一条件不匹配都会导致失败。

java 复制代码
public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {
  // 请求路径条件 path = {"/api"}
  private final PathPatternsRequestCondition pathPatternsCondition;
  // 方法条件 method = {RequestMethod.GET, RequestMethod.POST}
  private final RequestMethodsRequestCondition methodsCondition;
  // 参数条件 params = {"name"}
  private final ParamsRequestCondition paramsCondition;
  // 请求头条件 headers = {"token"}
  private final HeadersRequestCondition headersCondition;
  // 请求头Content-Type条件 consumes = {"application/json"}
  private final ConsumesRequestCondition consumesCondition;
  // 请求头Accept条件 produces = {"text/html"}
  private final ProducesRequestCondition producesCondition;
  // 自定义条件
  private final RequestConditionHolder customConditionHolder;
}

它是如何被构建的呢???

AbstractHandlerMethodMapping 属性填充完毕后,会自动检测容器内所有 ControllerBean,然后解析 Bean 上所有被 @RequestMapping 标注的方法,源码在 RequestMappingHandlerMapping#getMappingForMethod 。

  • 先解析方法上的注解,构建 RequestMappingInfo 实例
  • 再解析类上的注解,构建 RequestMappingInfo 实例
  • 最后将二者合并
java 复制代码
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
	// 基于方法构建RequestMappingInfo
	RequestMappingInfo info = createRequestMappingInfo(method);
	if (info != null) {
		// 基于类构建RequestMappingInfo
		RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
		if (typeInfo != null) {
			// 二者合并 比如类上path=/user 方法上path=/get/{id} 匹配的完整路径:/user/get/{id}
			info = typeInfo.combine(info);
		}
		String prefix = getPathPrefix(handlerType);
		if (prefix != null) {
			info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
		}
	}
	return info;
}

RequestMappingInfo 实例化采用建造者模式,通过内部类 DefaultBuilder 完成实例化,没什么复杂的,就是读取注解属性完成实例化:

java 复制代码
protected RequestMappingInfo createRequestMappingInfo(
		RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {

	RequestMappingInfo.Builder builder = RequestMappingInfo
			.paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
			.methods(requestMapping.method())
			.params(requestMapping.params())
			.headers(requestMapping.headers())
			.consumes(requestMapping.consumes())
			.produces(requestMapping.produces())
			.mappingName(requestMapping.name());
	if (customCondition != null) {
		builder.customCondition(customCondition);
	}
	return builder.options(this.config).build();
}

@RequestMapping注解上的配置,就对应了 RequestMappingInfo 内部的七个变量。

最后,我们看一下 RequestMappingInfo 的匹配过程吧。其实非常简单,就是按顺序依次匹配内部的七个条件对象,全部匹配通过才算通过。

java 复制代码
public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
	RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
	if (methods == null) {
		return null;
	}
	ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
	if (params == null) {
		return null;
	}
	HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
	if (headers == null) {
		return null;
	}
	ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
	if (consumes == null) {
		return null;
	}
	ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
	if (produces == null) {
		return null;
	}
	PathPatternsRequestCondition pathPatterns = null;
	if (this.pathPatternsCondition != null) {
		pathPatterns = this.pathPatternsCondition.getMatchingCondition(request);
		if (pathPatterns == null) {
			return null;
		}
	}
	PatternsRequestCondition patterns = null;
	if (this.patternsCondition != null) {
		patterns = this.patternsCondition.getMatchingCondition(request);
		if (patterns == null) {
			return null;
		}
	}
	RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
	if (custom == null) {
		return null;
	}
	return new RequestMappingInfo(this.name, pathPatterns, patterns,
			methods, params, headers, consumes, produces, custom, this.options);
}

尾巴

Spring MVC 把 HTTP 请求路由到 HandlerMethod 是一个极其复杂的过程,要匹配的条件项之多,为此专门抽象出了 RequestCondition 接口,代表请求要匹配的条件。@RequestMapping注解的每一项配置都对应一个 RequestCondition 实现类,每个实现类只负责自己的条件匹配逻辑。

最后,为了聚合这些条件,Spring MVC 还提供了 RequestMappingInfo 子类,内部用七个变量记录注解配置的条件对象,方便一次性完成匹配。有了 RequestMappingInfo 对象,Spring MVC 就能轻松把请求路由到 HandlerMethod。

相关推荐
xlsw_2 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹3 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭4 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫4 小时前
泛型(2)
java
超爱吃士力架4 小时前
邀请逻辑
java·linux·后端
南宫生4 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石4 小时前
12/21java基础
java
李小白664 小时前
Spring MVC(上)
java·spring·mvc
GoodStudyAndDayDayUp4 小时前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea
装不满的克莱因瓶5 小时前
【Redis经典面试题六】Redis的持久化机制是怎样的?
java·数据库·redis·持久化·aof·rdb