SpringBoot web 篇之自定義請求匹配條件 RequestCondition

小灰灰blog 發佈 2019-12-23T04:47:13+00:00

在 spring mvc 中,我們知道用戶發起的請求可以通過 url 匹配到我們通過@RequestMapping定義的服務端點上;不知道有幾個問題大家是否有過思考一個項目中,能否存在完全相同的 url?

在 spring mvc 中,我們知道用戶發起的請求可以通過 url 匹配到我們通過@RequestMapping定義的服務端點上;不知道有幾個問題大家是否有過思考

一個項目中,能否存在完全相同的 url?

有了解 http 協議的同學可能很快就能給出答案,當然可以,url 相同,請求方法不同即可;那麼能否出現 url 相同且請求方法 l 也相同的呢?

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

I. 環境相關

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

1. 項目搭建

首先我們需要搭建一個 web 工程,以方便後續的 servelt 註冊的實例演示,可以通過 spring boot 官網創建工程,也可以建立一個 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 中,通過DispatchServlet接收客戶端發起的一個請求之後,會通過 HanderMapping 來獲取對應的請求處理器;而 HanderMapping 如何找到可以處理這個請求的處理器呢,這就需要 RequestCondition 來決定了

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

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

}

簡單說下三個接口的作用

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

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

類說明PatternsRequestCondition路徑匹配,即 urlRequestMethodsRequestCondition請求方法,注意是指 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 系列博文

  • 191206-SpringBoot 系列教程 web 篇 Listener 四種註冊姿勢
  • 191122-SpringBoot 系列教程 web 篇 Servlet 註冊的四種姿勢
  • 191120-SpringBoot 系列教程 Web 篇之開啟 GZIP 數據壓縮
  • 191018-SpringBoot 系列教程 web 篇之過濾器 Filter 使用指南擴展篇
  • 191016-SpringBoot 系列教程 web 篇之過濾器 Filter 使用指南
  • 191012-SpringBoot 系列教程 web 篇之自定義異常處理 HandlerExceptionResolver
  • 191010-SpringBoot 系列教程 web 篇之全局異常處理
  • 190930-SpringBoot 系列教程 web 篇之 404、500 異常頁面配置
  • 190929-SpringBoot 系列教程 web 篇之重定向
  • 190913-SpringBoot 系列教程 web 篇之返回文本、網頁、圖片的操作姿勢
  • 190905-SpringBoot 系列教程 web 篇之中文亂碼問題解決
  • 190831-SpringBoot 系列教程 web 篇之如何自定義參數解析器
  • 190828-SpringBoot 系列教程 web 篇之 Post 請求參數解析姿勢匯總
  • 190824-SpringBoot 系列教程 web 篇之 Get 請求參數解析姿勢匯總
  • 190822-SpringBoot 系列教程 web 篇之 Beetl 環境搭建
  • 190820-SpringBoot 系列教程 web 篇之 Thymeleaf 環境搭建
  • 190816-SpringBoot 系列教程 web 篇之 Freemaker 環境搭建
  • 190421-SpringBoot 高級篇 WEB 之 websocket 的使用說明
  • 190327-Spring-RestTemplate 之 urlencode 參數解析異常全程分析
  • 190317-Spring MVC 之基於 java config 無 xml 配置的 web 應用構建
  • 190316-Spring MVC 之基於 xml 配置的 web 應用構建
  • 190213-SpringBoot 文件上傳異常之提示 The temporary upload location xxx is not valid

項目源碼

  • 工程:https://github.com/liuyueyi/spring-boot-demo
  • 項目: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/208-web-mapping

1. 一灰灰 Blog

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

下面一灰灰的個人博客,記錄所有學習和工作中的博文,歡迎大家前去逛逛

  • 一灰灰 Blog 個人博客 https://blog.hhui.top
  • 一灰灰 Blog-Spring 專題博客 http://spring.hhui.top
關鍵字: