JAVA && Spring && SpringBoot2.x — 學(xué)習(xí)目錄
Spring源碼篇(1)—RequestMappingHandlerMapping(Handler的注冊(cè))
Spring源碼篇(2)—RequestMappingInfo與RequestCondition(Handler的映射)
SpringBoot2.x—定制HandlerMapping映射規(guī)則
業(yè)務(wù)場(chǎng)景
場(chǎng)景:項(xiàng)目中有三個(gè)業(yè)務(wù)方法@RequestMapping
配置完全相同萨赁。
//用戶在請(qǐng)求中上送版本1或2時(shí)冰肴,調(diào)用該方法
@ApiVersion({1, 2})
@RequestMapping(value = {"/testApi"})
@ResponseBody
public String testAPIV1(HttpServletResponse response) {
System.out.println("請(qǐng)求進(jìn)入...V1");
return "success-V1";
}
//用戶在請(qǐng)求頭中上送版本3大脉,調(diào)用版本3的方法
@ApiVersion(3)
@RequestMapping(value = {"/testApi"})
@ResponseBody
public String testAPIV3(HttpServletResponse response) {
System.out.println("請(qǐng)求進(jìn)入...V3");
return "success-V3";
}
//用戶不指定版本號(hào)量淌,則調(diào)用最新版本的方法
@RequestMapping(value = {"/testApi"})
@ResponseBody
public String testAPIVX(HttpServletResponse response) {
System.out.println("請(qǐng)求進(jìn)入...VX");
return "success-VX";
}
我們理想是用戶在請(qǐng)求頭中兴垦,上送不同的參數(shù)到忽,如圖1所示:
可以精確的定位到Controller層的某個(gè)方法赔退?
分析
在@RequestMapping中配置headers屬性,也可以根據(jù)請(qǐng)求頭來匹配controller的方法尺铣。但請(qǐng)求中參數(shù)必須和注解參數(shù)相同。不能實(shí)現(xiàn)我們場(chǎng)景中用戶上送某個(gè)請(qǐng)求參數(shù)争舞,都可以匹配到controller中的方法凛忿。
所以,我們需要自定義HandlerMapping的映射規(guī)則竞川,來定制我們的業(yè)務(wù)店溢。
重寫HandlerMapping?
SpringMVC是通過RequestMappingHandlerMapping
來完成請(qǐng)求到HandlerExecutionChain
的映射的委乌。我們要在映射過程中床牧,加入我們自定義的映射邏輯,那么必須要重寫RequestMappingHandlerMapping
遭贸。
1. 如何重寫HandlerMapping
原理:RequestMappingHandlerMapping
是HandlerMapping
實(shí)現(xiàn)類戈咳,根據(jù)請(qǐng)求來映射得到controller
中帶有@RequestMapping
注解方法。實(shí)際上他會(huì)在項(xiàng)目啟動(dòng)時(shí)解析@RequestMapping
注解壕吹,并且將注解的屬性轉(zhuǎn)換為RequestCondition
以便和請(qǐng)求匹配除秀。而SpringMVC
給我們預(yù)留了獲取自定義條件空實(shí)現(xiàn)方法。故我們需要重寫方法算利,返回自定義的RequestCondition
條件册踩,那么該條件會(huì)影響請(qǐng)求的映射。
- 在含有
@RequestMapping
的方法/類上加上自定義注解效拭,以便自定義RequestCondition
得到參數(shù)暂吉。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
//該方法適配的版本號(hào)
int[] value();
}
- 當(dāng)SpringMVC解析帶有
@RequestMapping
方法/類時(shí)會(huì)調(diào)用下面的方法。若方法/類上存在自定義注解@ApiVersion
則生成自定義RequestCondition
對(duì)象缎患,以便影響請(qǐng)求映射得到mapping慕的。若不存在自定義注解,則返回null挤渔。
public class CustomRequestMappingHandlerMapping2 extends RequestMappingHandlerMapping {
@Override
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
//讀取方法上參數(shù)配置
return get(handlerType);
}
@Override
protected RequestCondition<?> getCustomMethodCondition(Method method) {
//讀取方法上參數(shù)配置
return get(method);
}
//每一個(gè)@RequestMapping都會(huì)解析
private ApiVersionCondition get(AnnotatedElement element) {
ApiVersion apiVersion = AnnotatedElementUtils.findMergedAnnotation(element, ApiVersion.class);
//有些@RequestMapping方法上沒有@ApiVersion注解肮街,故我們返回null。
return apiVersion != null ?
new ApiVersionCondition(Arrays.stream(apiVersion.value()).boxed().toArray(Integer[]::new)) : null;
}
}
- 該類繼承了
RequestCondition
接口判导,完成的功能:(1)類上自定義注解和方法自定義注解的屬性合并嫉父。(2)若RequestMappingInfo對(duì)象中@RequestMapping
屬性均和請(qǐng)求匹配時(shí)沛硅,再與請(qǐng)求進(jìn)行匹配。(3)若請(qǐng)求與多個(gè)mapping的自定義RequestCondition匹配绕辖,自定義RequestCondition需要指定一個(gè)最優(yōu)的mapping摇肌。
public class ApiVersionCondition extends AbstractRequestCondition<ApiVersionCondition> {
//版本號(hào)數(shù)組
private final Set<Integer> versions;
public ApiVersionCondition(Integer... versions) {
this(Arrays.asList(versions));
}
private ApiVersionCondition(Collection<Integer> versions) {
//將版本號(hào)進(jìn)行倒序排序
LinkedHashSet<Integer> integers = new LinkedHashSet<>(versions);
integers.stream().sorted(Comparator.reverseOrder());
this.versions = Collections.unmodifiableSet(integers);
}
@Override
protected Collection<?> getContent() {
return this.versions;
}
@Override
protected String getToStringInfix() {
return "||";
}
//參數(shù)的合并
@Override
public ApiVersionCondition combine(ApiVersionCondition other) {
return other.versions == null ? this : other;
}
//mapping對(duì)象屬性與請(qǐng)求進(jìn)行匹配
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
//查看該條件是否和請(qǐng)求匹配
String header = request.getHeader("Api-Version");
//若存在條件,但用戶并未上送版本號(hào)仪际,則該mapping不匹配
if (StringUtils.isBlank(header)) {
return null;
}
//若請(qǐng)求和條件匹配围小,則放行
for (Integer version : versions) {
if (header.equals(version.toString())) {
return this;
}
}
return null;
}
/**
* mapping中apiVersion(1,2) 另一個(gè)mapping中(2,3)
* 此時(shí)用戶請(qǐng)求版本為2,那么優(yōu)先執(zhí)行(2,3)的mapping
*/
@Override
public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
Integer thisVersion = this.versions.stream().findFirst().orElse(-1);
Integer otherVersion = other.versions.stream().findFirst().orElse(-1);
return otherVersion - thisVersion;
}
}
- 將自定義HandlerMapping替換RequestMappingHandlerMapping树碱。
@Component
public class CustomWebMvcRegistrations implements WebMvcRegistrations {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
CustomRequestMappingHandlerMapping2 handlerMapping=new CustomRequestMappingHandlerMapping2();
handlerMapping.setOrder(0);
return handlerMapping;
}
}
2. 創(chuàng)建ApiVersionCondition注意事項(xiàng)
定制HandlerMapping的原理實(shí)際上是使用了模板方法模式肯适,父類已經(jīng)定義好了算法骨架(即自定義Condition如何執(zhí)行)。那么我們?cè)趧?chuàng)建ApiVersionCondition時(shí)成榜,需要注意些什么呢疹娶?
2.1 類注解和方法注解的合并問題
@RequestMapping
為例可以在類和方法上使用,而自定義注解@ApiVersion(可以將其看做@RequestMapping的屬性)
伦连,也是可以在方法/類上配置,那么他們?nèi)绾芜M(jìn)行合并處理呢钳垮?
//類級(jí)別mapping與方法級(jí)別mapping進(jìn)行合并
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
//讀取method上的@RequestMapping注解惑淳。
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
//讀取類上的@RequestMapping注解。
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
//類上的mapping對(duì)象去合并方法的mapping對(duì)象饺窿。
info = typeInfo.combine(info);
}
...
return info;
}
按照RequestMappingHandlerMapping的處理歧焦,是類級(jí)別的RequestMapping調(diào)用的合并方法,那么在ApiVersionCondition
中肚医,若想方法覆蓋類級(jí)別自定義注解屬性绢馍,需要返回other對(duì)象。
2.2 無@ApiVersion時(shí)的匹配問題
每個(gè)RequestMappingInfo的屬性條件都會(huì)和Request進(jìn)行匹配肠套。當(dāng)controller的Handler方法不存在@ApiVersion
注解時(shí)舰涌,既不會(huì)執(zhí)行我們自定義的映射邏輯。那么該mapping與請(qǐng)求的匹配規(guī)則又是如何的你稚。
//源碼:org.springframework.web.servlet.mvc.condition.RequestConditionHolder#getMatchingCondition
public RequestConditionHolder getMatchingCondition(HttpServletRequest request) {
//若自定義條件為null瓷耙,則為匹配成功
if (this.condition == null) {
return this;
}
//調(diào)用自定義條件的匹配方法。
RequestCondition<?> match = (RequestCondition<?>) this.condition.getMatchingCondition(request);
//若返回this刁赖,則證明匹配成功搁痛。否則,匹配失敗宇弛。
return (match != null ? new RequestConditionHolder(match) : null);
}
在源碼中鸡典,我們可以看到,若是@RequestMapping
的條件都匹配的情況下枪芒,會(huì)調(diào)用getMatchingCondition
執(zhí)行自定義匹配條件彻况,若該條件我null谁尸,則直接返回成功。即該mapping匹配該請(qǐng)求疗垛。
在@RequestMapping相同的情況下症汹,即無@ApiVersion注解的Mapping在任何情況下都會(huì)匹配。
場(chǎng)景:用戶未上送版本號(hào)時(shí)贷腕,執(zhí)行無自定義注解的方法背镇。
方案:getMatchingCondition
中,若用戶未上送版本號(hào)返回null泽裳。這樣只有無@ApiVersion的方法才會(huì)經(jīng)過篩選瞒斩。
2.3 多個(gè)mapping時(shí)自定義條件比較
對(duì)于自定義映射規(guī)則來說,我們的篩選方法可能會(huì)得到多個(gè)Mapping對(duì)象涮总。
例如用戶請(qǐng)求頭攜帶的參數(shù)是版本3
胸囱,會(huì)有兩個(gè)Mapping經(jīng)過篩選。
- 無@ApiVersion注解方法瀑梗。
- 存在@APiVersion(3)的注解的方法烹笔。
//一個(gè)mapping的自定義條件不存在,那么另一個(gè)mapping優(yōu)先級(jí)高
//源碼:org.springframework.web.servlet.mvc.condition.RequestConditionHolder#compareTo
@Override
public int compareTo(RequestConditionHolder other, HttpServletRequest request) {
if (this.condition == null && other.condition == null) {
return 0;
}
else if (this.condition == null) {
return 1;
}
else if (other.condition == null) {
return -1;
}
else {
assertEqualConditionTypes(this.condition, other.condition);
//若兩個(gè)Mapping的自定義條件均存在抛丽,執(zhí)行自定義的比較方法谤职。
return this.condition.compareTo(other.condition, request);
}
}
那么不存在@ApiVersion注解的Controller方法,在@RequestMapping配置相同的情況下亿鲜,優(yōu)先級(jí)最低允蜈。
總結(jié):若是使用自定義映射規(guī)則,SpringMVC的處理規(guī)則是:
@RequestMapping
屬性與請(qǐng)求匹配蒿柳,但不存在自定義條件,那么也會(huì)和請(qǐng)求匹配垒探;@RequestMapping
屬性優(yōu)先級(jí)相同的請(qǐng)求下仔引,若RequestMappingInfo的自定義條件為null,則優(yōu)先級(jí)最低儿倒。