SpringBoot 系列教程 web 篇之自定义请求匹配条件 RequestCondition

image

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,返回的结果应该会不一样,实测结果如下

image

注意最后两个,一个是指定了一个不匹配我们的平台的请求头,一个是没有对应的请求头,都是走了默认的匹配规则;这是因为我们在PlatformRequestCondition中做了兼容,无法匹配平台时,分配到默认的Platform.ALL

然后还有一个小疑问,如果有一个服务不区分平台,那么不加上@Platform注解是否可以呢?

@GetMapping(path = "hello")
public String hello() {
    return "hello";
}

当然是可以的实测结果如下:

image

在不加上@Platform注解时,有一点需要注意,这个时候就不能出现多个 url 和请求方法相同的,在启动的时候会直接抛出异常哦

image

III. 其他

web 系列博文

项目源码

1. 一灰灰 Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容