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


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

在 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 路徑匹配,即 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM