SpringBoot 系列教程 web 篇之自定義請(qǐng)求匹配條件 RequestCondition

image

191222-SpringBoot 系列教程 web 篇之自定義請(qǐng)求匹配條件 RequestCondition

在 spring mvc 中,我們知道用戶發(fā)起的請(qǐng)求可以通過(guò) url 匹配到我們通過(guò)@RequestMapping定義的服務(wù)端點(diǎn)上;不知道有幾個(gè)問(wèn)題大家是否有過(guò)思考

一個(gè)項(xiàng)目中,能否存在完全相同的 url?

有了解 http 協(xié)議的同學(xué)可能很快就能給出答案,當(dāng)然可以,url 相同,請(qǐng)求方法不同即可;那么能否出現(xiàn) url 相同且請(qǐng)求方法 l 也相同的呢?

本文將介紹一下如何使用RequestCondition結(jié)合RequestMappingHandlerMapping,來(lái)實(shí)現(xiàn) url 匹配規(guī)則的擴(kuò)展,從而支持上面提出的 case

I. 環(huán)境相關(guān)

本文介紹的內(nèi)容和實(shí)際 case 將基于spring-boot-2.2.1.RELEASE版本,如果在測(cè)試時(shí),發(fā)現(xiàn)某些地方?jīng)]法兼容時(shí),請(qǐng)確定一下版本

1. 項(xiàng)目搭建

首先我們需要搭建一個(gè) web 工程,以方便后續(xù)的 servelt 注冊(cè)的實(shí)例演示,可以通過(guò) spring boot 官網(wǎng)創(chuàng)建工程,也可以建立一個(gè) maven 工程,在 pom.xml 中如下配置

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.1.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </pluginManagement>
</build>
<repositories>
    <repository>
        <id>spring-snapshots</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/libs-snapshot-local</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/libs-milestone-local</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>spring-releases</id>
        <name>Spring Releases</name>
        <url>https://repo.spring.io/libs-release-local</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

2. RequestCondition 介紹

在 spring mvc 中,通過(guò)DispatchServlet接收客戶端發(fā)起的一個(gè)請(qǐng)求之后,會(huì)通過(guò) HanderMapping 來(lái)獲取對(duì)應(yīng)的請(qǐng)求處理器;而 HanderMapping 如何找到可以處理這個(gè)請(qǐng)求的處理器呢,這就需要 RequestCondition 來(lái)決定了

接口定義如下,主要有三個(gè)方法,

public interface RequestCondition<T> {

    // 一個(gè)http接口上有多個(gè)條件規(guī)則時(shí),用于合并
    T combine(T other);

    // 這個(gè)是重點(diǎn),用于判斷當(dāng)前匹配條件和請(qǐng)求是否匹配;如果不匹配返回null
    // 如果匹配,生成一個(gè)新的請(qǐng)求匹配條件,該新的請(qǐng)求匹配條件是當(dāng)前請(qǐng)求匹配條件針對(duì)指定請(qǐng)求request的剪裁
    // 舉個(gè)例子來(lái)講,如果當(dāng)前請(qǐng)求匹配條件是一個(gè)路徑匹配條件,包含多個(gè)路徑匹配模板,
    // 并且其中有些模板和指定請(qǐng)求request匹配,那么返回的新建的請(qǐng)求匹配條件將僅僅
    // 包含和指定請(qǐng)求request匹配的那些路徑模板。
    @Nullable
    T getMatchingCondition(HttpServletRequest request);

    // 針對(duì)指定的請(qǐng)求對(duì)象request發(fā)現(xiàn)有多個(gè)滿足條件的,用來(lái)排序指定優(yōu)先級(jí),使用最優(yōu)的進(jìn)行響應(yīng)
    int compareTo(T other, HttpServletRequest request);

}

簡(jiǎn)單說(shuō)下三個(gè)接口的作用

  • combine: 某個(gè)接口有多個(gè)規(guī)則時(shí),進(jìn)行合并 - 比如類(lèi)上指定了@RequestMapping的 url 為 root - 而方法上指定的@RequestMapping的 url 為 method - 那么在獲取這個(gè)接口的 url 匹配規(guī)則時(shí),類(lèi)上掃描一次,方法上掃描一次,這個(gè)時(shí)候就需要把這兩個(gè)合并成一個(gè),表示這個(gè)接口匹配root/method

  • getMatchingCondition: - 判斷是否成功,失敗返回 null;否則,則返回匹配成功的條件

  • compareTo: - 多個(gè)都滿足條件時(shí),用來(lái)指定具體選擇哪一個(gè)

在 Spring MVC 中,默認(rèn)提供了下面幾種

類(lèi) 說(shuō)明
PatternsRequestCondition 路徑匹配,即 url
RequestMethodsRequestCondition 請(qǐng)求方法,注意是指 http 請(qǐng)求方法
ParamsRequestCondition 請(qǐng)求參數(shù)條件匹配
HeadersRequestCondition 請(qǐng)求頭匹配
ConsumesRequestCondition 可消費(fèi) MIME 匹配條件
ProducesRequestCondition 可生成 MIME 匹配條件

II. 實(shí)例說(shuō)明

單純的看說(shuō)明,可能不太好理解它的使用方式,接下來(lái)我們通過(guò)一個(gè)實(shí)際的 case,來(lái)演示使用姿勢(shì)

1. 場(chǎng)景說(shuō)明

我們有個(gè)服務(wù)同時(shí)針對(duì) app/wap/pc 三個(gè)平臺(tái),我們希望可以指定某些接口只為特定的平臺(tái)提供服務(wù)

2. 實(shí)現(xiàn)

首先我們定義通過(guò)請(qǐng)求頭中的x-platform來(lái)區(qū)分平臺(tái);即用戶發(fā)起的請(qǐng)求中,需要攜帶這個(gè)請(qǐng)求頭

定義平臺(tái)枚舉類(lèi)

public enum PlatformEnum {
    PC("pc", 1), APP("app", 1), WAP("wap", 1), ALL("all", 0);

    @Getter
    private String name;

    @Getter
    private int order;

    PlatformEnum(String name, int order) {
        this.name = name;
        this.order = order;
    }

    public static PlatformEnum nameOf(String name) {
        if (name == null) {
            return ALL;
        }

        name = name.toLowerCase().trim();
        for (PlatformEnum sub : values()) {
            if (sub.name.equals(name)) {
                return sub;
            }
        }
        return ALL;
    }
}

然后定義一個(gè)注解@Platform,如果某個(gè)接口需要指定平臺(tái),則加上這個(gè)注解即可

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Platform {
    PlatformEnum value() default PlatformEnum.ALL;
}

定義匹配規(guī)則PlatformRequestCondition繼承自RequestCondition,實(shí)現(xiàn)三個(gè)接口,從請(qǐng)求頭中獲取平臺(tái),根據(jù)平臺(tái)是否相同過(guò)來(lái)判定是否可以支持請(qǐng)求

public class PlatformRequestCondition implements RequestCondition<PlatformRequestCondition> {
    @Getter
    @Setter
    private PlatformEnum platform;

    public PlatformRequestCondition(PlatformEnum platform) {
        this.platform = platform;
    }

    @Override
    public PlatformRequestCondition combine(PlatformRequestCondition other) {
        return new PlatformRequestCondition(other.platform);
    }

    @Override
    public PlatformRequestCondition getMatchingCondition(HttpServletRequest request) {
        PlatformEnum platform = this.getPlatform(request);
        if (this.platform.equals(platform)) {
            return this;
        }

        return null;
    }

    /**
     * 優(yōu)先級(jí)
     *
     * @param other
     * @param request
     * @return
     */
    @Override
    public int compareTo(PlatformRequestCondition other, HttpServletRequest request) {
        int thisOrder = this.platform.getOrder();
        int otherOrder = other.platform.getOrder();
        return otherOrder - thisOrder;
    }

    private PlatformEnum getPlatform(HttpServletRequest request) {
        String platform = request.getHeader("x-platform");
        return PlatformEnum.nameOf(platform);
    }
}

匹配規(guī)則指定完畢之后,需要注冊(cè)到 HandlerMapping 上才能生效,這里我們自定義一個(gè)PlatformHandlerMapping

public class PlatformHandlerMapping extends RequestMappingHandlerMapping {
    @Override
    protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
        return buildFrom(AnnotationUtils.findAnnotation(handlerType, Platform.class));
    }

    @Override
    protected RequestCondition<?> getCustomMethodCondition(Method method) {
        return buildFrom(AnnotationUtils.findAnnotation(method, Platform.class));
    }

    private PlatformRequestCondition buildFrom(Platform platform) {
        return platform == null ? null : new PlatformRequestCondition(platform.value());
    }
}

最后則是需要將我們的 HandlerMapping 注冊(cè)到 Spring MVC 容器,在這里我們借助WebMvcConfigurationSupport來(lái)手動(dòng)注冊(cè)(注意一下,不同的版本,下面的方法可能會(huì)不太一樣哦)

@Configuration
public class Config extends WebMvcConfigurationSupport {
    @Override
    public RequestMappingHandlerMapping requestMappingHandlerMapping(
            @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
            @Qualifier("mvcConversionService") FormattingConversionService conversionService,
            @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {
        PlatformHandlerMapping handlerMapping = new PlatformHandlerMapping();
        handlerMapping.setOrder(0);
        handlerMapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
        return handlerMapping;
    }
}

3. 測(cè)試

接下來(lái)進(jìn)入實(shí)測(cè)環(huán)節(jié),定義幾個(gè)接口,分別指定不同的平臺(tái)

@RestController
@RequestMapping(path = "method")
public class DemoMethodRest {
    @Platform
    @GetMapping(path = "index")
    public String allIndex() {
        return "default index";
    }

    @Platform(PlatformEnum.PC)
    @GetMapping(path = "index")
    public String pcIndex() {
        return "pc index";
    }


    @Platform(PlatformEnum.APP)
    @GetMapping(path = "index")
    public String appIndex() {
        return "app index";
    }

    @Platform(PlatformEnum.WAP)
    @GetMapping(path = "index")
    public String wapIndex() {
        return "wap index";
    }
}

如果我們的規(guī)則可以正常生效,那么在請(qǐng)求頭中設(shè)置不同的x-platform,返回的結(jié)果應(yīng)該會(huì)不一樣,實(shí)測(cè)結(jié)果如下

image

注意最后兩個(gè),一個(gè)是指定了一個(gè)不匹配我們的平臺(tái)的請(qǐng)求頭,一個(gè)是沒(méi)有對(duì)應(yīng)的請(qǐng)求頭,都是走了默認(rèn)的匹配規(guī)則;這是因?yàn)槲覀冊(cè)?code>PlatformRequestCondition中做了兼容,無(wú)法匹配平臺(tái)時(shí),分配到默認(rèn)的Platform.ALL

然后還有一個(gè)小疑問(wèn),如果有一個(gè)服務(wù)不區(qū)分平臺(tái),那么不加上@Platform注解是否可以呢?

@GetMapping(path = "hello")
public String hello() {
    return "hello";
}

當(dāng)然是可以的實(shí)測(cè)結(jié)果如下:

image

在不加上@Platform注解時(shí),有一點(diǎn)需要注意,這個(gè)時(shí)候就不能出現(xiàn)多個(gè) url 和請(qǐng)求方法相同的,在啟動(dòng)的時(shí)候會(huì)直接拋出異常哦

image

III. 其他

web 系列博文

項(xiàng)目源碼

1. 一灰灰 Blog

盡信書(shū)則不如,以上內(nèi)容,純屬一家之言,因個(gè)人能力有限,難免有疏漏和錯(cuò)誤之處,如發(fā)現(xiàn) bug 或者有更好的建議,歡迎批評(píng)指正,不吝感激

下面一灰灰的個(gè)人博客,記錄所有學(xué)習(xí)和工作中的博文,歡迎大家前去逛逛

一灰灰blog
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容