191222-SpringBoot 系列教程 web 篇之自定義請求匹配條件 RequestCondition
在 spring mvc 中,咱們知道用戶發起的請求能夠經過 url 匹配到咱們經過@RequestMapping
定義的服務端點上;不知道有幾個問題你們是否有過思考java
一個項目中,可否存在徹底相同的 url?git
有了解 http 協議的同窗可能很快就能給出答案,固然能夠,url 相同,請求方法不一樣便可;那麼可否出現 url 相同且請求方法 l 也相同的呢?github
本文將介紹一下如何使用RequestCondition
結合RequestMappingHandlerMapping
,來實現 url 匹配規則的擴展,從而支持上面提出的 caseweb
<!-- more -->spring
本文介紹的內容和實際 case 將基於spring-boot-2.2.1.RELEASE
版本,若是在測試時,發現某些地方無法兼容時,請肯定一下版本websocket
首先咱們須要搭建一個 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>
在 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 匹配條件 |
單純的看說明,可能不太好理解它的使用方式,接下來咱們經過一個實際的 case,來演示使用姿式
咱們有個服務同時針對 app/wap/pc 三個平臺,咱們但願能夠指定某些接口只爲特定的平臺提供服務
首先咱們定義經過請求頭中的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; } }
接下來進入實測環節,定義幾個接口,分別指定不一樣的平臺
@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 和請求方法相同的,在啓動的時候會直接拋出異常哦
盡信書則不如,以上內容,純屬一家之言,因我的能力有限,不免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激
下面一灰灰的我的博客,記錄全部學習和工做中的博文,歡迎你們前去逛逛