SpringBoot 系列教程 web 篇之自定義請求匹配條件 RequestCondition

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

在 spring mvc 中,咱們知道用戶發起的請求能夠經過 url 匹配到咱們經過@RequestMapping定義的服務端點上;不知道有幾個問題你們是否有過思考java

一個項目中,可否存在徹底相同的 url?git

有了解 http 協議的同窗可能很快就能給出答案,固然能夠,url 相同,請求方法不一樣便可;那麼可否出現 url 相同且請求方法 l 也相同的呢?github

本文將介紹一下如何使用RequestCondition結合RequestMappingHandlerMapping,來實現 url 匹配規則的擴展,從而支持上面提出的 caseweb

<!-- more -->spring

I. 環境相關

本文介紹的內容和實際 case 將基於spring-boot-2.2.1.RELEASE版本,若是在測試時,發現某些地方無法兼容時,請肯定一下版本websocket

1. 項目搭建

首先咱們須要搭建一個 web 工程,以方便後續的 servelt 註冊的實例演示,能夠經過 spring boot 官網建立工程,也能夠創建一個 maven 工程,在 pom.xml 中以下配置mvc

<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 中,經過DispatchServlet接收客戶端發起的一個請求以後,會經過 HanderMapping 來獲取對應的請求處理器;而 HanderMapping 如何找到能夠處理這個請求的處理器呢,這就須要 RequestCondition 來決定了app

接口定義以下,主要有三個方法,socket

public interface RequestCondition<T> {

    // 一個http接口上有多個條件規則時,用於合併
    T combine(T other);

    // 這個是重點,用於判斷當前匹配條件和請求是否匹配;若是不匹配返回null
    // 若是匹配,生成一個新的請求匹配條件,該新的請求匹配條件是當前請求匹配條件針對指定請求request的剪裁
    // 舉個例子來說,若是當前請求匹配條件是一個路徑匹配條件,包含多個路徑匹配模板,
    // 而且其中有些模板和指定請求request匹配,那麼返回的新建的請求匹配條件將僅僅
    // 包含和指定請求request匹配的那些路徑模板。
    @Nullable
    T getMatchingCondition(HttpServletRequest request);

    // 針對指定的請求對象request發現有多個知足條件的,用來排序指定優先級,使用最優的進行響應
    int compareTo(T other, HttpServletRequest request);

}

簡單說下三個接口的做用maven

  • combine: 某個接口有多個規則時,進行合併 - 好比類上指定了@RequestMapping的 url 爲 root - 而方法上指定的@RequestMapping的 url 爲 method - 那麼在獲取這個接口的 url 匹配規則時,類上掃描一次,方法上掃描一次,這個時候就須要把這兩個合併成一個,表示這個接口匹配root/method
  • getMatchingCondition: - 判斷是否成功,失敗返回 null;不然,則返回匹配成功的條件
  • compareTo: - 多個都知足條件時,用來指定具體選擇哪個

在 Spring MVC 中,默認提供了下面幾種

說明
PatternsRequestCondition 路徑匹配,即 url
RequestMethodsRequestCondition 請求方法,注意是指 http 請求方法
ParamsRequestCondition 請求參數條件匹配
HeadersRequestCondition 請求頭匹配
ConsumesRequestCondition 可消費 MIME 匹配條件
ProducesRequestCondition 可生成 MIME 匹配條件

II. 實例說明

單純的看說明,可能不太好理解它的使用方式,接下來咱們經過一個實際的 case,來演示使用姿式

1. 場景說明

咱們有個服務同時針對 app/wap/pc 三個平臺,咱們但願能夠指定某些接口只爲特定的平臺提供服務

2. 實現

首先咱們定義經過請求頭中的x-platform來區分平臺;即用戶發起的請求中,須要攜帶這個請求頭

定義平臺枚舉類

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;
    }
}

而後定義一個註解@Platform,若是某個接口須要指定平臺,則加上這個註解便可

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

定義匹配規則PlatformRequestCondition繼承自RequestCondition,實現三個接口,從請求頭中獲取平臺,根據平臺是否相同過來斷定是否能夠支持請求

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;
    }

    /**
     * 優先級
     *
     * @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);
    }
}

匹配規則指定完畢以後,須要註冊到 HandlerMapping 上才能生效,這裏咱們自定義一個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 註冊到 Spring MVC 容器,在這裏咱們藉助WebMvcConfigurationSupport來手動註冊(注意一下,不一樣的版本,下面的方法可能會不太同樣哦)

@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. 測試

接下來進入實測環節,定義幾個接口,分別指定不一樣的平臺

@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";
    }
}

若是咱們的規則能夠正常生效,那麼在請求頭中設置不一樣的x-platform,返回的結果應該會不同,實測結果以下

注意最後兩個,一個是指定了一個不匹配咱們的平臺的請求頭,一個是沒有對應的請求頭,都是走了默認的匹配規則;這是由於咱們在PlatformRequestCondition中作了兼容,沒法匹配平臺時,分配到默認的Platform.ALL

而後還有一個小疑問,若是有一個服務不區分平臺,那麼不加上@Platform註解是否能夠呢?

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

固然是能夠的實測結果以下:

在不加上@Platform註解時,有一點須要注意,這個時候就不能出現多個 url 和請求方法相同的,在啓動的時候會直接拋出異常哦

III. 其餘

web 系列博文

項目源碼

1. 一灰灰 Blog

盡信書則不如,以上內容,純屬一家之言,因我的能力有限,不免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激

下面一灰灰的我的博客,記錄全部學習和工做中的博文,歡迎你們前去逛逛

一灰灰blog

相關文章
相關標籤/搜索