在 spring mvc 中,我们知道用户发起的请求可以通过 url 匹配到我们通过@RequestMapping
定义的服务端点上;不知道有几个问题大家是否有过思考
一个项目中,能否存在完全相同的 url?
有了解 http 协议的同学可能很快就能给出答案,当然可以,url 相同,请求方法不同即可;那么能否出现 url 相同且请求方法 l 也相同的呢?
本文将介绍一下如何使用RequestCondition
结合RequestMappingHandlerMapping
,来实现 url 匹配规则的扩展,从而支持上面提出的 case
本文介绍的内容和实际 case 将基于spring-boot-2.2.1.RELEASE
版本,如果在测试时,发现某些地方没法兼容时,请确定一下版本
首先我们需要搭建一个 web 工程,以方便后续的 servelt 注册的实例演示,可以通过 spring boot 官网创建工程,也可以建立一个 maven 工程,在 pom.xml 中如下配置
org.springframework.boot
spring-boot-starter-parent
2.2.1.RELEASE
UTF-8
UTF-8
1.8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-maven-plugin
spring-snapshots
Spring Snapshots
https://repo.spring.io/libs-snapshot-local
true
spring-milestones
Spring Milestones
https://repo.spring.io/libs-milestone-local
false
spring-releases
Spring Releases
https://repo.spring.io/libs-release-local
false
在 spring mvc 中,通过DispatchServlet
接收客户端发起的一个请求之后,会通过 HanderMapping 来获取对应的请求处理器;而 HanderMapping 如何找到可以处理这个请求的处理器呢,这就需要 RequestCondition 来决定了
接口定义如下,主要有三个方法,
public interface RequestCondition {
// 一个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
来区分平台;即用户发起的请求中,需要携带这个请求头
定义平台枚举类
publicenum PlatformEnum {
PC("pc", 1), APP("app", 1), WAP("wap", 1), ALL("all", 0);
@Getter
private String name;
@Getter
privateint 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
,实现三个接口,从请求头中获取平台,根据平台是否相同过来判定是否可以支持请求
publicclass PlatformRequestCondition implements RequestCondition {
@Getter
@Setter
private PlatformEnum platform;
public PlatformRequestCondition(PlatformEnum platform) {
this.platform = platform;
}
@Override
public PlatformRequestCondition combine(PlatformRequestCondition other) {
returnnew PlatformRequestCondition(other.platform);
}
@Override
public PlatformRequestCondition getMatchingCondition(HttpServletRequest request) {
PlatformEnum platform = this.getPlatform(request);
if (this.platform.equals(platform)) {
returnthis;
}
returnnull;
}
/**
* 优先级
*
* @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
publicclass 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
publicclass 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")
publicclass 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 和请求方法相同的,在启动的时候会直接抛出异常哦