51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

SpringBoot多版本接口实现

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

一般来说,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注解}

定义@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&lt;?&gt; 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&lt;?&gt; getCustomTypeCondition(Class&lt;?&gt; handlerType) {
        ApiVersion typeAnnotation = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
        return createCondition(typeAnnotation);
    }

    @Override
    protected RequestCondition&lt;?&gt; getCustomMethodCondition(Method method) {
        ApiVersion methodAnnotation = AnnotationUtils.findAnnotation(method, ApiVersion.class);
        return createCondition(methodAnnotation);
    }

    @Override
    protected boolean isHandler(Class&lt;?&gt; beanType) {
        return super.isHandler(beanType);
    }

    /**
     * 创建关于apiVersion的条件
     * @param apiVersion
     * @return
     */
    private RequestCondition&lt;?&gt; 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());




  }




`}
`

源码地址 {#源码地址}

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

赞(0)
未经允许不得转载:工具盒子 » SpringBoot多版本接口实现