为什么接口要使用多个版本

一般来说,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(请求路径条件判断) 等,如下图所示:

CleanShot 2024-04-15 at 17.50.56@2x

RequestMappingInfo重写了compareTo方法,其中所有条件都符合,才是真正要找的RequestMappingHandlerMapping。

CleanShot 2024-04-15 at 17.54.51@2x

默认的RequestMappingHandlerMapping是没有版本号处理逻辑的,但是我们可以看getMatchingCondition方法中的最后一个条件如下:

RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);

是有提供一个自定义的条件判断器的,那么我们只需要继承RequestMappingHandlerMapping,并重写getCustomMethodCondition,在其中返回自定义的条件判断器(RequestCondition),并且在这里条件判断器里面处理版本号的逻辑就行了。

代码实现

定义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

定义请求的比较器,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

在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很简单,增加一个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类

@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());
    }

}

源码地址

https://github.com/flycati/spring-mvc-api-version