在 spring mvc 中,咱们知道用户发起的请求能够经过 url 匹配到咱们经过@RequestMapping
定义的服务端点上;不知道有几个问题你们是否有过思考git
一个项目中,可否存在彻底相同的 url?github
有了解 http 协议的同窗可能很快就能给出答案,固然能够,url 相同,请求方法不一样便可;那么可否出现 url 相同且请求方法 l 也相同的呢?web
本文将介绍一下如何使用RequestCondition
结合RequestMappingHandlerMapping
,来实现 url 匹配规则的扩展,从而支持上面提出的 casespring
<!-- more -->websocket
本文介绍的内容和实际 case 将基于spring-boot-2.2.1.RELEASE
版本,若是在测试时,发现某些地方无法兼容时,请肯定一下版本mvc
首先咱们须要搭建一个 web 工程,以方便后续的 servelt 注册的实例演示,能够经过 spring boot 官网建立工程,也能够创建一个 maven 工程,在 pom.xml 中以下配置app
<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 来决定了socket
接口定义以下,主要有三个方法,maven
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 匹配条件 |
单纯的看说明,可能不太好理解它的使用方式,接下来咱们经过一个实际的 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 或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的我的博客,记录全部学习和工做中的博文,欢迎你们前去逛逛