背景
在业务开发中,尤其是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() ;
}
@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