很多大型的平台涉及到对外开放API接口或者内部终端Restful接口, 接口涉及到升级和更新后, 一般会考虑保留原有API接口, 将升级版本的API接口通过更高的版本号进行标记, 这样已有用户的客户端程序可以保持正常工作的同时, 选择某个时机升级到新版本接口.
一般接口的版本设计路径大致如下:
/v1/api/user/info
升级后的接口如下:
/v2/api/user/info
如何在Spring Cloud体系中做到更加优雅的API版本管理是我们本文所要描述的重点, 一般情况我们希望通过配置或者注解的方式很简单的标记某个接口的版本, 这里我们通过注解实现API的版本标记, 使用方式如下:
/**
* 已有老的获取用户信息接口, 实际请求路径/v1/api/user/info
*/
@ApiVersion(1)
@GetMapping("/api/user/info")
User selectByUser(String userId);
/**
* 新增获取用户信息接口, 并增加是否获取内部用户的标记
* 实际请求路径/v2/api/user/info
*/
@ApiVersion(2)
@GetMapping("/api/user/info")
User selectByUser(String userId, boolean isInner);
为了更加扩展化, 我们将支持更多的自定义注解路径标记, 比如我们可以通过@Inner接口标记微服务之间的接口仅支持内部调用, 不能对外开放. 通过@Web标记此接口只能网页端掉用, 通过@App标记此接口仅能APP端访问, 这样我们就可以在统一权限拦截中心进行注解解析和统一安全拦截. 使用方式如下:
/**
* APP获取用户信息接口
* 实际请求路径/app/user/info
*/
@App
@GetMapping("/user/info")
User selectByUserForApp(String userId);
/**
* web获取用户信息接口
* 实际请求路径/web/user/info
*/
@Web
@GetMapping("/user/info")
User selectByUserForWeb(String userId);
要实现Spring Cloud自定义路径解析, 我们需要有如下三个步骤:
1、自定义路径注解.
2、Spring实现解析映射.
3、Feigh实现解析映射.
为了实现更多路径注解, 我们统一定义父类注解实现所有的自定义路由:
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomPathPrefix {
@AliasFor("path")
String value() default "";
@AliasFor("value")
String path() default "";
int version() default -1;
}
为了语义性更好, 我们定义了path属性与value互为别名. 然后在自定义路径前缀上实现如下特殊注解, 这里仅举例:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@CustomPathPrefix("/v")
public @interface ApiVersion {
/**
* 版本编号
* @return
*/
@AliasFor(annotation = CustomPathPrefix.class, attribute = "version")
int value();
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@CustomPathPrefix("/web")
public @interface Web {
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@CustomPathPrefix("/app")
public @interface App {
}
要实现Spring的解析映射, 我们需要首先了解RequestMappingHandlerMapping的机制, RequestMappingHandlerMapping作为Spring MVC中的核心类, 用于解析类和方法上的映射关系, RequestMappingHandlerMapping实现了完整的映射解析, 我们只需要继承它并将自定义注解的解析逻辑增加上即可.
首先我们需要自定义一个CustomPathPrefix的RequestCondition信息:
public class CustomPathPrefixCondition implements RequestCondition {
private String prefixPath;
public CustomPathPrefixCondition(String prefixPath) {
this.prefixPath = prefixPath;
}
@Override
public CustomPathPrefixCondition combine(CustomPathPrefixCondition other) {
return new CustomPathPrefixCondition(other.getPrefixPath());
}
@Override
public CustomPathPrefixCondition getMatchingCondition(HttpServletRequest request) {
return matchPrefixPath(request) ? this : null;
}
@Override
public int compareTo(CustomPathPrefixCondition other, HttpServletRequest request) {
return other.getPrefixPath() == null ? -1
: this.prefixPath == null ? 1 : other.getPrefixPath().compareTo(this.prefixPath);
}
public String getPrefixPath() {
return prefixPath;
}
/**
* 匹配路径中的前缀
*
* @param requestPath
* @return
*/
private boolean matchPrefixPath(HttpServletRequest request) {
String uri = request.getRequestURI();
String contextPath = request.getContextPath();
if (StringUtils.isNotBlank(contextPath)) {
uri = uri.substring(contextPath.length());
}
return uri.startsWith(this.prefixPath);
}
}
然后继承RequestMappingHandlerMapping实现自定义路径的解析映射, 详见代码中解释:
public class CustomPathPrefixRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
private static final Logger LOG = LoggerFactory.getLogger(CustomPathPrefixRequestMappingHandlerMapping.class);
@Override
protected RequestCondition getCustomTypeCondition(Class> handlerType) {
CustomPathPrefix customPathPrefix = AnnotationUtils.findAnnotation(handlerType, CustomPathPrefix.class);
return createCondition(customPathPrefix);
}
@Override
protected RequestCondition getCustomMethodCondition(Method method) {
CustomPathPrefix customPathPrefix = AnnotationUtils.findAnnotation(method, CustomPathPrefix.class);
return createCondition(customPathPrefix);
}
/**
* 从CustomPathPrefix注解中解析路径和版本号信息
* @param customPathPrefix
* @return
*/
private RequestCondition createCondition(CustomPathPrefix customPathPrefix) {
return customPathPrefix == null ? null
: new CustomPathPrefixCondition(customPathPrefix.value()
+ (customPathPrefix.version() >= 0 ? String.valueOf(customPathPrefix.version()) : ""));
}
@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class> handlerType) {
// 过滤掉不包含@RequestMapping的方法
if (!hasRequestMapping(method)) {
return null;
}
if (hasRequestMapping(handlerType)) {
// Class上包含@RequestMapping
// method按照原有模式生成RequestMappingInfo
RequestMappingInfo requestMappingInfo = this.createRequestMappingInfo(method, null);
if (requestMappingInfo != null) {
// class追加版本号前缀
CustomPathPrefix customPathPrefix = (CustomPathPrefix) AnnotatedElementUtils.findMergedAnnotation(method,
CustomPathPrefix.class);
RequestMappingInfo classRequestMappingInfo = this.createRequestMappingInfo(handlerType, customPathPrefix);
if (classRequestMappingInfo != null) {
requestMappingInfo = classRequestMappingInfo.combine(requestMappingInfo);
}
}
return requestMappingInfo;
} else {
// Class上不包含@RequestMapping
CustomPathPrefix customPathPrefix = (CustomPathPrefix) AnnotatedElementUtils.findMergedAnnotation(method,
CustomPathPrefix.class);
return this.createRequestMappingInfo(method, customPathPrefix);
}
}
/**
* class/method上是否有RequestMapping
*
* @param element
* @return
*/
private boolean hasRequestMapping(AnnotatedElement element) {
return AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class) != null;
}
/**
* 生成RequestMappingInfo
*
* @param element
* @param customPathPrefix
* @return
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element, CustomPathPrefix customPathPrefix) {
RequestMapping requestMapping = (RequestMapping) AnnotatedElementUtils.findMergedAnnotation(element,
RequestMapping.class);
RequestCondition> condition = element instanceof Class ? this.getCustomTypeCondition((Class) element)
: this.getCustomMethodCondition((Method) element);
if (customPathPrefix == null || StringUtils.isEmpty(customPathPrefix.value())) {
// 没有路径前缀信息或者前缀配置为空, 直接按照原有方式生成RequestMappingInfo
return this.createRequestMappingInfo(requestMapping, condition);
}
try {
// 动态修改RequestMapping注解的属性
InvocationHandler invocationHandler = Proxy.getInvocationHandler(requestMapping);
Field field = invocationHandler.getClass().getDeclaredField("valueCache");
field.setAccessible(true);
Map map = (Map) field.get(invocationHandler);
String prefix = customPathPrefix.value()
+ (customPathPrefix.version() >= 0 ? String.valueOf(customPathPrefix.version()) : "");
// 更新path信息
String[] paths = new String[requestMapping.path().length];
for (int i = 0; i < requestMapping.path().length; i++) {
paths[i] = prefix.concat(
requestMapping.path()[i].startsWith("/") ? requestMapping.path()[i] : "/" + requestMapping.path()[i]);
}
map.put("path", paths);
// 更新value信息
String[] values = new String[requestMapping.value().length];
for (int i = 0; i < requestMapping.value().length; i++) {
values[i] = prefix.concat(
requestMapping.value()[i].startsWith("/") ? requestMapping.value()[i] : "/" + requestMapping.value()[i]);
}
map.put("value", values);
} catch (Exception e) {
LOG.error("fail to createRequestMappingInfo", e);
}
return this.createRequestMappingInfo(requestMapping, condition);
}
}
然后将自定义handler注入到Spring容器:
@Configuration
public class CustomPathPrefixSpringWebMvcConfiguration implements WebMvcRegistrations {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new CustomPathPrefixRequestMappingHandlerMapping();
}
}
Feigh通过SpringMvcContract进行解析映射, 我们只需要继承SpringMvcContract实现自己特有的CustomPathPrefix解析即可, 核心通过processAnnotationOnMethod方法进行实现, 详细代码如下:
public class CustomPathPrefixSpringMvcContract extends SpringMvcContract {
public CustomPathPrefixSpringMvcContract() {
super();
}
public CustomPathPrefixSpringMvcContract(List annotatedParameterProcessors) {
super(annotatedParameterProcessors);
}
public CustomPathPrefixSpringMvcContract(List annotatedParameterProcessors,
ConversionService conversionService) {
super(annotatedParameterProcessors, conversionService);
}
@Override
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
super.processAnnotationOnMethod(data, methodAnnotation, method);
if (!needProcessCustomPathPrefix(method)) {
return;
}
RequestMapping requestMapping = (RequestMapping) AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
if (requestMapping.value().length == 0) {
return;
}
CustomPathPrefix customPathPrefix = (CustomPathPrefix) AnnotatedElementUtils.findMergedAnnotation(method,
CustomPathPrefix.class);
String pathValue = emptyToNull(requestMapping.value()[0]);
String customPrefix = customPathPrefix.value()
+ (customPathPrefix.version() >= 0 ? String.valueOf(customPathPrefix.version()) : "");
if (pathValue != null && StringUtils.isNotBlank(customPathPrefix.value())
&& !data.template().url().contains(customPrefix)) {
pathValue = pathValue.startsWith("/") ? pathValue : "/" + pathValue;
String pathPrefix = customPrefix.startsWith("/") ? customPrefix : "/" + customPrefix;
int pathIndex = data.template().url().indexOf(pathValue);
if (pathIndex >= 0) {
// 将前缀插入到URI之前
data.template().insert(pathIndex, pathPrefix);
}
}
}
/**
* 是否需要处理自定义前缀注解
*
* @param method
* @return
*/
private boolean needProcessCustomPathPrefix(Method method) {
return AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class) != null
&& AnnotatedElementUtils.findMergedAnnotation(method, CustomPathPrefix.class) != null;
}
}
然后我们把自定义契约注入到Spring容器中:
@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignClientsConfiguration.class)
public class CustomPathPrefixFeignConfiguration {
@Autowired(required = false)
private List feignFormatterRegistrars = new ArrayList<>();
/**
* 自定义契约实现
*
* @param feignConversionService
* @return
*/
@Bean
public Contract feignContract() {
FormattingConversionService conversionService = new DefaultFormattingConversionService();
for (FeignFormatterRegistrar feignFormatterRegistrar : feignFormatterRegistrars) {
feignFormatterRegistrar.registerFormatters(conversionService);
}
List annotatedArgumentResolvers = Lists.newArrayList(new PathVariableParameterProcessor(),
new RequestParamParameterProcessor(), new RequestHeaderParameterProcessor(), new FormBodyParameterProcessor());
return new CustomPathPrefixSpringMvcContract(annotatedArgumentResolvers, conversionService);
}
}