文章目录
- Pre
- 设计思路
- Project
- [Starter Code](#Starter Code)
-
- [自定义注解 ApiVersion](#自定义注解 ApiVersion)
- 配置属性类用于管理API版本
- [自动配置基于Spring MVC的API版本控制](#自动配置基于Spring MVC的API版本控制)
- 实现WebMvcRegistrations接口,用于自定义WebMvc的注册逻辑
- 扩展RequestMappingHandlerMapping的类,支持API版本路由
- spring.factories
- [Test Code](#Test Code)
Pre
Spring Boot - 在Spring Boot中实现灵活的API版本控制(上)
设计思路
@ApiVersion
功能特性
-
支持类和方法上使用:
- 优先级:方法上的注解优先于类上的注解。
- 如果类和方法同时使用
@ApiVersion
,则以方法上的版本为准。
-
支持多版本同时生效:
@ApiVersion
的参数是数组,可以配置多个版本。例如:@ApiVersion({1, 2})
,此配置允许通过v1
或v2
访问。
-
可配置前缀和后缀:
- 默认前缀是
v
,可以通过配置项api-version.prefix
修改。 - 默认没有后缀,但可以通过
api-version.suffix
配置。
- 默认前缀是
-
使用简单:
- 仅需一个注解即可完成版本控制。
使用示例
假设你有一个 UserController
,需要支持 v1
和 v2
的版本访问:
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
方法在 v1
和 v2
版本都可访问,而 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"));
}
}