SpringMVC之RequestMappingInfo詳解之合並&請求匹配&排序


寫在前面

1.RequestMapping 概述

先來看一張圖:

從這張圖,我們可以發現幾個規律:

  1. @RequestMapping 的注解屬性中,除了 name 不是數組,其他注解屬性都支持數組。

  2. @RequestMapping 的注解屬性中,除了 method 屬性類型是枚舉類型 RequestMethod,其他注解屬性都用的 String 類型。

  3. DefaultBuilderRequestMappingInfo 的私有靜態內部類,該類設計上使用了建造者模式。

  4. DefaultBuilder # build() 方法中,除 mappingName 以外的屬性都被用來創建 RequestCondition 的子類實例。

  5. DefaultBuilder # build() 返回值是 RequestMappingInfo,該對象的構造函數包含 name 以及多個RequestCondition 的子類。

本節接下來對各個參數逐組進行說明,熟悉的同學可以跳過。

其中,@RequestMapping 的注解屬性 RequestMethod[ ] method() 的數組元素會通過構造函數傳遞給 RequestMethodsRequestCondition ,用於構成成員變量 Set<RequestMethod> methods ,也比較簡單,不多贅述。

params 和 headers

params 和 headers 相同點均有以下三種表達式格式:

  1. !param1: 表示允許不含有 param1 請求參數/請求頭參數(以下簡稱參數)

  2. param2!=value2:表示允許不包含 param2 或者 雖然包含 param2 參數但是值不等於 value2;不允許包含param2參數且值等於value2

  3. 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));
	}
}

TIPSHeadersRequestCondition / ParamsRequestConditionHttpServletRequest 是否能夠匹配,取決於 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/*"}
AcceptContent-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 不同點

headersAccept=valuevalue 會被 ProducesRequestCondition 解析。

相對地,headersContent-Type=valuevalue 會被 ConsumesRequestCondition 解析。

ProducesRequestCondition # parseExpressions

private Set parseExpressions(String[] produces, @Nullable String[] headers) {
Set result = 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;,常見的規則:

  • typesubType 不可以為空。

  • 分號 ; 可以用來分開 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.pathvalue 不能同時有值。

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 異常: pathvalue 只允許用其中一個。

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

  1. 首先是 RequestMethodsRequestCondition ,這個比較是最簡單的,只有有一個 method 和請求的 http 報文的 method 相同就就算匹配了
  2. 接着是比較簡單的一類表達式 ParamsRequestCondition,HeadersRequestCondition,ConsumesRequestCondition,ProducesRequestCondition
  3. 最后才是 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 ,然后就可以封裝執行過程了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM