设计一个支持多版本的APP的后端服务
- 以注册为例子的说明
我们以我们的用户中心的注册为例子我们实现的非常简单就是做一个校验,校验成功之后,把用户注册的数据入库即可。
随着我们产品的迭代注册肯定没有这么简单。比如说我需要填写一个电话号码并且拿到验证码并验证正确了才有资格注册。
但是有一个问题,我们的app升级了版本之后。用户在收到版本升级的消息时,并不一定愿意升级版本。
我们在遇到上述的情况,我们希望更新了新版本的用户使用的是需要经过验证并拿到了验证的标识。在注册的时候需要有验证标识才能做下一步的操作。所以我们可能要对接口进行改造。
第一:我们在接口中通过不同的版本号走不同的逻辑。这个其实也能解决我们遇到的问题。
但是随着版本的升级。我们会遇到更多的需求如果在代码中添加太多的判断肯定会降低接口的响应。也增加了后期维护代码的难度一句话low
第二:我们提供多个不同版本的接口。在请求发起的时候带上app的版本号。我们根据不同的版本号转发到不同的接口。这样我们便于后期代码的维护和重构。同一个接口不至于逻辑过于复杂
- 我们在用户中心实现以上的想法
由于我们在各个微服务的中都会需要多升级的版本进行控制。所以把这些通用的工具写到common的工具类中。
在common中添加api版本的包如下图:
定义一个自定义注解ApiVersion实现如下(我们把顺便把使用注解的方式以注释的方式写在实现类中):
/**
* 标识接口版本的注解类
* @author yusong
*/
/*定义该注解可以作用在的位置
* ElementType.METHOD可以在方法上加入注解
* ElementType.TYPE 在接口、类、枚举、注解可以使用该注解
*/
@Target({ElementType.METHOD,ElementType.TYPE})
/*定义注解的生命周期
* RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
*一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Documented //该注解标明在生成文档的时候是否显示该注解
@Mapping //定义用于mapping映射
public @interface ApiVersion {
/**
* 标识版本号
* @return
*/
String value();
}
- api版本管理的条件控制的实现
/**
* api版本管理的条件控制
* @author yusong
*/
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition>{
//API版本号
private String apiVersion;
//从头信息获取到的版本的字段
private static final String HEADER_VERSION = "version";
/**
* 构造函数传入版本号
*/
public ApiVersionCondition(String apiVersion) {
this.apiVersion = apiVersion;
}
/**
* 符合不同筛选条件的进行合并
*/
@Override
public ApiVersionCondition combine(ApiVersionCondition other) {
return new ApiVersionCondition(other.getApiVersion());
}
/**
* 版本对比用于排序
*/
@Override
public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
return compareTo(other.getApiVersion(),this.getApiVersion())?1:-1;
}
/**
* 根据request的header版本号进行查找匹配的筛选条件
*/
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
//从头信息中获取版本信息
String version = request.getHeader(HEADER_VERSION);
if(version!=null) {
//向下找到最近最新的一个版本 达到向下兼容的目的
if(compareTo(version, this.apiVersion)) {
return this;
}
}
//客户端未添加app版本的时候 给一个基础的版本
return new ApiVersionCondition("1.0.0");
}
private boolean compareTo(String version1,String version2) {
String[] split1 = version1.split("\\.");
String[] split2 = version2.split("\\.");
for(int i=0;i<split1.length;i++) {
if(Integer.parseInt(split1[i])<Integer.parseInt(split2[i])) {
return false;
}
}
return true;
}
public String getApiVersion() {
return apiVersion;
}
}
- 处理mapping映射管理
/**
* mapping映射管理
* @author yusong
*/
public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping{
/**
* 定义api条件控制的类 注解在类上
*/
@Override
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
return createCondition(apiVersion);
}
/**
* 定义api条件控制的类 注解在方法上
*/
@Override
protected RequestCondition<?> getCustomMethodCondition(Method method){
ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
return createCondition(apiVersion);
}
private RequestCondition<ApiVersionCondition> createCondition(ApiVersion apiVersion){
return apiVersion == null?null:new ApiVersionCondition(apiVersion.value());
}
}
- WebConfig的定义
@Configuration
public class WebConfig extends WebMvcConfigurationSupport{
//定义该值为了其他工程使用该注解生效的时候 扫描该包
public static final String WEB_CONFIG_API_VERSION = "cn.com.leimon.common.tool.util.apiversion";
/**
* 注册请求的版本请求方法
*/
@Override
public RequestMappingHandlerMapping requestMappingHandlerMapping(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {
RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping();
handlerMapping.setOrder(0);
handlerMapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
return handlerMapping;
}
}
- 让注解生效
我们在用户中心的启动类的注解@ComponentScan中加入common中公用的版本管理。如下:
@ComponentScan(basePackages={"cn.com.leimon.user",WebConfig.WEB_CONFIG_API_VERSION})
- 测试
Open接口定义添加同一个路径的的不同实现接口
@ApiOperation(value="用户注册(使用用户名密码的方式)1.0.0版本")
@PostMapping(value = "/register")
public ReturnResult register(
@ApiParam(value = "用户名",required = true)
@RequestParam(required = true)String userName,
@ApiParam(value = "密码",required = true)
@RequestParam(required = true)String password
);
@ApiOperation(value="用户注册(使用用户名密码的方式)")
@PostMapping(value = "/register")
public ReturnResult registertV1(
@ApiParam(value = "用户名",required = true)
@RequestParam(required = true)String userName,
@ApiParam(value = "密码",required = true)
@RequestParam(required = true)String password
);
@ApiOperation(value="用户注册(使用用户名密码的方式)")
@PostMapping(value = "/register")
public ReturnResult registertV2(
@ApiParam(value = "用户名",required = true)
@RequestParam(required = true)String userName,
@ApiParam(value = "密码",required = true)
@RequestParam(required = true)String password
);
我们在实现中加入版本的注解如下:
@Override
@ApiVersion(value = "1.0.0")
public ReturnResult register(String userName,String password) {
System.out.println("==========33333333333333=========================");
/**
* 1.判断用户名是否在系统中已经存在
* 2.判断密码是否符合既定的规则
* 3.入库
* 4.返回注册状态
* 5.
* 6.
*/
//判断用户是否存在
int num = userInfoService.checkRegisterUser(userName);
if(num==1) {//用户已经注册
return new ReturnResult(ResultMessage.USER_IS_REGISTER);
}
if(!StringCheckUtil.isLetterDigit(password)) {
return new ReturnResult(ResultMessage.PASSWORD_ISNOT_RULE);
}
//TODO 入库操作 并返回操作状态
//使用雪花算法计算分布式id
return null;
}
@Override
@ApiVersion(value = "1.0.1")
public ReturnResult registertV1(String userName, String password) {
System.out.println("============111111==========================");
return null;
}
@Override
@ApiVersion(value = "1.0.2")
public ReturnResult registertV2(String userName, String password) {
System.out.println("================22222222222=========================");
return null;
}
我们使用postman测试如下图所示:
发送请求时不同的version请求会落到不同的实现。说明功能已经完成了