文章目录
介绍
-
每次版本升级都强制用户更新升级,这种方式每次用户打开APP都要更新,非常影响用户的体验。然后这种方式会在旧系统过渡到新系统过程中让旧系统无法使用。并且这也会与业务冲突,很多场景业务就有需要新老系统并行的要求。
-
软件有一个不断升级迭代的过程,而在升级中我们业务需求可能不断在更新,所以我们需要对以前的API接口不断的更新以满足变化的需求,但是在更新升级的过程中我们势必又要保证原来功能的可用性,所以我们就必须要对同一个API接口进行多版本的管理。
-
首先在代码里面维护多个版本的API接口,然后在请求的URL、或者请求header里面带上一个版本参数,然后我们根据这个版本参数来区分调用对应版本的代码。
-
通过保存多个版本的接口,然后在URL标识对应的版本号来调用对应的接口,这种方式解决了多版本管理的问题,但使用这种方式对于前端工程师来说也比较麻烦,必须在每个请求的URL上标记一个版本号来使用对应的版本的API,看着眼花缭乱的URL显然不太优雅,所以我们会对这种方式进行一下优化。
-
通过注解来标识不同接口的版本。
-
当一个请求进来,我们可以根据URL+apiversion 来执行对应版本的Controller方法。
依赖maven
- 引用公共模块
<dependency>
<groupId>com.edt</groupId>
<artifactId>edt-shop-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
架构演示
- 在config模块中 配置api版本管理, 其他微服务只需要引用该模块依赖即可
apiversion包
ApiVersion自定义注解(贴在接口方法上)
- 一个是版本号的值,一个是版本管理的方式(uri接口前缀拼接版本号,传参带版本号,请求头带版本号)
/**
* 标示当前请求版本
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
/**
* 版本号
*/
String value();
ApiVersionProperties.Type type() default ApiVersionProperties.Type.URI;
}
- 该演示的实际请求接口就是
ip地址:网关端口 /该微服务路由名称/v1/base/xxx接口
- 配置做了处理 有相同接口名不会报错 内部会将相同接口名通过版本号拼接成不同接口名
- api版本管理有3种方式区分, 一种是uri地址栏接口,一种是传参,一种是请求头带版本号(配置上自定义:接口前缀拼接/传参属性/请求头key属性)
- 根据
type
来区分方式,默认使用uri地址栏接口带版本号方式 - 传参方式:参数
api_version
,值为一个版本号数字 - 请求头方式:请求头的key:
X-API-VERSION
,值为一个版本号数字
EnableApiVersioning自定义注解(贴在微服务启动类上)
- 在引用该模块的微服务的启动类上 贴上该注解 即可开启api版本管理
- 使
ApiVersionAutoConfiguration
类注入spring容器 其相关配置生效
/**
* 开启多版本API控制
*
* @see ApiVersionProperties 配置属性
* @see ApiVersionAutoConfiguration 配置类
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ApiVersionAutoConfiguration.class)
public @interface EnableApiVersioning {
}
ApiVersionAutoConfiguration.class
- 该类是不注入spring容器的,通过自定义启动注解 使该类注入spring容器
- 使
ApiVersionProperties
类注入spring容器管理
/**
* 配置api版本
*/
@EnableConfigurationProperties(ApiVersionProperties.class)
public class ApiVersionAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ApiVersionWebMvcRegistrations apiVersionWebMvcRegistrations(ApiVersionProperties apiVersionProperties) {
return new ApiVersionWebMvcRegistrations(apiVersionProperties);
}
}
ApiVersionProperties.class
/**
* Api-Version配置
*/
@Data
@ConfigurationProperties(prefix = "api.version")
public class ApiVersionProperties implements Serializable {
/**
* 实现多版本的方式
*/
private Type type = Type.URI;
/**
* URI地址前缀, 例如: /api
*/
private String uriPrefix;
/**
* URI的位置
*/
private UriLocation uriLocation = UriLocation.BEGIN;
/**
* 版本请求头名
*/
private String header = "X-API-VERSION";
/**
* 版本请求参数名
*/
private String param = "api_version";
public enum Type {
/**
* URI路径
*/
URI,
/**
* 请求头
*/
HEADER,
/**
* 请求参数
*/
PARAM;
}
public enum UriLocation {
BEGIN, END
}
}
ApiVersionWebMvcRegistrations.class
- 重写mvc的方法
@AllArgsConstructor
public class ApiVersionWebMvcRegistrations implements WebMvcRegistrations {
@NonNull
private ApiVersionProperties apiVersionProperties;
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new VersionedRequestMappingHandlerMapping(apiVersionProperties);
}
}
ApiVersionRequestCondition.class
@Getter
public class ApiVersionRequestCondition implements RequestCondition<ApiVersionRequestCondition> {
private final String apiVersion;
private final ApiVersionProperties apiVersionProperties;
public ApiVersionRequestCondition(@NonNull String apiVersion, @NonNull ApiVersionProperties apiVersionProperties) {
this.apiVersion = apiVersion.trim();
this.apiVersionProperties = apiVersionProperties;
}
@Override
public ApiVersionRequestCondition combine(ApiVersionRequestCondition other) {
// method annotation first
return new ApiVersionRequestCondition(other.getApiVersion(), other.getApiVersionProperties());
}
@Override
public int compareTo(ApiVersionRequestCondition other, HttpServletRequest request) {
return other.getApiVersion().compareTo(getApiVersion());
}
@Override
public ApiVersionRequestCondition getMatchingCondition(HttpServletRequest request) {
ApiVersionProperties.Type type = apiVersionProperties.getType();
String version = null;
switch (type) {
case HEADER:
version = request.getHeader(apiVersionProperties.getHeader());
break;
case PARAM:
version = request.getParameter(apiVersionProperties.getParam());
break;
}
boolean match = version != null && version.length() > 0 && version.trim().equals(apiVersion);
if (match) {
return this;
}
return null;
}
@Override
public String toString() {
return "@ApiVersion(" + apiVersion + ")";
}
}
InnerUtils.class
- 配合配置处理的工具类
class InnerUtils {
private final static Pattern VERSION_NUMBER_PATTERN = Pattern.compile("^\\d+(\\.\\d+){0,2}$");
/**
* 检查版本匹配是否复合(最大三个版本)
*/
public static void checkVersionNumber(String version, Object targetMethodOrType) {
if (!matchVersionNumber(version)) {
throw new IllegalArgumentException(String.format("Invalid version number: @ApiVersion(\"%s\") at %s", version, targetMethodOrType));
}
}
/**
* 判断是否满足最大3个版本号的匹配
*/
public static boolean matchVersionNumber(String version) {
return version.length() != 0 && VERSION_NUMBER_PATTERN.matcher(version).find();
}
}
VersionedRequestMappingHandlerMapping.class(业务核心)
- 以上配置的最终处理的业务逻辑
@Slf4j
@AllArgsConstructor
public class VersionedRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
/**
* 多版本配置属性
*/
private ApiVersionProperties apiVersionProperties;
@Override
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
return createRequestCondition(handlerType);
}
@Override
protected RequestCondition<?> getCustomMethodCondition(Method method) {
return createRequestCondition(method);
}
private RequestCondition<ApiVersionRequestCondition> createRequestCondition(AnnotatedElement target) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(target, ApiVersion.class);
if (apiVersion == null) {
return null;
}
apiVersionProperties.setType(apiVersion.type());
if (apiVersionProperties.getType() == ApiVersionProperties.Type.URI) {
return null;
}
String version = apiVersion.value().trim();
InnerUtils.checkVersionNumber(version, target);
return new ApiVersionRequestCondition(version, apiVersionProperties);
}
//--------------------- 动态注册URI -----------------------//
@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMappingInfo info = this.createRequestMappingInfo(method);
if (info != null) {
RequestMappingInfo typeInfo = this.createRequestMappingInfo(handlerType);
if (typeInfo != null) {
info = typeInfo.combine(info);
}
ApiVersion apiVersion = AnnotationUtils.getAnnotation(method, ApiVersion.class);
if (apiVersion == null) {
apiVersion = AnnotationUtils.getAnnotation(handlerType, ApiVersion.class);
}
if (apiVersion != null) {
apiVersionProperties.setType(apiVersion.type());
}
// 指定URL前缀
if (apiVersionProperties.getType() == ApiVersionProperties.Type.URI) {
if (apiVersion != null) {
String version = apiVersion.value().trim();
InnerUtils.checkVersionNumber(version, method);
String prefix = "/v" + version;
if (apiVersionProperties.getUriLocation() == ApiVersionProperties.UriLocation.END) {
info = info.combine(RequestMappingInfo.paths(new String[]{prefix}).build());
} else {
if (!StringUtils.isEmpty(apiVersionProperties.getUriPrefix())) {
prefix = apiVersionProperties.getUriPrefix().trim() + prefix;
}
info = RequestMappingInfo.paths(new String[]{prefix}).build().combine(info);
}
}
}
}
return info;
}
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
RequestCondition<?> condition = element instanceof Class ? this.getCustomTypeCondition((Class) element) : this.getCustomMethodCondition((Method) element);
return requestMapping != null ? this.createRequestMappingInfo(requestMapping, condition) : null;
}
}