自定义注解实现 API 版本管理 实现多版本共存
spring boot 实现接口多版本共存 灵活修改。GitHub 地址:https://github.com/zsr251/SpringBoot-Mybatis-ApiVersion
现状和问题
- 一般设计中无法针对单个接口进行灵活的版本管理
- 接口更新时需要兼容之前的接口,导致接口的参数会越来越多
- 接口更新后会有很多判断当前版本的 if else
- 接口多个版本不能并存
原理
- 自定义方法和参数注解
- 利用拦截器和反射,根据传入的版本参数调用 Controller 方法注解中指定的类和对应的方法
解决的问题
- 可针对单个 Controller 方法进行版本控制,允许只升级某一个或几个接口,无需整体版本升级。加入版本控制的 Controller 方法只需要在方法上加上注解即可
- 不更改原 URL,兼容以前版本。使用该版本控制可以有效的兼容旧版本的 APP,只需要修改服务端,无需强制升级 APP
- 接口版本完全不用 if else 判断
- 接口多个版本可并存,升级之前的代码完全保留
- 同一个接口每个版本之间参数可以完全不同,无需兼容之前其他版本的参数
- 入口和业务实现完全分离,灵活可配置,升级接口无需修改之前实现
存在的问题
- 依赖与 spring mvc
- 没有对返回值进行 json 处理
- 返回页面的请求无法使用(ps:虽然返回页面的也不建议使用版本控制)
- 没有复用 spring 获取参数的方法,目前是自己的简单实现
- 目前不支持从 body 中获取参数
- 目前只支持基础路径参数,不支持路径中添加正则方式获取参数
实现
核心实现在自定义的两个注解和一个拦截器
类名 | 类型 | 主要作用 |
---|---|---|
PathVariable | 参数注解 | spring 自带注解,用于引用在 URL 中的参数 |
ApiVersion | 方法注解 | 注解在需要进行接口版本控制的 Controller 的对应方法上 |
ApiParam | 参数注解 | 注解在真正处理方法的参数中,非 PathVariable 参数必须含有该注解 |
DefaultValueEnum | 默认参数值 | 枚举 |
ApiVersionException | 异常 | 工具解析中抛出的异常 |
ApiVersionInterceptor | 接口拦截器 | 核心类,进行所有处理操作 |
方法注解
/**
* 需要拦截的API接口方法
* Created by jasonzhu on 2016/11/28.
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiVersion {
/**
* 指定执行操作的类名
*/
Class targetClass();
/**
* 指定执行操作的方法名前缀
*/
String methodPreName() default "";
}
targetClass 必须指定,指定真正进行业务处理的类,methodPreName 如果不指定则为当前 Controller 方法的方法名。例如:
/**
* API版本管理测试
*/
@ApiVersion(targetClass = TestApiVersionDo.class)
@RequestMapping("/api/test")
public void test(){}
真正执行业务处理的类为:TestApiVersionDo,对应的版本方法是 test + 版本号,默认是:test1 方法
参数注解
/**
* 处理方法的参数注解
* Created by jasonzhu on 2016/11/30.
*/
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiParam {
/**
* 参数名
*/
String value();
/**
* 是否必须有值 默认必须有
*/
boolean required() default true;
/**
* 值非必须时 如果未传值 默认值
*/
DefaultValueEnum defaultValue() default DefaultValueEnum.NULL;
}
注解在真正处理方法的参数中,非 PathVariable 参数必须含有该注解,如果没有指定则会报错。
/**
* 版本拦截器之后真正执行的方法
* Created by jasonzhu on 2016/12/1.
*/
@Controller
public class TestApiVersionDo {
public String test1(){
return "调用成功 没有参数";
}
public String test2(@ApiParam("av") Integer av,@ApiParam("a") String app,@ApiParam(value = "b",required = false) String b){
return "调用成功 三个参数 app:"+app+" av:"+av+" b:"+b;
}
}
拦截器
代码有点长,摘录一部分,建议直接 Github 下载源码查看,文章中可直接跳过。
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod method = (HandlerMethod) handler;
ApiVersion apiVersion = method.getMethodAnnotation(ApiVersion.class);
//判断是否纳入接口版本控制
if (apiVersion == null) {
return true;
}
//TODO 判断如果是返回页面的方法 则不纳入版本控制
//controller中调用的方法
RequestMapping requestMapping = method.getMethodAnnotation(RequestMapping.class);
String [] mappingPaths = requestMapping.value()[0].split("/");
String [] requestPaths = request.getRequestURI().split("\\.")[0].split("/");
Class cls = apiVersion.targetClass();
Object o;
try {
o = context.getBean(cls);
} catch (Exception e) {
throw new ApiVersionException("指定的处理类必须纳入spring的bean管理", e);
}
String preName = apiVersion.methodPreName();
if (preName == null || preName.trim().isEmpty()) {
preName = method.getMethod().getName();
}
//默认接口版本号
String av = "1";
//参数列表
Map<String, String[]> requestParam = request.getParameterMap();
if (requestParam.get(API_VERSION) != null) {
av = requestParam.get(API_VERSION)[0];
}
Method[] methods = cls.getMethods();
if (methods == null) {
writeMsg(response, "未找到响应方法");
return false;
}
Method targetMethod = null;
//找到指定的处理方法
for (Method me : methods) {
if (me.getName().equals(preName + av)) {
targetMethod = me;
break;
}
}
if (targetMethod == null) {
writeMsg(response, "非法请求");
return false;
}
if (!targetMethod.getReturnType().equals(String.class)) {
throw new ApiVersionException("响应方法返回类型必须为String :" + targetMethod.getName());
}
//获得方法的参数
Class<?>[] paramTypes = targetMethod.getParameterTypes();
Integer paramLength = paramTypes.length;
//调动方法的参数
Object[] paramList = new Object[paramLength];
Annotation[][] annotationss = targetMethod.getParameterAnnotations();
//总注解参数个数
for (int i = 0; i < annotationss.length; i++) {
Annotation[] annotations = annotationss[i];
if (annotations.length < 1)
throw new ApiVersionException("存在未添加@ApiParam注解参数的方法 :" + targetMethod.getName());
//是否存在ApiParam或PathVariable注解
boolean hasAnn = false;
for (int j = 0; j < annotations.length; j++) {
Annotation annotation = annotations[j];
if (annotation instanceof ApiParam) {
if (paramTypes[i].equals(HttpServletRequest.class)){
paramList[i] = request;
}else if (paramTypes[i].equals(HttpServletResponse.class)) {
paramList[i] = response;
}else{
//为参数赋值
paramList[i] = getParam(requestParam, (ApiParam) annotation, paramTypes[i]);
}
hasAnn = true;
break;
}
if (annotation instanceof PathVariable){
//为参数赋值
paramList[i] = getPathVariable(requestPaths,mappingPaths,(PathVariable) annotation,paramTypes[i]);
hasAnn = true;
break;
}
}
if (!hasAnn)
throw new ApiVersionException("存在未添加@ApiParam或@PathVariable注解参数的方法 :" + targetMethod.getName());
}
//反射方法调用
String result = (String) targetMethod.invoke(o, paramList);
writeMsg(response, result);
return false;
}
针对存在问题的解决方案
干!
其他
没有特殊需求的话,这个版本已经可以使用了。在 2017 年的最后一个月还是要维护一个版本,毕竟将近一年没有更新了 ~
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于