叙述
一般来说,api 接口是提供给其他系统或是其他公司使用,不能随意频繁的变更。然而,需求和业务不断变化,接口和参数也会发生相应的变化。如果直接对原来的接口进行修改,势必会影响线其他系统的正常运行。这就必须对api 接口进行有效的版本控制。
例如,添加用户的接口,由于业务需求变化,接口的字段属性也发生了变化而且可能和之前的功能不兼容。为了保证原有的接口调用方不受影响,只能重新定义一个新的接口。
http://localhost:8080/api/v1/user
http://localhost:8080/api/v2/user
Api 版本控制的方式:
- 域名区分管理,即不同的版本使用不同的域名,
v1.api.test.com
,v2.api.test.com
- 请求url 路径区分,在同一个域名下使用不同的url路径,test.com/api/v1/,test.com/api/v2 11
- 请求参数区分,在同一url路径下,增加version=v1或v2 等,然后根据不同的版本,选择执行不同的方法。
实际项目中,一般选择第二种:请求url路径区分。因为第二种既能保证水平扩展,有不影响以前的老版本。
解决方案
实现方案:
- 首先创建自定义的@APIVersion 注解和自定义URL匹配规则ApiVersionCondition。
- 然后创建自定义的 RequestMappingHandlerMapping 匹配对应的request,选择符合条件的method handler。
创建自定义注解
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;
}
自定义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<ApiVersionCondition> {
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是否符合版本要求。
自定义匹配的处理器
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<ApiVersionCondition> 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);
}
}
配置注册自定义的RequestMappingHandlerMapping
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();
}
}
验证
@RequestMapping("api/{version}/user")
@RestController
public class UserV1Controller {
@GetMapping("/test")
public String test() {
return "version1";
}
@GetMapping("/extend")
public String extendTest() {
return "user v1 extend";
}
}
@RequestMapping("api/{version}/user")
@RestController
@ApiVersion(2)
public class UserV2Controller {
@GetMapping("/test")
public String test() {
return "user v2 test";
}
}
测试结果:
上图的前两个截图说明,请求正确的版本地址,会自动匹配版本的对应接口。当请求的版本大于当前版本时,默认匹配当前版本。
第三个截图说明,当请求对应的版本不存在接口时,会匹配之前版本的接口,即请求
/v2/user/extend
接口时,由于v2 控制器未实现该接口,所以自动匹配v1 版本中的接口。这就是所谓的版本继承。