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

简介: 在 spring mvc 中,我们知道用户发起的请求可以通过 url 匹配到我们通过@RequestMapping定义的服务端点上;不知道有几个问题大家是否有过思考一个项目中,能否存在完全相同的 url?有了解 http 协议的同学可能很快就能给出答案,当然可以,url 相同,请求方法不同即可;那么能否出现 url 相同且请求方法 l 也相同的呢?本文将介绍一下如何使用RequestCondition结合RequestMappingHandlerMapping,来实现 url 匹配规则的扩展,从而支持上面提出的 case

在 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 和请求方法相同的,在启动的时候会直接抛出异常哦




相关文章
|
2月前
|
存储 开发框架 JSON
在 Python 中,如何处理 Web 请求和响应?
【2月更文挑战第26天】【2月更文挑战第90篇】在 Python 中,如何处理 Web 请求和响应?
|
16天前
|
JSON Java fastjson
Spring Boot 底层级探索系列 04 - Web 开发(2)
Spring Boot 底层级探索系列 04 - Web 开发(2)
24 0
|
2月前
|
API
2024常用Web支付开发讲解教程
本教程为web支付开发,讲解了最常用的两钟支付:支付宝支付和微信支付,服务器配置和API对接,学完本课程可以学会微信支付、和支付宝支付开发。
19 2
2024常用Web支付开发讲解教程
|
2月前
|
架构师 前端开发
web全栈架构师第16期教程
互联网时代已进入后半场,行业环境发生了显著变化。互联网人,尤其是技术人员,如何在加速更迭的技术浪潮中持续充电,提升自身价值,是当下必须面对的挑战。课程涉及了现下前端实际开发时所需要的各块内容,并深度对标 阿里 P6+级别所具备的知识储备及开发技能,奠定源码阅读基础和全栈开发能力。
19 3
web全栈架构师第16期教程
|
2月前
|
XML JavaScript 前端开发
Web 扫描神器:WhatWeb 保姆级教程(附链接)
Web 扫描神器:WhatWeb 保姆级教程(附链接)
|
2月前
|
存储 网络协议 安全
Web 扫描神器:Gobuster 保姆级教程(附链接)
Web 扫描神器:Gobuster 保姆级教程(附链接)
|
2月前
|
存储 搜索推荐 Java
springboot280基于WEB的旅游推荐系统设计与实现
springboot280基于WEB的旅游推荐系统设计与实现
|
13天前
|
开发框架 前端开发 .NET
C#编程与Web开发
【4月更文挑战第21天】本文探讨了C#在Web开发中的应用,包括使用ASP.NET框架、MVC模式、Web API和Entity Framework。C#作为.NET框架的主要语言,结合这些工具,能创建动态、高效的Web应用。实际案例涉及企业级应用、电子商务和社交媒体平台。尽管面临竞争和挑战,但C#在Web开发领域的前景将持续拓展。
|
2天前
|
关系型数据库 MySQL
web简易开发(二){html5+php实现文件上传及通过关键字搜索已上传图片)}
web简易开发(二){html5+php实现文件上传及通过关键字搜索已上传图片)}
|
4天前
|
开发框架 JavaScript 前端开发
【JavaScript 与 TypeScript 技术专栏】TypeScript 在 Web 开发中的前沿应用
【4月更文挑战第30天】TypeScript在Web开发中日益重要,以其强大的类型系统提升代码质量,支持组件化开发,与React、Vue、Angular等框架良好集成。在大型项目管理中,TypeScript助于代码组织和优化,提高团队协作效率。此外,它提升开发体验,提供智能提示和错误检测。众多成功案例证明其前沿应用,未来将在Web开发领域持续发挥关键作用。