SpringBoot2.x—定制HandlerMapping映射規(guī)則

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所示:


圖1-設(shè)置請(qǐng)求頭.png

可以精確的定位到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

原理:RequestMappingHandlerMappingHandlerMapping實(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)求的映射。

  1. 在含有@RequestMapping的方法/類上加上自定義注解效拭,以便自定義RequestCondition得到參數(shù)暂吉。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
    //該方法適配的版本號(hào)
    int[] value();
}
  1. 當(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;
    }
}
  1. 該類繼承了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;
    }
}
  1. 將自定義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)過篩選。

  1. 無@ApiVersion注解方法瀑梗。
  2. 存在@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ī)則是:

  1. @RequestMapping屬性與請(qǐng)求匹配蒿柳,但不存在自定義條件,那么也會(huì)和請(qǐng)求匹配垒探;

  2. @RequestMapping屬性優(yōu)先級(jí)相同的請(qǐng)求下仔引,若RequestMappingInfo的自定義條件為null,則優(yōu)先級(jí)最低儿倒。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市汞幢,隨后出現(xiàn)的幾起案子森篷,更是在濱河造成了極大的恐慌,老刑警劉巖钓辆,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件娶眷,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡席揽,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門他爸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來系谐,“玉大人纪他,你說我怎么就攤上這事梯刚⊥鲎剩” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長演顾,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上佑钾,老公的妹妹穿的比我還像新娘。我一直安慰自己邮偎,他們只是感情好禾进,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般快集。 火紅的嫁衣襯著肌膚如雪猴蹂。 梳的紋絲不亂的頭發(fā)上珍逸,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天勤婚,我揣著相機(jī)與錄音凝果,去河邊找鬼型雳。 笑死沿量,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的乌妒。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼匹中,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼屎篱!你這毒婦竟也來了重虑?” 一聲冷哼從身側(cè)響起提针,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤嗜价,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后错妖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體痴施,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡厘惦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了让歼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡挪钓,死狀恐怖是越,靈堂內(nèi)的尸體忽然破棺而出耳舅,到底是詐尸還是另有隱情碌上,我是刑警寧澤倚评,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站馏予,受9級(jí)特大地震影響天梧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜霞丧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一呢岗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蛹尝,春花似錦后豫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至愕难,卻和暖如春早龟,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背猫缭。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來泰國打工葱弟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人猜丹。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓芝加,卻偏偏與公主長得像,于是被迫代替她去往敵國和親射窒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子妖混,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345