文章
问答
冒泡
spring基于RequestMappingHandlerMapping的Api版本控制

背景
在业务开发中,尤其是app项目,由于客户端的与服务端很难保持一致,经常会遇到版本控制的需求,即根据请求上的版本号,调用服务端的不同实现。

spring接口服务的实现方案
spring的路由是基于RequestMappingHandlerMapping实现路由映射的。那么我们对应的解决方案也应该从RequestMappingHandlerMapping入手。

而RequestMappingInfo中判断是否匹配,是基于多个RequestCondition的组合,只有都满足的时候,才能匹配到对于的实现。

RequestMappingInfoHandlerMapping#getMatchingMapping

public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
    RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
    if (methods == null) {
       return null;
    }
    ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
    if (params == null) {
       return null;
    }
    HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
    if (headers == null) {
       return null;
    }
    ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
    if (consumes == null) {
       return null;
    }
    ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
    if (produces == null) {
       return null;
    }
    PathPatternsRequestCondition pathPatterns = null;
    if (this.pathPatternsCondition != null) {
       pathPatterns = this.pathPatternsCondition.getMatchingCondition(request);
       if (pathPatterns == null) {
          return null;
       }
    }
    PatternsRequestCondition patterns = null;
    if (this.patternsCondition != null) {
       patterns = this.patternsCondition.getMatchingCondition(request);
       if (patterns == null) {
          return null;
       }
    }
    RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
    if (custom == null) {
       return null;
    }
    return new RequestMappingInfo(this.name, pathPatterns, patterns,
          methods, params, headers, consumes, produces, custom, this.options);
}

由代码可见,RequestMappingInfo中的每个Condition都会判断是否匹配,而里面其他的Condition都是系统默认的,这里我们能操作的只有CustomCondition,所以,这里我们的重点就在于,怎样定义一个自己的RequestCondition,来进行版本的匹配。
RequestCondition 中主要有3个方法:
- combine:某个接口有多个规则时,进行合并-比如类上指定了 @RequestMapping 的ur 为root-而方法上指定的 @Requestmapping 的url 为 method -那么在获取这个接口的 ul 匹配规则时,类上扫描一次,方法上扫描一次、这个时候就需要把这两个合并成一个,表示这个接口匹配 root/method
- getMatchingCondition:判断是否成功,失败返回 null;否则,则返回匹配成功的条件 
- compareTo:多个都满足条件时,用来指定具体选择哪一个
根据业务需求实现一个自己的RequestCondition,并加入到 RequestConditionHolder中,在路由和方法映射的时候,就会去进行判断。

 

apiversion的实现方案

基于路径的实现
根据路径的版本控制,将版本标识写在路径上,类似 api/v2/test 。
PathApiVersion

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface PathApiVersion {
    String value() ;
}

由于需要解析出路径的上的变量,需要获得当前路径匹配路径表达式,而这个表达式存在于 PathPatternsRequestCondition 中,所以,我们需要获得RequestMappingInfo,根据函数的调用顺序,我们需要在 RequestCondition 的 getMatchingCondition 环节  获得 RequestMappingInfo,于是,构造一个抽象的 RequestMappingHandlerMapping 在getMatchingMapping 的时候,设置 RequestMappingInfo 变量。
ApiVersionRequestMappingHandlerMapping 

@Getter
public abstract class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
    private RequestMappingInfo info;

    @Override
    protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
        this.info = info;
        return info.getMatchingCondition(request);
    }
}

PathApiVersionCondition

@Getter
public class PathApiVersionCondition implements RequestCondition<PathApiVersionCondition> {
    private final ApiVersionRequestMappingHandlerMapping mapping;
    private final String pathVersion;

    public PathApiVersionCondition(ApiVersionRequestMappingHandlerMapping mapping, String pathVersion) {
        this.mapping = mapping;
        this.pathVersion = pathVersion;
    }


    @Override
    public PathApiVersionCondition combine(PathApiVersionCondition other) {
        return new PathApiVersionCondition(other.getMapping(), other.getPathVersion());
    }

    @Override
    public PathApiVersionCondition getMatchingCondition(HttpServletRequest request) {
        PathContainer path = ServletRequestPathUtils.getParsedRequestPath(request).pathWithinApplication();
        assert this.getMapping().getInfo().getPathPatternsCondition() != null;
        PathPattern bestPattern = this.getMapping().getInfo().getPathPatternsCondition().getFirstPattern();
        PathPattern.PathMatchInfo result = bestPattern.matchAndExtract(path);
        Assert.notNull(result, () ->
                "Expected bestPattern: " + bestPattern + " to match lookupPath " + path);
        Map<String, String> uriVariables = result.getUriVariables();
        if (StringUtils.hasText(pathVersion) && Objects.equals(uriVariables.get("version"), this.pathVersion)) {
            return this;
        }
        return null;
    }

    @Override
    public int compareTo(PathApiVersionCondition other, HttpServletRequest request) {
        return 0;
    }

}

PathApiVersionRequestMappingHandlerMapping

@Getter
public class PathApiVersionRequestMappingHandlerMapping extends ApiVersionRequestMappingHandlerMapping {

    @Override
    protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
        return createVersionCondition(handlerType.getAnnotation(PathApiVersion.class));
    }

    @Override
    protected RequestCondition<?> getCustomMethodCondition(Method method) {
        return createVersionCondition(method.getAnnotation(PathApiVersion.class));
    }

    private RequestCondition<PathApiVersionCondition> createVersionCondition(PathApiVersion pathVersion) {
        if (Objects.isNull(pathVersion) || !StringUtils.hasText(pathVersion.value())) {
            return null;
        }
        return new PathApiVersionCondition(this, Optional.ofNullable(pathVersion).map(PathApiVersion::value).orElse(null));
    }

}

ApiVersionConfiguration

@Configuration
@ConditionalOnWebApplication
public class ApiVersionConfiguration {


    @Bean
    public WebMvcRegistrations pathVersionWebMvcRegistrations()
    {
        return new WebMvcRegistrations() {
            @Override
            public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
                return new PathApiVersionRequestMappingHandlerMapping();
            }
        };
    }

}

PathTestController

@RestController
@RequestMapping("api/{version}/test")
public class PathTestController {
    
    @GetMapping
    public String test(){
        return "test";
    }

    @PathApiVersion("v1")
    @GetMapping
    public String testV1(){
        return "testV1";
    }

    @PathApiVersion("v2")
    @GetMapping
    public String testV2(){
        return "testV2";
    }

}

请求测试
v1版本的请求,进入v1的方法

v2版本的请求,进入v2的方法

v3版本请求,由于v3的版本不存在,则进入默认的方法

基于header的实现
header的版本控制,就是将版本参数放在请求的头文件中。在判断的时候,通过获取header中的值进行判断。
spring web 本身就有HeadersRequestCondition ,也可以直接使用,自己实现的话,可以更好的去控制。

HeaderApiVersion

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface HeaderApiVersion {
    String value() ;
}
HeadApiVersionCondition 通过获取header中的X-Version值,进行判断
@Getter
public class HeadApiVersionCondition implements RequestCondition<HeadApiVersionCondition> {
    private final String headerVersion;

    public HeadApiVersionCondition(String headerVersion) {
        this.headerVersion = headerVersion;
    }


    @Override
    public HeadApiVersionCondition combine(HeadApiVersionCondition other) {
        return new HeadApiVersionCondition(other.getHeaderVersion());
    }

    @Override
    public HeadApiVersionCondition getMatchingCondition(HttpServletRequest request) {
        String reqHeaderVersion = request.getHeader("X-Version");
        if (StringUtils.hasText(headerVersion) && Objects.equals(reqHeaderVersion, this.headerVersion)) {
            return this;
        }
        return null;
    }

    @Override
    public int compareTo(HeadApiVersionCondition other, HttpServletRequest request) {
        return 0;
    }

}

HeaderApiVersionRequestMappingHandlerMapping

public class HeaderApiVersionRequestMappingHandlerMapping extends ApiVersionRequestMappingHandlerMapping {

    @Override
    protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
        return createVersionCondition(handlerType.getAnnotation(HeaderApiVersion.class));
    }

    @Override
    protected RequestCondition<?> getCustomMethodCondition(Method method) {
        return createVersionCondition(method.getAnnotation(HeaderApiVersion.class));
    }

    private RequestCondition<HeadApiVersionCondition> createVersionCondition(HeaderApiVersion headerVersion) {
        if (Objects.isNull(headerVersion) || !StringUtils.hasText(headerVersion.value())) {
            return null;
        }
        return new HeadApiVersionCondition(
                Optional.ofNullable(headerVersion).map(HeaderApiVersion::value).orElse(null)
        );
    }
}

ApiVersionConfiguration

@Configuration
@ConditionalOnWebApplication
public class ApiVersionConfiguration {

    @Bean
    public WebMvcRegistrations headerVersionWebMvcRegistrations()
    {
        return new WebMvcRegistrations() {
            @Override
            public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
                return new HeaderApiVersionRequestMappingHandlerMapping();
            }
        };
    }

}

HeaderTestController

@RestController
@RequestMapping("api/test")
public class HeaderTestController {

    @GetMapping
    public String test(){
        return "test";
    }

    @HeaderApiVersion("v1")
    @GetMapping
    public String testV1(){
        return "testV1";
    }

    @HeaderApiVersion("v2")
    @GetMapping
    public String testV2(){
        return "testV2";
    }

}

v1版本的请求,进入v1的方法


v2版本的请求,进入v2的方法


v3版本请求,由于v3的版本不存在,则进入默认的方法

至于用什么实现方案,可以根据业务情况自己选择。

github代码: https://github.com/trionesdev/triones-spring-commons/tree/master/triones-spring-core/src/main/java/com/trionesdev/spring/web/apiversion

spring

关于作者

落雁沙
非典型码农
获得点赞
文章被阅读