Spring MVC 实现REST风格API版本控制
项目的开发会是一个迭代一直更新的过程,从软件工程的角度说,除非项目进入废弃,否则项目从开始到维护升级都是一个不断更新的项目。项目的版本更新和升级这个概念很好理解,但是实施起来切实是一个痛点。现在大部分的系统后端架构通常都是基于一个网关对外暴露API接口,这些API包括网页端API接口,APP端接口和小程序端接口等。
后端项目的升级可以通过我们自己来升级,但是对于用户而言,类似APP或者PC端程序用户而言,更新程序与后端接口达成一致性是很困难的,特别是你的项目群体特别大的时候,没有出现特别大的后端版本更新的时候,通常都只是提示用户更新而已。所以会造成部分用户更新至最新的版本,而部分用户还停留在旧版本中。所以一个接口操作会存在不同版本客户端请求。相比PC端应用程序和移动端应用程序,C/S架构的应用程序,可以做到及时更新,但是也会存在一个问题,
- 对于当前的接口,Web端API更新到最新了,但是移动端或者PC端用户还没有更新上来。
所以系统的更新同样需要考虑接口的版本问题。本文章综合网络上的各种解决方案,写一个基于Spring MVC的REST风格的API版本方法。
上代码:
定义一个Api版本的注解
/**
* @author shaoyayu
* @date 2021/12/10
* @apiNote api接口的注解
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
int value();
}
定义一个类实现RequestCondition
/**
* @author shaoyayu
* @date 2021/12/10
* @apiNote
*/
@Getter
@Setter
@Slf4j
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
private static final Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+)/");
private int apiVersion;
public ApiVersionCondition(int apiVersion) {
this.apiVersion = apiVersion;
}
@Override
public ApiVersionCondition combine(ApiVersionCondition apiVersionCondition) {
return new ApiVersionCondition(apiVersionCondition.getApiVersion());
}
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
try {
Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI());
if (m.find()){
int version = Integer.parseInt(m.group(1));
if (version>=this.apiVersion){
return this;
}
}
return null;
}catch (Exception e){
log.info("api 版本转换异常:"+request.getRequestURI());
}
return null;
}
@Override
public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
return other.getApiVersion() - this.apiVersion;
}
}
到这里已经完成一半了,但是需要一个继承RequestMappingHandlerMapping的实现类
/**
* @author shaoyayu
* @date 2021/12/10
* @apiNote
*/
public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
@Override
protected RequestCondition<ApiVersionCondition> getCustomTypeCondition(Class<?> handlerType) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
return createCondition(apiVersion);
}
@Override
protected RequestCondition<ApiVersionCondition> 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());
}
}
剩下的就是把这个实现RequestMappingHandlerMapping的CustomRequestMappingHandlerMapping注入到容器中。
有两种方法实现注册。一通过配置类里面,注入
/**
* @author shaoyayu
* @date 2021/12/10
* @apiNote
*/
@Configuration
public class WebConfiguration extends WebMvcAutoConfiguration {
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping(){
RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping();
handlerMapping.setOrder(0);
return handlerMapping;
}
}
这种方式注入Spring Boot版本越高也是不推荐这种方法。
第二种方法,通过继承WebMvcConfigurationSupport,在createRequestMappingHandlerMapping方法中返回
/**
* @author shaoyayu
* @date 2021/12/10
* @apiNote
*/
@Configuration
public class CustomWebMvcConfigurationSupport extends WebMvcConfigurationSupport {
@Override
protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping();
handlerMapping.setOrder(0);
return handlerMapping;
}
}
个人推荐第二种方式。
测试
定义一个UserController
/**
* @author shaoyayu
* @date 2021/12/10
* @apiNote SonarLint: Remove this empty class, write its code or make it an "interface".
*/
@Slf4j
@RestController
@RequestMapping("{version}/web/user")
public class UserController {
@GetMapping("/hello")
public Map<String,Object> hello(){
Map<String, Object> map = new HashMap<>();
map.put("code",200);
map.put("ok",true);
map.put("msg","v api interface");
map.put("data",null);
return map;
}
@GetMapping("/hello")
@ApiVersion(1)
public Map<String,Object> hello1(){
Map<String, Object> map = new HashMap<>();
map.put("code",200);
map.put("ok",true);
map.put("msg","v1 api interface");
map.put("data",null);
return map;
}
@GetMapping("/hello")
@ApiVersion(2)
public Map<String,Object> hello2(){
Map<String, Object> map = new HashMap<>();
map.put("code",200);
map.put("ok",true);
map.put("msg","v2 api interface");
map.put("data",null);
return map;
}
}
测试的结果
-
如访问/web/user/hello不带版本号,出现404的结果
-
访问/v/web/user/hello会调用第一个方法
-
访问/v1/web/user/helo会调用第二个方法。
-
访问/v2/web/user/helo会调用第三个方法。
-
访问/v3/web/user/hello会调用第三个方法。
综合的测试结果,
-
不带v的版本控制会出现404
-
版本号会向下访问比自己更低一级的版本