SpringMVC之RequestMappingInfo詳解之合併&請求匹配&排序
寫在前面
1.RequestMapping 概述
先來看一張圖:
從這張圖,我們可以發現幾個規律:
-
@RequestMapping 的註解屬性中,除了 name 不是陣列,其他註解屬性都支援陣列。
-
@RequestMapping 的註解屬性中,除了 method 屬性型別是列舉型別 RequestMethod,其他註解屬性都用的 String 型別。
-
DefaultBuilder 是 RequestMappingInfo 的私有靜態內部類,該類設計上使用了建造者模式。
-
在 DefaultBuilder # build() 方法中,除 mappingName 以外的屬性都被用來建立 RequestCondition
-
DefaultBuilder # build() 返回值是 RequestMappingInfo,該物件的建構函式包含 name 以及多個RequestCondition 的子類。
本節接下來對各個引數逐組進行說明,熟悉的同學可以跳過。
其中,@RequestMapping 的註解屬性 RequestMethod[ ] method() 的陣列元素會通過建構函式傳遞給 RequestMethodsRequestCondition ,用於構成成員變數 Set<RequestMethod> methods ,也比較簡單,不多贅述。
params 和 headers
params 和 headers 相同點:均有以下三種表示式格式:
-
!param1: 表示允許不含有 param1 請求引數/請求頭引數(以下簡稱引數)
-
param2!=value2:表示允許不包含 param2 或者 雖然包含 param2 引數但是值不等於 value2;不允許包含param2引數且值等於value2
-
param3=value3:表示需要包含 param3 引數且值等於 value3
這三種表示式的解析邏輯來自 AbstractNameValueExpression 點選展開
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)); } }
TIPS:HeadersRequestCondition / ParamsRequestCondition 和 HttpServletRequest 是否能夠匹配,取決於 getMatchingCondition 方法。該方法返回 null 表示不匹配,有返回值表示可以匹配。
params 和 headers 不同點:大小寫敏感度不同:
params: 大小寫敏感:"!Param" 和 "!param" ,前者表示不允許 Param 引數,後者則表示不允許 param 引數。反映在原始碼上,即 ParamsRequestCondition 的成員變數 expressions 包含 2 個 ParamExpression 物件。
headers: 大小寫不敏感:"X-Forwarded-For=unknown" 和 "x-forwarded-for=unknown" 表示式含義是一樣的。反映在原始碼上,即 HeadersRequestCondition 的成員變數 expressions 僅包含 1 個 HeaderExpression 物件。
headers 的額外注意點:
headers={"Accept=application/*","Content-Type=application/*"}
Accept 和 Content-Type 解析得到的 HeaderExpression 不會被新增到 HeadersRequestCondition 中。
private static Collection parseExpressions(String... headers) {
Set expressions = new LinkedHashSet<>();
for (String header : headers) {
HeaderExpression expr = new HeaderExpression(header);
if ("Accept".equalsIgnoreCase(expr.name) || "Content-Type".equalsIgnoreCase(expr.name)) {
continue;
}
expressions.add(expr);
}
return expressions;
}
consumes 和 produces
consumes 和 produces 不同點:
headers 中 Accept=value 的 value 會被 ProducesRequestCondition 解析。
相對地,headers 中 Content-Type=value 的 value 會被 ConsumesRequestCondition 解析。
ProducesRequestCondition # parseExpressions
private Set
parseExpressions(String[] produces, @Nullable String[] headers) {
Setresult = new LinkedHashSet<>();
if (headers != null) {
for (String header : headers) {
HeaderExpression expr = new HeaderExpression(header);
if ("Accept".equalsIgnoreCase(expr.name) && expr.value != null) {
for (MediaType mediaType : MediaType.parseMediaTypes(expr.value)) {
result.add(new ProduceMediaTypeExpression(mediaType, expr.isNegated));
}
}
}
}
for (String produce : produces) {
result.add(new ProduceMediaTypeExpression(produce));
}
return result;
}
ConsumesRequestCondition # parseExpressions
private static Set parseExpressions(String[] consumes, @Nullable String[] headers) {
Set result = new LinkedHashSet<>();
if (headers != null) {
for (String header : headers) {
HeaderExpression expr = new HeaderExpression(header);
if ("Content-Type".equalsIgnoreCase(expr.name) && expr.value != null) {
for (MediaType mediaType : MediaType.parseMediaTypes(expr.value)) {
result.add(new ConsumeMediaTypeExpression(mediaType, expr.isNegated));
}
}
}
}
for (String consume : consumes) {
result.add(new ConsumeMediaTypeExpression(consume));
}
return result;
}
consumes 和 produces 相同點:均有正反 2 種表示式
-
肯定表示式:"text/plain"
-
否定表示式:"!text/plain"
常見的型別,可以從 org.springframework.http.MediaType 引用,比如 MediaType.APPLICATION_JSON_VALUE = "application/json"。
MimeTypeUtils.parseMimeType 這個靜態方法可以將字串轉換為 MimeType:
public static MimeType parseMimeType(String mimeType) {
// 驗證成分是否齊全
if (!StringUtils.hasLength(mimeType)) {
throw new InvalidMimeTypeException(mimeType, "'mimeType' must not be empty");
}
// 如果包含分號(;),那麼就取分號之前的部分進行解析
int index = mimeType.indexOf(';');
String fullType = (index >= 0 ? mimeType.substring(0, index) : mimeType).trim();
if (fullType.isEmpty()) {
throw new InvalidMimeTypeException(mimeType, "'mimeType' must not be empty");
}
// 遇上單個星號(*)轉換成全萬用字元(*/*)
if (MimeType.WILDCARD_TYPE.equals(fullType)) {
fullType = "*/*";
}
// 斜槓左右兩邊分別是 type 和 subType
int subIndex = fullType.indexOf('/');
if (subIndex == -1) {
throw new InvalidMimeTypeException(mimeType, "does not contain '/'");
}
// subType 為空,丟擲異常
if (subIndex == fullType.length() - 1) {
throw new InvalidMimeTypeException(mimeType, "does not contain subtype after '/'");
}
String type = fullType.substring(0, subIndex);
String subtype = fullType.substring(subIndex + 1, fullType.length());
// type 為 *, subType 不為 * 是不合法的萬用字元格式, 例如 */json
if (MimeType.WILDCARD_TYPE.equals(type) && !MimeType.WILDCARD_TYPE.equals(subtype)) {
throw new InvalidMimeTypeException(mimeType, "wildcard type is legal only in '*/*' (all mime types)");
}
// 解析引數部分
Map parameters = null;
do {
int nextIndex = index + 1;
boolean quoted = false;
while (nextIndex < mimeType.length()) {
char ch = mimeType.charAt(nextIndex);
if (ch == ';') {
// 雙引號之間的分號不能作為引數分割符,比如 name="Sam;Uncle" ,掃描到分號時,不會退出迴圈
if (!quoted) {
break;
}
}
else if (ch == '"') {
quoted = !quoted;
}
nextIndex++;
}
String parameter = mimeType.substring(index + 1, nextIndex).trim();
if (parameter.length() > 0) {
if (parameters == null) {
parameters = new LinkedHashMap<>(4);
}
// 等號分隔引數key和value
int eqIndex = parameter.indexOf('=');
// 如果沒有等號,這個引數不會被解析出來,比如 ;hello; ,其中 hello 就不會被解析為引數
if (eqIndex >= 0) {
String attribute = parameter.substring(0, eqIndex).trim();
String value = parameter.substring(eqIndex + 1, parameter.length()).trim();
parameters.put(attribute, value);
}
}
index = nextIndex;
}
while (index < mimeType.length());
try {
// 建立並返回一個 MimeType 物件
return new MimeType(type, subtype, parameters);
}
catch (UnsupportedCharsetException ex) {
throw new InvalidMimeTypeException(mimeType, "unsupported charset '" + ex.getCharsetNam
}
catch (IllegalArgumentException ex) {
throw new InvalidMimeTypeException(mimeType, ex.getMessage());
}
}
MimeType 由三部分組成:類 type,子類 subType ,引數 parameters。
字串結構為 type/subType;parameter1=value1;parameter2=value2;,常見的規則:
-
type 和 subType 不可以為空。
-
分號 ; 可以用來分開 mineType 和 引數,還可以分隔多個引數。
-
雙引號""之間的分號 ; 將不會被識別為分隔符。
-
如果 type 已經使用了
*
,subType 就只能是*
。這種表示式寫法是不合法的,無法被解析。*/json
path 和 value
1.如果一個註解中有一個名稱為 value 的屬性,且你只想設定value屬性(即其他屬性都採用預設值或者你只有一個value屬性),那麼可以省略掉“value=”部分。
If there is just one element named value, then the name can be omitted. Docs. here
// 就像這樣使用,十分熟悉的“味道”
@RequestMapping("/user")
public class UserController { ... }
2.path 和 value 不能同時有值。
import org.junit.Test;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import java.lang.reflect.Method;
import java.util.Arrays;
public class AnnotationTest {
@Test
@RequestMapping(path = "hello", value = "hi")
public void springAnnotation() throws NoSuchMethodException {
Method springAnnotation = AnnotationTest.class.getDeclaredMethod("springAnnotation");
RequestMapping annotation = AnnotatedElementUtils.findMergedAnnotation(springAnnotation, RequestMapping.class);
System.out.println("value=" + Arrays.toString(annotation.value()));
System.out.println("path=" + Arrays.toString(annotation.path()));
}
}
上面這段程式碼會丟擲一個 AnnotationConfigurationException 異常: path 和 value 只允許用其中一個。
org.springframework.core.annotation.AnnotationConfigurationException: In annotation [org.springframework.web.bind.annotation.RequestMapping] declared on public void coderead.springframework.requestmapping.AnnotationTest.springAnnotation() throws java.lang.NoSuchMethodException and synthesized from [@org.springframework.web.bind.annotation.RequestMapping(path=[hello], headers=[], method=[], name=, produces=[], params=[], value=[], consumes=[])] , attribute 'value' and its alias 'path' are present with values of [{hi}] and [{hello}], but only one is permitted.
RequestMappingHandlerMapping # createRequestMappingInfo 方法,獲取註解就是用的 AnnotatedElementUtils.findMergedAnnotation 方法。
合併 combine
-
合併字串:
1. 如果類和方法的 @RequestMapping 註解只有其中一個聲明瞭 name 屬性,那麼選取不為 null 的這條即可。
2. name:如果類和方法上都聲明瞭 name 屬性,那麼需要用 # 連線字串 -
取並集:
+ 選取類和方法的 @RequestMapping 註解屬性 method[] / headers[] / params[] 的合集
+ RequestMethodsRequestCondition
+ HeadersRequestCondition
+ ParamsRequestCondition -
合併字串且取並集
+ PatternsRequestCondition:如果類和方法上都聲明瞭 value[] / path[] 屬性,連線類和方法上的字串組成新的表示式。
+ 具體的合併規則參考 AntPathMatcher#combine
+ 並集數量的類註解 value 陣列長度 * 方法註解 value 陣列長度 - 重複合併結果數量
類路徑 | 方法路徑 | 合併路徑 |
---|---|---|
/* | /hotel | /hotel |
/. | /*.html | /*.html |
/hotels/* | /booking | /hotels/booking |
/hotels/** | /booking | /hotels/**/booking |
/{foo} | /bar | /{foo}/bar |
- 細粒度覆蓋粗粒度:
如果類和方法上的 @RequestMapping 註解的 consumes[] 和 produces[] 都不為 null,則方法上的註解屬性覆蓋類上的註解屬性
+ ConsumesRequestCondition
+ ProducesRequestCondition
請求匹配 getMatchingCondition
- 首先是 RequestMethodsRequestCondition ,這個比較是最簡單的,只有有一個 method 和請求的 http 報文的 method 相同就就算匹配了
- 接著是比較簡單的一類表示式 ParamsRequestCondition,HeadersRequestCondition,ConsumesRequestCondition,ProducesRequestCondition
- 最後才是 PatternsRequestCondition
只有一個條件不匹配,就直接返回 null,否則繼續執行到所有條件都匹配完成。
排序 compareTo
當存在多個 Match 物件時,自然要排出個次序來,因此,需要用到 compareTo 方法
優先順序:
- 不包含
**
優先於 包含**
- 一個
{param}
或者一個*
記數一次,計數值小的優先 - @PathVariable 字串短優先匹配:
{age}
>{name}
/*
萬用字元用得少的優先- @PathVariable 用得少的優先
假如在一個 Controller 中同時包含 /prefix/info
, /prefix/{name}
, /prefix/*
, /prefix/**
這四個模式:
請求url | 匹配 patterns |
---|---|
/prefix/info | /prefix/info |
/prefix/hello | /prefix/{name} |
/prefix | /prefix/* |
/prefix/ | /prefix/** |
/prefix/abc/123 | /prefix/** |
總結
在 Web 應用啟動時,@RequestMapping 註解解析成 RequestMappingInfo 物件,並且註解的每個屬性都解析成一個對應的 RequestCondition。
通過對條件的篩選,選出符合條件的 RequestMappingInfo,如果包含多個 RequestMappingInfo,需要對條件進行排序,再選出優先順序最高的一個 RequestMappingInfo。
最後再通過 RequestMappingInfoHandlerMapping 獲取對應的 HandlerMethod ,然後就可以封裝執行過程了。