为什么接口要使用多个版本 {#为什么接口要使用多个版本}
一般来说,Restful API接口是提供给其它模块,系统或是其他公司使用,不能随意频繁的变更。然而,需求和业务不断变化,接口和参数也会发生相应的变化。如果直接对原来的接口进行修改,势必会影响线其他系统的正常运行。这就必须对api 接口进行有效的版本控制。
有哪些实现多版本的思路 {#有哪些实现多版本的思路}
- 不同的版本使用不同的Controller
比如,UserController,通过创建UserControllerV1和UserControllerV2两个类来控制版本,类级别的@RequestMapping("/api/v1/user")分别为@RequestMapping("/api/v2/user") - 使用一个UserController,类级别@RequestMapping("")路径参数为空,路径全写在方法级别的@RequestMapping中,比如@GetMapping("/api/v2/user")
- 使用一个UserController,类级别@RequestMapping("/api/user")路径参数为统一的/api/user,在方法中添加版本信息,比如/api/user/getUserV1,/api/user/getUserV2
但是以上的实现方式都不够优雅,我希望接口都放在一个Controller中,并通过一个注解来指定版本号,并且可以从请求路径、请求头、请求参数中任意一个地方解析版本号,并且不影响路径的命名。
比如:
- 请求路径解析版本号:/api/v1.0/user
- 请求头解析版本号 :/api/user
请求头参数: x-api-version: 1.0 - 请求参数解析版本号:/api/user?version=1.0
需求解析 {#需求解析}
1、通过注解指定接口版本,可以定义在类上,也可以定义在方法上
2、版本号信息支持放在请求头、请求路径、请求参数中任意一个地方
3、当使用请求头、请求参数方式时,如果不同版本的接口签名相同时不能报错
4、封装为starter,开箱即用
实现思路 {#实现思路}
我们知道RequestMappingHandlerMapping是SpringMVC中核心的组件。
简单理解的话,一个RequestMappingHandlerMapping对应我们自己定义的一个@GetMapping、@PostMapping等,而实际执行的方法被封装为HandlerMethod,在SpringMVC中,保存了一个类似Map<RequestMappingHandlerMapping, HandlerMethod>的结构,通过请求过来的url找到对应的RequestMappingHandlerMapping,那么就能找到对应需要执行的方法HandlerMethod了。
那么如果通过url找到对应的RequestMappingHandlerMapping呢?
通过RequestMappingHandlerMapping可以获取RequestMappingInfo信息,其中包装了各种条件判断器,比如RequestMethodsRequestCondition(请求方法条件判断) 、PatternsRequestCondition(请求路径条件判断) 等,如下图所示:
RequestMappingInfo重写了compareTo方法,其中所有条件都符合,才是真正要找的RequestMappingHandlerMapping。
默认的RequestMappingHandlerMapping是没有版本号处理逻辑的,但是我们可以看getMatchingCondition方法中的最后一个条件如下:
RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
是有提供一个自定义的条件判断器的,那么我们只需要继承RequestMappingHandlerMapping,并重写getCustomMethodCondition,在其中返回自定义的条件判断器(RequestCondition),并且在这里条件判断器里面处理版本号的逻辑就行了。
代码实现 {#代码实现}
定义ApiVersion注解 {#定义ApiVersion注解}
定义@ApiVersion注解,Target为ElementType.METHOD和ElementType.TYPE,即可以作用于类和方法上,String version()用于填入版本值,格式为String类型。
package tech.flycat.apiverson;
import java.lang.annotation.\*;
`@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ApiVersion {
/**
* 版本号
*/
String version();
}
`
定义版本号解析器 {#定义版本号解析器}
为了提高组件的扩展性,可以定义一个版本号解析器接口,满足自定义版本号解析的需求。
组件提供两种实现方式:请求路径版本号和请求头版本号,请求参数版本号解析可以自己实现。
/**
* 版本号解析器
*/
public interface ApiVersionParser {
/**
* 解析请求版本号
*
* @param request
* @return 匹配到的版本号列表
*/
String parseVersion(HttpServletRequest request);
/**
* 校验版本号格式是否正确
* @param versionText
* @return
*/
boolean validateVersionFormat(String versionText);
/**
* 比较两个版本号大小
* @param version1
* @param version2
* @return
*/
int compareVersion(String version1, String version2);
`}
`
以下是请求路径版本号解析器的实现(组件默认解析器)。
版本号支持三种形式:
- v1.0.0
- v1.0
- v1
版本号信息在请求路径中,如以下为业务相同版本不同的两个接口:
localhost:8080/api/v1.0.1/user
localhost:8080/api/v1.0.2/user
/**
* 版本传输位置:请求路径{version},比如:/api/{version}/user
* 版本格式:v1或v1.0或v1.0.0
*/
public class RequestPathVersionParser implements ApiVersionParser {
private final Pattern VERSION_PATTERN = Pattern.compile("/v\\d+(\\.\\d+){0,2}");
@Override
public String parseVersion(HttpServletRequest request) {
String requestURI = request.getRequestURI();
Matcher matcher = VERSION_PATTERN.matcher(requestURI);
if (!matcher.find()) {
return null;
}
return matcher.group(0).replace("/v", "");
}
@Override
public boolean validateVersionFormat(String versionText) {
return true;
}
@Override
public int compareVersion(String version1, String version2) {
return version1.compareTo(version2);
}
`}
`
定义RequestCondition {#定义RequestCondition}
定义请求的比较器,ApiVersionRequestCondition的matchApiVersion保存了接口/类注解的版本信息,在getMatchingCondition方法中,通过版本解析器ApiVersionParser 获取请求的版本信息,并对版本格式进行校验,通过后根据compareVersion方法判断版本是否一致,如果一致则表示版本匹配,否则返回null,表示不匹配。
/**
* url处理器适配条件-apiVersion相关
**/
public class ApiVersionRequestCondition extends AbstractRequestCondition<ApiVersionRequestCondition> {
/**
* 当前注解匹配的版本
*/
private final String matchApiVersion;
private final ApiVersionParser apiVersionParser;
public ApiVersionRequestCondition(String matchApiVersion,
ApiVersionParser apiVersionParser) {
this.matchApiVersion = matchApiVersion;
this.apiVersionParser = apiVersionParser;
}
@Override
protected Collection<?> getContent() {
return Stream.of(this.matchApiVersion).collect(Collectors.toList());
}
@Override
protected String getToStringInfix() {
return "||";
}
@Override
public ApiVersionRequestCondition combine(ApiVersionRequestCondition other) {
return new ApiVersionRequestCondition(other.matchApiVersion, apiVersionParser);
}
@Override
@Nullable
public ApiVersionRequestCondition getMatchingCondition(HttpServletRequest request) {
if (matchApiVersion == null || matchApiVersion.trim().isEmpty()) {
return this;
}
String requestVersion = apiVersionParser.parseVersion(request);
if (requestVersion == null || requestVersion.trim().isEmpty()) {
return null;
}
boolean legalFormat = apiVersionParser.validateVersionFormat(requestVersion);
if (!legalFormat) {
throw new IllegalVersionException("接口版本号[" + requestVersion + "]格式不正确");
}
if (apiVersionParser.compareVersion(requestVersion, matchApiVersion) != 0) {
return null;
}
return this;
}
@Override
public int compareTo(ApiVersionRequestCondition other, HttpServletRequest request) {
return apiVersionParser.compareVersion(this.matchApiVersion, other.matchApiVersion);
}
`}
`
定义RequestMappingHandlerMapping {#定义RequestMappingHandlerMapping}
在getCustomMethodCondition方法中,通过AnnotationUtils工具类获取到方法/类的版本信息,然后创建一个自定义的RequestCondition。
public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
private final ApiVersionParser apiVersionParser;
public ApiVersionRequestMappingHandlerMapping(ApiVersionParser apiVersionParser) {
super();
this.apiVersionParser = apiVersionParser;
}
@Override
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
ApiVersion typeAnnotation = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
return createCondition(typeAnnotation);
}
@Override
protected RequestCondition<?> getCustomMethodCondition(Method method) {
ApiVersion methodAnnotation = AnnotationUtils.findAnnotation(method, ApiVersion.class);
return createCondition(methodAnnotation);
}
@Override
protected boolean isHandler(Class<?> beanType) {
return super.isHandler(beanType);
}
/**
* 创建关于apiVersion的条件
* @param apiVersion
* @return
*/
private RequestCondition<?> createCondition(ApiVersion apiVersion) {
String version = apiVersion == null ? null : apiVersion.version();
return new ApiVersionRequestCondition(version, apiVersionParser);
}
`}
`
配置注册 {#配置注册}
这里没有添加@Configuration注解,是因为后面要通过starter进行封装。
public class WebConfiguration implements WebMvcRegistrations {
private final ApiVersionParser apiVersionParser;
public WebConfiguration(ApiVersionParser apiVersionParser) {
this.apiVersionParser = apiVersionParser;
}
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new ApiVersionRequestMappingHandlerMapping(apiVersionParser);
}
`}
`
封装starter {#封装starter}
封装starter很简单,增加一个XxxAutoConfiguration的类,然后在/resources/META-INF目录下增加spring.factories,利用Spring的SPI机制将配置注册到Spring容器中。
定义ApiVersionAutoConfiguration
默认ApiVersionParser是RequestPathVersionParser(请求路径版本解析器)
package tech.flycat.apiverson;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tech.flycat.apiverson.parser.RequestPathVersionParser;
/\*\*
`
`
*
@author <a href="mailto:zengbin@hltn.com">zengbin</a>
* `
`@since 2024/3/7
*/
@Configuration
public class ApiVersionAutoConfiguration {
`
`
@Bean
public WebConfiguration webConfiguration(@Autowired ApiVersionParser apiVersionParser) {
return new WebConfiguration(apiVersionParser);
}
`
`
`@Bean
@ConditionalOnMissingBean(ApiVersionParser.class)
public ApiVersionParser apiVersionParser() {
return new RequestPathVersionParser();
}
}
`
添加spring.factories文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
tech.flycat.apiverson.ApiVersionAutoConfiguration
测试 {#测试}
引入依赖 {#引入依赖}
!暂时没有推maven公开仓库,需要将源码导入到项目中,或者推maven私库
<dependency>
<groupId>tech.flycat</groupId>
<artifactId>spring-mvc-api-version-starter</artifactId>
<version>1.0</version>
</dependency>
定义Controller类 {#定义Controller类}
@RestController
@RequestMapping("api/{version}/test")
public class RequestPathVersionTestController {
@ApiVersion(version = "1")
@GetMapping("oneLevelVersion")
public String oneLevelVersion1(@PathVariable("version") String version) {
return "api-version: " + version;
}
@ApiVersion(version = "2")
@GetMapping("oneLevelVersion")
public String oneLevelVersion2(@PathVariable("version") String version) {
return "api-version: " + version;
}
@ApiVersion(version = "1.0")
@GetMapping("twoLevelVersion")
public String twoLevelVersion1(@PathVariable("version") String version) {
return "api-version: " + version;
}
@ApiVersion(version = "1.1")
@GetMapping("twoLevelVersion")
public String twoLevelVersion2(@PathVariable("version") String version) {
return "api-version: " + version;
}
@ApiVersion(version = "1.0.0")
@GetMapping("threeLevelVersion")
public String threeLevelVersion1(@PathVariable("version") String version) {
return "api-version: " + version;
}
@ApiVersion(version = "1.1.1")
@GetMapping("threeLevelVersion")
public String threeLevelVersion2(@PathVariable("version") String version) {
return "api-version: " + version;
}
`}
`
单元测试类 {#单元测试类}
package test.flycat.apiversion.test;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import tech.flycat.apiverson.test.Application;
import test.flycat.apiversion.test.config.RequestPathVersionParserConfiguration;
/\*\*
*
@author \<a href="mailto:zengbin@hltn.com"\>zengbin\</a\>
*
@since 2024/4/15
\*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
// 配置请求路径版本号解析器
@Import(RequestPathVersionParserConfiguration.class)
public class RequestPathVersionTest {
private MockMvc mockMvc;
@Autowired
private WebApplicationContext context;
@Before
public void setup() {
//初始化mockMvc对象
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
}
@Test
public void oneLevelVersion1_thenReturn200_Test() throws Exception {
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders
.get("/api/v1/test/oneLevelVersion");
mockMvc.perform(requestBuilder)
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print());
}
@Test
public void oneLevelVersion2_thenReturn200_Test() throws Exception {
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders
.get("/api/v2/test/oneLevelVersion");
mockMvc.perform(requestBuilder)
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print());
}
@Test
public void oneLevelVersion3_thenReturn404_Test() throws Exception {
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders
.get("/api/v3/test/oneLevelVersion");
mockMvc.perform(requestBuilder)
.andExpect(MockMvcResultMatchers.status().isNotFound())
.andDo(MockMvcResultHandlers.print());
}
@Test
public void oneLevelVersion4_thenReturn404_Test() throws Exception {
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders
.get("/api/v1.1/test/oneLevelVersion");
mockMvc.perform(requestBuilder)
.andExpect(MockMvcResultMatchers.status().isNotFound())
.andDo(MockMvcResultHandlers.print());
}
@Test
public void twoLevelVersion1_thenReturn200_Test() throws Exception {
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders
.get("/api/v1.1/test/twoLevelVersion");
mockMvc.perform(requestBuilder)
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print());
}
@Test
public void twoLevelVersion2_thenReturn404_Test() throws Exception {
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders
.get("/api/v1.2/test/twoLevelVersion");
mockMvc.perform(requestBuilder)
.andExpect(MockMvcResultMatchers.status().isNotFound())
.andDo(MockMvcResultHandlers.print());
}
@Test
public void treeLevelVersion1_thenReturn200_Test() throws Exception {
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders
.get("/api/v1.0.0/test/threeLevelVersion");
mockMvc.perform(requestBuilder)
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print());
}
`}
`