前言
我们已经知道,被@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。