目录
方案一
一、为什么加版本号
二、Spring Boot如何实现
测试
最后
方案二
一、自定义一个注解
二、匹配方式
三、编写配置类,加载至spring容器中
四、最后的操作
4.1、编写一个测试类
4.2、各项请求
4.2.1、test1()
4.2.2、test3()
五、代码下载
本方案采取别人劳动成果,在此谢过。本人只是为了方便自己日后查询,防止丢失,每套方案下有原链接。
方案1和方案2几乎相同,都是继承同样类重写方法不太一样 ,目前没搞明白方案一为什么这么操作,也可能只是为了类上可以不添加版本注解吧。如果有看懂的可以留言解释下哈
一般来说,api 接口是提供给其他系统或是其他公司使用,不能随意频繁的变更。然而,需求和业务不断变化,接口和参数也会发生相应的变化。如果直接对原来的接口进行修改,势必会影响线其他系统的正常运行。这就必须对api 接口进行有效的版本控制。
例如,添加用户的接口,由于业务需求变化,接口的字段属性也发生了变化而且可能和之前的功能不兼容。为了保证原有的接口调用方不受影响,只能重新定义一个新的接口。
Api 版本控制的方式:
1、域名区分管理,即不同的版本使用不同的域名,v1.api.test.com,v2.api.test.com
2、请求url 路径区分,在同一个域名下使用不同的url路径,test.com/api/v1/,test.com/api/v2
3、请求参数区分,在同一url路径下,增加version=v1或v2 等,然后根据不同的版本,选择执行不同的方法。
实际项目中,一般选择第二种:请求url路径区分。因为第二种既能保证水平扩展,有不影响以前的老版本
实现方案:
1、首先创建自定义的@APIVersion 注解和自定义URL匹配规则ApiVersionCondition。
2、然后创建自定义的 RequestMappingHandlerMapping 匹配对应的request,选择符合条件的method handler。
1、创建自定义注解
首先,在com.weiz.config 包下,创建一个自定义版本号标记注解 @ApiVersion。
package com.weiz.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* API版本控制注解
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
/**
* @return 版本号
*/
int value() default 1;
}
说明:
ApiVersion 为自定义的注解,API版本控制,返回对应的版本号。
2、自定义url匹配逻辑
创建 ApiVersionCondition 类,并继承RequestCondition 接口,作用是:版本号筛选,将提取请求URL中版本号,与注解上定义的版本号进行比对,以此来判断某个请求应落在哪个controller上。
在com.weiz.config 包下创建ApiVersionCondition 类,重写 RequestCondition,创建自定义的url匹配逻辑。
package com.weiz.config;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import javax.servlet.http.HttpServletRequest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ApiVersionCondition implements RequestCondition {
private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile(".*v(\\d+).*");
private int apiVersion;
ApiVersionCondition(int apiVersion) {
this.apiVersion = apiVersion;
}
private int getApiVersion() {
return apiVersion;
}
@Override
public ApiVersionCondition combine(ApiVersionCondition apiVersionCondition) {
return new ApiVersionCondition(apiVersionCondition.getApiVersion());
}
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {
Matcher m = VERSION_PREFIX_PATTERN.matcher(httpServletRequest.getRequestURI());
if (m.find()) {
Integer version = Integer.valueOf(m.group(1));
if (version >= this.apiVersion) {
return this;
}
}
return null;
}
@Override
public int compareTo(ApiVersionCondition apiVersionCondition, HttpServletRequest httpServletRequest) {
return apiVersionCondition.getApiVersion() - this.apiVersion;
}
}
当方法级别和类级别都有ApiVersion注解时,二者将进行合并(ApiVersionRequestCondition.combine)。最终将提取请求URL中版本号,与注解上定义的版本号进行比对,判断url是否符合版本要求。
3、自定义匹配的处理器
在com.weiz.config 包下创建 ApiRequestMappingHandlerMapping 类,重写部分 RequestMappingHandlerMapping 的方法。
package com.weiz.config;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.lang.reflect.Method;
public class ApiRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
private static final String VERSION_FLAG = "{version}";
private static RequestCondition createCondition(Class> clazz) {
RequestMapping classRequestMapping = clazz.getAnnotation(RequestMapping.class);
if (classRequestMapping == null) {
return null;
}
StringBuilder mappingUrlBuilder = new StringBuilder();
if (classRequestMapping.value().length > 0) {
mappingUrlBuilder.append(classRequestMapping.value()[0]);
}
String mappingUrl = mappingUrlBuilder.toString();
if (!mappingUrl.contains(VERSION_FLAG)) {
return null;
}
ApiVersion apiVersion = clazz.getAnnotation(ApiVersion.class);
return apiVersion == null ? new ApiVersionCondition(1) : new ApiVersionCondition(apiVersion.value());
}
@Override
protected RequestCondition> getCustomMethodCondition(Method method) {
return createCondition(method.getClass());
}
@Override
protected RequestCondition> getCustomTypeCondition(Class> handlerType) {
return createCondition(handlerType);
}
}
4、配置注册自定义的RequestMappingHandlerMapping
重写请求过处理的方法,将之前创建的 ApiRequestMappingHandlerMapping 注册到系统中。
package com.weiz.config;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
@Configuration
public class WebMvcRegistrationsConfig implements WebMvcRegistrations {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new ApiRequestMappingHandlerMapping();
}
}
上面四步,把api 版本控制配置完了。代码看着复杂,其实都是重写spring boot 内部的处理流程。
配置完成之后,接下来编写测试的控制器进行测试。
1、在Controller/api 目录下,分别创建UserV1Controller 和 UserV2Controller
UserV1Controller
@RequestMapping("api/{version}/user")
@RestController
public class UserV1Controller {
@GetMapping("/test")
public String test() {
return "version1";
}
@GetMapping("/extend")
public String extendTest() {
return "user v1 extend";
}
}
UserV2Controller
@RequestMapping("api/{version}/user")
@RestController
@ApiVersion(2)
public class UserV2Controller {
@GetMapping("/test")
public String test() {
return "user v2 test";
}
}
2、启动项目后,输入相关地址,查看版本控制是否生效
测试结果:
正确的接口地址
继承的接口地址
说明:
上图的前两个截图说明,请求正确的版本地址,会自动匹配版本的对应接口。当请求的版本大于当前版本时,默认匹配当前版本。
第三个截图说明,当请求对应的版本不存在接口时,会匹配之前版本的接口,即请求/v2/user/extend 接口时,由于v2 控制器未实现该接口,所以自动匹配v1 版本中的接口。这就是所谓的版本继承。
以上,就把Spring Boot 如何优雅的设计 Restful API 接口版本号,实现 API 版本控制介绍完了。版本控制和权限验证是rest api 的基础,虽然看着比较复杂,但是理解了,要实现还是比较简单的。
来源:Spring Boot入门系列(二十一)如何优雅的设计 Restful API 接口版本号,实现 API 版本控制! - 章为忠 - 博客园
一个项目上线后,会根据不同的需求不断的进行更新和维护操作。有可能接口的请求也会发生改变,比如之前 /api/v1 ,后来随着业务变更,需要使用 /api/v9,但有些接口根本就无需变更,一样沿用即可。
如何做到一个接口多个版本可以动态适用呢?
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.web.bind.annotation.Mapping;
@Target({ElementType.METHOD,ElementType.TYPE})//用于方法和类上
@Retention(RetentionPolicy.RUNTIME)//运行时有效
@Documented //标识这是个注解并应该被 javadoc工具记录
@Mapping //标识映射
public @interface ApiVersion {
/**
* 标识版本号
* @return
*/
int value() default 1;//default 表示默认值
}
《@Mapping注解》
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
public class ApiVersionCondition implements RequestCondition {
private static Logger log = LoggerFactory.getLogger(ApiVersionCondition.class);
// 路径中版本的前缀, 这里用 /v[1-9]/的形式
private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+)/");
//类中的成员变量,此处表示到时候接口中传递的参数接收
private int apiVersion;
//构造
public ApiVersionCondition(int apiVersion){
this.apiVersion = apiVersion;
}
public int getApiVersion() {
return apiVersion;
}
//创建新的实例
@Override
public ApiVersionCondition combine(ApiVersionCondition other) {
// 采用最后定义优先原则,则方法上的定义覆盖类上面的定义
return new ApiVersionCondition(other.getApiVersion());
}
//校验请求url中是否包含指定的字段,如果存在则进行正则匹配
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
log.info("---- getMatchingCondition ----");
Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI());
if(m.find()){
Integer version = Integer.valueOf(m.group(1));
log.info("---- getMatchingCondition ----version="+String.valueOf(version));
//如果当前url中传递的版本信息高于(或等于)申明(或默认)版本,则用url的版本
if(version >= this.apiVersion){
return this;
}
}
//不匹配,则不用管
return null;
}
//重写比较方式
@Override
public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
// 优先匹配最新的版本号
return other.getApiVersion() - this.apiVersion;
}
}
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
@Configuration
public class ApiConfiguration implements WebMvcRegistrations {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new CustomRequestMappingHandlerMapping();
}
}
在父接口中,我们需要重写指定的一个方法。
但是,它本身返回的是一个org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping类,这个类中有很多处理请求映射的方法,但是我们需要使用几个指定的。
import java.lang.reflect.Method;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
@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);
}
private RequestCondition createCondition(ApiVersion apiVersion) {
return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value());
}
}
最后的操作当然是测试咯
@Controller
public class VersionControllerTest {
@ApiVersion
@GetMapping("/{version}/test1")
@ResponseBody
public String test1(){
return "test 1";
}
@ApiVersion
@GetMapping("/{ver}/test2")
@ResponseBody
public String test2(){
return "test 2";
}
@ApiVersion(3)
@GetMapping("/{version}/test3")
@ResponseBody
public String test3(){
return "test 3";
}
}
test3()和test1()相比,变更了一个注解。
@ApiVersion(3)
这个有什么用呢?
我们先请求 大于等于3的
换个小于3的
由此可见 @ApiVersion(3)自定义注解,传递参数信息表示默认最低的版本限制。
《springboot 自定义版本控制代码》
参考来源 :SpringBoot 2.X——接口版本控制_专注写bug的博客-CSDN博客