SpringBoot 接口版本控制
一个系统在上线后会不断迭代更新,需求也会不断变化,有可能接口的参数也会发生变化,如果在原有的参数上直接修改,可能会影响到现有项目的正常运行,这时我们就需要设置不同的版本,这样即使参数发生变化,由于老版本没有变化,因此不会影响上线系统的运行。
这里我们选择使用带有一位小数的浮点数作为版本号,在请求地址末尾中带上版本号,大致的地址如:http://api/test/1.0,其中,1.0即代表的是版本号。具体做法请看代码
自定义一个版本号的注解接口ApiVersion.java
import org.springframework.web.bind.annotation.Mapping; import java.lang.annotation.*; /** * 版本控制 * @author Zac */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Mapping public @interface ApiVersion { /** * 标识版本号 * @return */ double value(); }
版本号筛选器ApiVersionCondition
import org.springframework.web.servlet.mvc.condition.RequestCondition; import javax.servlet.http.HttpServletRequest; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 版本号匹配筛选器 * @author Zac */ public class ApiVersionCondition implements RequestCondition{ /** * 路径中版本的正则表达式匹配, 这里用 /1.0的形式 */ private static final Pattern VERSION_PREFIX_PATTERN = Pattern.compile("^\\S+/([1-9][.][0-9])$"); private double apiVersion; public ApiVersionCondition(double apiVersion) { this.apiVersion = apiVersion; } @Override public ApiVersionCondition combine(ApiVersionCondition other) { // 采用最后定义优先原则,则方法上的定义覆盖类上面的定义 return new ApiVersionCondition(other.getApiVersion()); } @Override public ApiVersionCondition getMatchingCondition(HttpServletRequest request) { Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI()); if (m.find()) { Double version = Double.valueOf(m.group(1)); // 如果请求的版本号大于配置版本号, 则满足 if (version >= this.apiVersion) { return this; } } return null; } @Override public int compareTo(ApiVersionCondition other, HttpServletRequest request) { // 优先匹配最新的版本号 return Double.compare(other.getApiVersion(), this.apiVersion); } public double getApiVersion() { return apiVersion; } }
版本号匹配拦截器
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.web.servlet.mvc.condition.RequestCondition; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import java.lang.reflect.Method; /** * 版本号匹配拦截器 * @author Zac */ public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping { @Override protected RequestConditiongetCustomTypeCondition(Class> handlerType) { ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class); return createCondition(apiVersion); } @Override protected RequestCondition getCustomMethodCondition(Method method) { ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class); return createCondition(apiVersion); } private RequestCondition createCondition(ApiVersion apiVersion) { return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value()); } }
配置WebMvcRegistrationsConfig
import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.web.WebMvcRegistrationsAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; @SpringBootConfiguration public class WebMvcRegistrationsConfig extends WebMvcRegistrationsAdapter { @Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { return new CustomRequestMappingHandlerMapping(); } } controller层实现 /** * version * @return */ @GetMapping("/api/test/{version}") @ApiVersion(2.0) public String searchTargetImage() { return "Hello! Welcome to Version2"; } /** * 多目标类型搜索,关联图片查询接口 * * @param attribute * @return */ @GetMapping("/api/test/{version}") @ApiVersion(1.0) public AppResult searchTargetImage() { return "Hello! Welcome to Version1"; }
SpringBoot 2.x 接口多版本
准备将现有的接口加上版本管理,兼容以前的版本。网上一调研,发现有很多示例,但是还是存在以下两个问题。
1.大部分使用Integer作为版本号,但是通常的版本号形式为v1.0.0,
2.版本号携带在header中,对接调用不清晰。
针对以上两个问题,做如下改造。
1.自定义接口版本注解ApiVersion
后面条件映射使用equals匹配,此处是否将String变为String[]应对多个版本使用同一代码的问题。
package com.yugioh.api.common.core.version; import org.springframework.web.bind.annotation.Mapping; import java.lang.annotation.*; /** * 接口版本 * * @author lieber */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Mapping public @interface ApiVersion { String value() default "1.0.0"; }
2.请求映射条件ApiVersionCondition
package com.yugioh.api.common.core.version; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.web.servlet.mvc.condition.RequestCondition; import javax.servlet.http.HttpServletRequest; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 版本控制 * * @author lieber */ @AllArgsConstructor @Getter @Slf4j public class ApiVersionCondition implements RequestCondition{ private String version; private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile(".*v(\\d+(.\\d+){0,2}).*"); public final static String API_VERSION_CONDITION_NULL_KEY = "API_VERSION_CONDITION_NULL_KEY"; @Override public ApiVersionCondition combine(ApiVersionCondition other) { // 方法上的注解优于类上的注解 return new ApiVersionCondition(other.getVersion()); } @Override public ApiVersionCondition getMatchingCondition(HttpServletRequest request) { Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI()); if (m.find()) { String version = m.group(1); if (this.compareTo(version)) { return this; } } // 将错误放在request中,可以在错误页面明确提示,此处可重构为抛出运行时异常 request.setAttribute(API_VERSION_CONDITION_NULL_KEY, true); return null; } @Override public int compareTo(ApiVersionCondition other, HttpServletRequest request) { return this.compareTo(other.getVersion()) ? 1 : -1; } private boolean compareTo(String version) { return Objects.equals(version, this.version); } }
3.创建自定义匹配处理器ApiVersionRequestMappingHandlerMapping
网上大部分只重写了getCustomTypeCondition和getCustomMethodCondition方法。这里为了解决路由映射问题,重写getMappingForMethod方法,在路由中加入前缀{version},加入后路由变为/api/{version}/xxx
package com.yugioh.api.common.core.version; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.web.servlet.mvc.condition.RequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import java.lang.reflect.Method; /** * 自定义匹配处理器 * * @author lieber */ public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping { private final static String VERSION_PREFIX = "{version}"; @Override protected RequestCondition> getCustomTypeCondition(Class> handlerType) { ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class); return createCondition(apiVersion); } @Override protected RequestCondition> getCustomMethodCondition(Method method) { ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class); return createCondition(apiVersion); } @Override protected RequestMappingInfo getMappingForMethod(Method method, Class> handlerType) { RequestMappingInfo requestMappingInfo = super.getMappingForMethod(method, handlerType); if (requestMappingInfo == null) { return null; } return createCustomRequestMappingInfo(method, handlerType, requestMappingInfo); } private RequestMappingInfo createCustomRequestMappingInfo(Method method, Class> handlerType, RequestMappingInfo requestMappingInfo) { ApiVersion methodApi = AnnotatedElementUtils.findMergedAnnotation(method, ApiVersion.class); ApiVersion handlerApi = AnnotatedElementUtils.findMergedAnnotation(handlerType, ApiVersion.class); if (methodApi != null || handlerApi != null) { return RequestMappingInfo.paths(VERSION_PREFIX).options(this.config).build().combine(requestMappingInfo); } return requestMappingInfo; } private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration(); private RequestConditioncreateCondition(ApiVersion apiVersion) { return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value()); } }
4.使用ApiVersionConfig配置来决定是否开启多版本
package com.yugioh.api.common.core.version; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; /** * 版本管理器配置 * * @author lieber */ @Data @Configuration @ConfigurationProperties(prefix = "api.config.version", ignoreInvalidFields = true) public class ApiVersionConfig { /** * 是否开启 */ private boolean enable; }
5.配置WebMvcRegistrations,启用自定义路由Mapping
package com.yugioh.api.common.core.version; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; /** * @author lieber */ @Configuration @Order(Ordered.HIGHEST_PRECEDENCE) public class ApiWebMvcRegistrations implements WebMvcRegistrations { private final ApiVersionConfig apiVersionConfig; @Autowired public ApiWebMvcRegistrations(ApiVersionConfig apiVersionConfig) { this.apiVersionConfig = apiVersionConfig; } @Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { if (apiVersionConfig.isEnable()) { return new ApiVersionRequestMappingHandlerMapping(); } return null; } }
至此我们就能在项目中愉快的使用@APIVersion来指定版本了,但是现在这个和swagger整合还会有问题,继续研究中…
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。