准备将现有的接口加上版本管理,兼容以前的版本。网上一调研,发现有很多示例,但是还是存在以下两个问题。
1.大部分使用Integer作为版本号,但是通常的版本号形式为v1.0.0,
2.版本号携带在header中,对接调用不清晰。
针对以上两个问题,做如下改造。
1.自定义接口版本注解ApiVersion
后面条件映射使用equals匹配,此处是否将String变为String[]应对多个版本使用同一代码的问题。
package com.yugioh.api.common.core.version;
import org.springframework.web.bind.annotation.Mapping;
import java.lang.annotation.*;
/**
* 接口版本
*
* @author lieber
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
String value() default "1.0.0";
}
2.请求映射条件ApiVersionCondition
package com.yugioh.api.common.core.version;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 版本控制
*
* @author lieber
*/
@AllArgsConstructor
@Getter
@Slf4j
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
private String version;
private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile(".*v(\\d+(.\\d+){0,2}).*");
public final static String API_VERSION_CONDITION_NULL_KEY = "API_VERSION_CONDITION_NULL_KEY";
@Override
public ApiVersionCondition combine(ApiVersionCondition other) {
// 方法上的注解优于类上的注解
return new ApiVersionCondition(other.getVersion());
}
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI());
if (m.find()) {
String version = m.group(1);
if (this.compareTo(version)) {
return this;
}
}
// 将错误放在request中,可以在错误页面明确提示,此处可重构为抛出运行时异常
request.setAttribute(API_VERSION_CONDITION_NULL_KEY, true);
return null;
}
@Override
public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
return this.compareTo(other.getVersion()) ? 1 : -1;
}
private boolean compareTo(String version) {
return Objects.equals(version, this.version);
}
}
3.创建自定义匹配处理器ApiVersionRequestMappingHandlerMapping
网上大部分只重写了getCustomTypeCondition和getCustomMethodCondition方法。这里为了解决路由映射问题,重写getMappingForMethod方法,在路由中加入前缀{version},加入后路由变为/api/{version}/xxx
package com.yugioh.api.common.core.version;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.lang.reflect.Method;
/**
* 自定义匹配处理器
*
* @author lieber
*/
public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
private final static String VERSION_PREFIX = "{version}";
@Override
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
return createCondition(apiVersion);
}
@Override
protected RequestCondition<?> getCustomMethodCondition(Method method) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
return createCondition(apiVersion);
}
@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMappingInfo requestMappingInfo = super.getMappingForMethod(method, handlerType);
if (requestMappingInfo == null) {
return null;
}
return createCustomRequestMappingInfo(method, handlerType, requestMappingInfo);
}
private RequestMappingInfo createCustomRequestMappingInfo(Method method, Class<?> handlerType, RequestMappingInfo requestMappingInfo) {
ApiVersion methodApi = AnnotatedElementUtils.findMergedAnnotation(method, ApiVersion.class);
ApiVersion handlerApi = AnnotatedElementUtils.findMergedAnnotation(handlerType, ApiVersion.class);
if (methodApi != null || handlerApi != null) {
return RequestMappingInfo.paths(VERSION_PREFIX).options(this.config).build().combine(requestMappingInfo);
}
return requestMappingInfo;
}
private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();
private RequestCondition<ApiVersionCondition> createCondition(ApiVersion apiVersion) {
return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value());
}
}
4.使用ApiVersionConfig配置来决定是否开启多版本
package com.yugioh.api.common.core.version;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 版本管理器配置
*
* @author lieber
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "api.config.version", ignoreInvalidFields = true)
public class ApiVersionConfig {
/**
* 是否开启
*/
private boolean enable;
}
5.配置WebMvcRegistrations,启用自定义路由Mapping
package com.yugioh.api.common.core.version;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
* @author lieber
*/
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
public class ApiWebMvcRegistrations implements WebMvcRegistrations {
private final ApiVersionConfig apiVersionConfig;
@Autowired
public ApiWebMvcRegistrations(ApiVersionConfig apiVersionConfig) {
this.apiVersionConfig = apiVersionConfig;
}
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
if (apiVersionConfig.isEnable()) {
return new ApiVersionRequestMappingHandlerMapping();
}
return null;
}
}
至此我们就能在项目中愉快的使用@APIVersion来指定版本了,但是现在这个和swagger整合还会有问题,继续研究中…
参考资料:
1.https://www.jianshu.com/p/1389fdf20ee2
2.https://blog.csdn.net/gongjian1990/article/details/105533574