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

相关推荐
Hello Dam13 分钟前
基于 Spring Boot 实现图片的服务器本地存储及前端回显
服务器·前端·spring boot
cnsxjean10 小时前
SpringBoot集成Minio实现上传凭证、分片上传、秒传和断点续传
java·前端·spring boot·分布式·后端·中间件·架构
《源码好优多》10 小时前
基于Java Springboot汽配销售管理系统
java·开发语言·spring boot
ℳ₯㎕ddzོꦿ࿐11 小时前
Spring Boot集成MyBatis-Plus:自定义拦截器实现动态表名切换
spring boot·后端·mybatis
尘浮生11 小时前
Java项目实战II基于Java+Spring Boot+MySQL的智能停车计费系统(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·mysql·微信小程序·maven
hai4058712 小时前
Spring Boot整合Redis Stack构建本地向量数据库相似性查询
数据库·spring boot·redis
years_GG12 小时前
【Git多人开发与协作之团队的环境搭建】
spring boot·深度学习·vue·github·团队开发·个人开发
小钟不想敲代码12 小时前
第4章 Spring Boot自动配置
java·spring boot·后端
代码小鑫13 小时前
A046-基于SpringBoot的论坛系统的设计与实现
java·开发语言·数据库·spring boot·毕业设计
我的运维人生14 小时前
Spring Boot应用开发实战:构建RESTful API服务
spring boot·后端·restful·运维开发·技术共享