当前位置:   article > 正文

Spring Boot - 在Spring Boot中实现灵活的API版本控制(下)_ 封装场景启动器Starter

Spring Boot - 在Spring Boot中实现灵活的API版本控制(下)_ 封装场景启动器Starter

在这里插入图片描述

Pre

Spring Boot - 在Spring Boot中实现灵活的API版本控制(上)


设计思路

@ApiVersion 功能特性

  1. 支持类和方法上使用:

    • 优先级:方法上的注解优先于类上的注解。
    • 如果类和方法同时使用 @ApiVersion,则以方法上的版本为准。
  2. 支持多版本同时生效:

    • @ApiVersion 的参数是数组,可以配置多个版本。例如:@ApiVersion({1, 2}),此配置允许通过 v1v2 访问。
  3. 可配置前缀和后缀:

    • 默认前缀是 v,可以通过配置项 api-version.prefix 修改。
    • 默认没有后缀,但可以通过 api-version.suffix 配置。
  4. 使用简单:

    • 仅需一个注解即可完成版本控制。

使用示例

假设你有一个 UserController,需要支持 v1v2 的版本访问:

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping
    @ApiVersion({1, 2})
    public List<User> getUsers() {
        // 获取用户列表的实现
    }

    @GetMapping("/{id}")
    @ApiVersion(2)
    public User getUserV2(@PathVariable Long id) {
        // 获取用户详细信息的实现,仅在 v2 版本中有效
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

在这个示例中,getUsers 方法在 v1v2 版本都可访问,而 getUserV2 方法仅在 v2 版本可访问。

配置示例

application.properties 中配置版本前缀和后缀:

api-version.prefix=v
api-version.suffix=-api
  • 1
  • 2

这样,API 的 URL 可以是 /v1-api/users/v2-api/users

通过这种方式,@ApiVersion 注解简化了 API 版本控制的实现,提高了代码的可维护性和灵活性。


Project

在这里插入图片描述


Starter Code

自定义注解 ApiVersion

package com.github.artisan.annotation;

import org.springframework.web.bind.annotation.Mapping;

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;

/**
 * 接口版本标识注解
 * @author artisan
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
    /**
     * 指定API的版本号。
     * 此方法返回一个整型数组,数组中的每个元素代表一个API版本号。
     *
     * @return 代表API版本号的整数数组。
     */
    int[] value();
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

配置属性类用于管理API版本

package com.github.artisan;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * 配置属性类用于管理API版本。
 * 通过前缀 "api-version" 绑定配置属性,以方便管理API版本。
 * @author Artisan
 */

@ConfigurationProperties(prefix = "api-version")
public class ApiVersionProperties {

    /**
     * API版本的前缀,用于定义版本的起始部分。
     */
    private String prefix;

    /**
     * 获取API版本的前缀。
     *
     * @return 返回API版本的前缀。
     */
    public String getPrefix() {
        return prefix;
    }

    /**
     * 设置API版本的前缀。
     *
     * @param prefix 设置API版本的前缀。
     */
    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    /**
     * API版本的后缀,用于定义版本的结束部分。
     */
    private String suffix;

    /**
     * 获取API版本的后缀。
     *
     * @return 返回API版本的后缀。
     */
    public String getSuffix() {
        return suffix;
    }

    /**
     * 设置API版本的后缀。
     *
     * @param suffix 设置API版本的后缀。
     */
    public void setSuffix(String suffix) {
        this.suffix = suffix;
    }

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

自动配置基于Spring MVC的API版本控制

package com.github.artisan;

import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * ApiVersionAutoConfiguration类用于自动配置基于Spring MVC的API版本控制
 * 该类通过@EnableConfigurationProperties注解激活ApiVersionProperties配置类
 * 并且通过@Bean注解的方法创建和管理ApiVersionWebMvcRegistrations的单例对象
 * @author Artisan
 */
@ConditionalOnWebApplication
@Configuration
@EnableConfigurationProperties(ApiVersionProperties.class)
public class ApiVersionAutoConfiguration {

    /**
     * 通过@Bean注解声明此方法将返回一个单例对象,由Spring容器管理
     * 该方法的目的是根据ApiVersionProperties配置生成ApiVersionWebMvcRegistrations实例
     * 这对于自动配置基于Spring MVC的API版本控制至关重要
     *
     * @param apiVersionProperties 一个包含API版本控制相关配置的实体类
     *                             该参数用于初始化ApiVersionWebMvcRegistrations对象
     * @return 返回一个ApiVersionWebMvcRegistrations对象,用于注册和管理API版本控制相关的设置
     */
    @Bean
    public ApiVersionWebMvcRegistrations apiVersionWebMvcRegistrations(ApiVersionProperties apiVersionProperties) {
        return new ApiVersionWebMvcRegistrations(apiVersionProperties);
    }
}


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

实现WebMvcRegistrations接口,用于自定义WebMvc的注册逻辑

package com.github.artisan;

import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

/**
 * 实现WebMvcRegistrations接口,用于自定义WebMvc的注册逻辑
 * 主要用于API版本的请求映射配置
 *
 *  @author Artisan
 */
public class ApiVersionWebMvcRegistrations implements WebMvcRegistrations {

    /**
     * API版本配置属性
     * 用于获取API版本的前缀和后缀配置
     */
    private ApiVersionProperties apiVersionProperties;

    /**
     * 构造函数,初始化API版本配置属性
     *
     * @param apiVersionProperties API版本配置属性对象
     */
    public ApiVersionWebMvcRegistrations(ApiVersionProperties apiVersionProperties) {
        this.apiVersionProperties = apiVersionProperties;
    }

    /**
     * 获取请求映射处理器映射对象
     * 此方法用于配置API版本的请求映射处理逻辑
     * 它根据配置决定映射路径的前缀和后缀
     *
     * @return 返回一个初始化好的RequestMappingHandlerMapping对象,用于处理API版本的请求映射
     */
    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        // 根据API版本配置的前缀情况决定使用默认前缀"v"还是用户配置的前缀
        // 如果未配置前缀,则默认使用"v",否则使用配置的前缀
        // 后缀直接使用配置的值
        return new ApiVersionRequestMappingHandlerMapping(StringUtils.isEmpty(apiVersionProperties.getPrefix()) ?
                "v" : apiVersionProperties.getPrefix(), apiVersionProperties.getSuffix());
    }

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

扩展RequestMappingHandlerMapping的类,支持API版本路由

package com.github.artisan;

import com.github.artisan.annotation.ApiVersion;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition;
import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.validation.constraints.NotNull;
import java.lang.reflect.Method;

/**
 * 一个扩展了RequestMappingHandlerMapping的类,支持API版本路由。
 * 它允许方法或类通过ApiVersion注解来支持版本控制。
 * @author Artisan
 */
public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    /**
     * API版本在URL中的前缀
     */
    private final String prefix;
    /**
     * API版本在URL中的后缀,默认为空字符串,如果未提供则为空字符串
     */
    private final String suffix;

    /**
     * 构造函数用于初始化API版本的前缀和后缀。
     *
     * @param prefix API版本在URL中的前缀
     * @param suffix API版本在URL中的后缀,如果没有提供则默认为空字符串
     */
    public ApiVersionRequestMappingHandlerMapping(String prefix, String suffix) {
        this.prefix = prefix;
        this.suffix = StringUtils.isEmpty(suffix) ? "" : suffix;
    }

    /**
     * 覆盖此方法以获取方法的路由信息,并支持基于ApiVersion注解的自定义条件。
     *
     * @param method 需要获取路由信息的方法
     * @param handlerType 处理器类型
     * @return 方法的路由信息,包括基于API版本的自定义条件
     */
    @Override
    protected RequestMappingInfo getMappingForMethod(Method method, @NotNull Class<?> handlerType) {
        // 获取基本的路由信息
        RequestMappingInfo info = super.getMappingForMethod(method, handlerType);
        if (info == null) {
            return null;
        }

        // 检查方法是否使用了ApiVersion注解
        ApiVersion methodAnnotation = AnnotationUtils.findAnnotation(method, ApiVersion.class);
        if (methodAnnotation != null) {
            // 获取自定义方法条件
            RequestCondition<?> methodCondition = getCustomMethodCondition(method);
            // 创建基于API版本的信息并合并到基本信息中
            info = createApiVersionInfo(methodAnnotation, methodCondition).combine(info);
        } else {
            // 如果方法没有使用ApiVersion注解,则检查类是否使用了该注解
            ApiVersion typeAnnotation = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
            if (typeAnnotation != null) {
                // 获取自定义类条件
                RequestCondition<?> typeCondition = getCustomTypeCondition(handlerType);
                // 创建基于API版本的信息并合并到基本信息中
                info = createApiVersionInfo(typeAnnotation, typeCondition).combine(info);
            }
        }

        return info;
    }

    /**
     * 根据ApiVersion注解创建路由信息。
     *
     * 该方法解析ApiVersion注解的值,并根据这些值构建URL模式,
     * 然后结合自定义条件创建RequestMappingInfo对象,用于支持版本控制。
     *
     * @param annotation ApiVersion注解实例,包含API版本信息。
     * @param customCondition 自定义条件,用于进一步细化请求映射。
     * @return 基于API版本的路由信息,用于将请求映射到特定版本的API处理方法上。
     */
    private RequestMappingInfo createApiVersionInfo(ApiVersion annotation, RequestCondition<?> customCondition) {
        // 获取注解中指定的API版本数组
        int[] values = annotation.value();
        // 为每个API版本创建对应的URL模式
        String[] patterns = new String[values.length];
        for (int i = 0; i < values.length; i++) {
            // 构建URL前缀
            patterns[i] = prefix + values[i] + suffix;
        }

        // 使用构建的URL模式和其他请求条件创建并返回RequestMappingInfo对象
        return new RequestMappingInfo(
                new PatternsRequestCondition(patterns, getUrlPathHelper(), getPathMatcher(),
                        useSuffixPatternMatch(), useTrailingSlashMatch(), getFileExtensions()),
                new RequestMethodsRequestCondition(),
                new ParamsRequestCondition(),
                new HeadersRequestCondition(),
                new ConsumesRequestCondition(),
                new ProducesRequestCondition(),
                customCondition);
    }

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115

spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.github.artisan.ApiVersionAutoConfiguration
  • 1
  • 2

Test Code

无版本控制

package com.github.artisan.web;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Artisan
 */
@RestController
public class NoVersionController {

    @GetMapping("foo")
    public String foo() {
        return "不使用版本注解";
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

多版本控制

package com.github.artisan.web;

import com.github.artisan.annotation.ApiVersion;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Artisan
 */
@RestController
public class MultiVersionController {

    @GetMapping("foo3")
    @ApiVersion({1, 2})
    public String foo3() {
        return "注解支持多版本";
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

v1

package com.github.artisan.web.v1;

import com.github.artisan.annotation.ApiVersion;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Artisan
 */
@ApiVersion(1)
@RestController
public class TestController {

    @GetMapping("foo1")
    public String foo1() {
        return "方法没有注解, 使用类注解";
    }

    @GetMapping("foo2")
    @ApiVersion(1)
    public String foo2() {
        return "方法有注解, 使用方法注解";
    }

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

v2

package com.github.artisan.web.v2;

import com.github.artisan.annotation.ApiVersion;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Artisan
 */
@ApiVersion(2)
@RestController
public class TestController {

    @GetMapping("foo1")
    public String foo1() {
        return "方法没有注解, 使用类注解";
    }

    @GetMapping("foo2")
    @ApiVersion(2)
    public String foo2() {
        return "方法有注解, 使用方法注解";
    }

    @GetMapping("foo4")
    @ApiVersion(1)
    public String foo4() {
        return "xxxx 方法有注解使用方法注解";
    }

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

Test

整个swagger吧

 

package com.github.artisan.swagger;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;


@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket swaggerAll() {
        Docket docket = new Docket(DocumentationType.SWAGGER_2);
        return docket.apiInfo(apiInfo("all"))
                .groupName("all")
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.github.artisan.web"))
                .paths(PathSelectors.any())
                .build()
                .enable(true);
    }

    private ApiInfo apiInfo(String version) {
        return new ApiInfoBuilder()
                .title("api-version-test doc")
                .description("api-version-test")
                .termsOfServiceUrl("")
                .version(version)
                .build();
    }

    @Bean
    public Docket swaggerV1() {
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("v1")
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.github.artisan.web"))
                .paths(PathSelectors.regex("/v1.*"))
                .build()
                .apiInfo(apiInfo("v1"));
    }

    @Bean
    public Docket swaggerV2() {
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("v2")
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.github.artisan.web"))
                .paths(PathSelectors.regex("/v2.*"))
                .build()
                .apiInfo(apiInfo("v2"));
    }

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64

访问: http://localhost:9090/swagger-ui.html

在这里插入图片描述

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/神奇cpp/article/detail/984796
推荐阅读
相关标签
  

闽ICP备14008679号