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 的版本访问:

java 复制代码
@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 版本中有效
    }
}

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

配置示例

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

properties 复制代码
api-version.prefix=v
api-version.suffix=-api

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

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


Project


Starter Code

自定义注解 ApiVersion

java 复制代码
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();
}

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

java 复制代码
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;
    }

}

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

java 复制代码
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);
    }
}

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

java 复制代码
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());
    }

}

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

java 复制代码
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);
    }

}

spring.factories

bash 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.github.artisan.ApiVersionAutoConfiguration

Test Code

无版本控制

java 复制代码
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 "不使用版本注解";
    }
}

多版本控制

java 复制代码
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 "注解支持多版本";
    }
}

v1

java 复制代码
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 "方法有注解, 使用方法注解";
    }

}

v2

java 复制代码
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 方法有注解使用方法注解";
    }

}

Test

整个swagger吧

java 复制代码
 

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"));
    }

}

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

相关推荐
懒虫虫~23 分钟前
基于SpringBoot解决RabbitMQ消息丢失问题
spring boot·rabbitmq
java干货1 小时前
深度解析:Spring Boot 配置加载顺序、优先级与 bootstrap 上下文
前端·spring boot·bootstrap
sclibingqing2 小时前
SpringBoot项目接口集中测试方法及实现
java·spring boot·后端
KK溜了溜了4 小时前
JAVA-springboot log日志
java·spring boot·logback
我命由我123455 小时前
Spring Boot 项目集成 Redis 问题:RedisTemplate 多余空格问题
java·开发语言·spring boot·redis·后端·java-ee·intellij-idea
面朝大海,春不暖,花不开5 小时前
Spring Boot消息系统开发指南
java·spring boot·后端
hshpy5 小时前
setting up Activiti BPMN Workflow Engine with Spring Boot
数据库·spring boot·后端
jay神6 小时前
基于Springboot的宠物领养系统
java·spring boot·后端·宠物·软件设计与开发
不知几秋6 小时前
Spring Boot
java·前端·spring boot
howard20057 小时前
5.4.2 Spring Boot整合Redis
spring boot·整合redis