API版本控制是项目开发过程必不可少的一个手段,随着系统业务规则的不断变化,API相应的也需要进行调整;当变化特别的大的时候往往会对API进行版本区分,拆分成v1,v2这种,这样做的好处是当业务规则发生较大的改动时,可保证改动对前一个版本不受影响,也方便进行灰度发布,当v2版本完全稳定后,可根据实际需要将v1进行弃用。
API版本控制的方式
URL路径方式版本控制
在url路径添加版本号是最常见的版本控制方式,这种方式清晰明了,通过请求的url就能知道对应的版本
/api/v1/test
/api/v2/test
嵌入式版本控制
将版本号嵌入到域名中,通过不同的子域名访问不同的版本,这种方式同样也清晰明了通过子域名就能区分版本,但增加域名管理的成本
https://v1.api.geekyous.com
https://v2.api.geekyous.com
基于请求参数方式控制
将版本号放在请求参数中,根据请求的版本号进入相对应的业务处理方法,这种方式的好处是api的请求URL不变,可动态的控制版本号
https://api.geekyous.com/test?version=1
https://api.geekyous.com/test?version=2
基于请求头参数方式控制
将版本放在请求头中,可通过自定义的http head进行传递,此方式同样保持了api的URL不变,动态的传递版本号
curl -H X-VERSION:v1 http://127.0.0.1:34434/api/sys/test
curl -H X-VERSION:v2 http://127.0.0.1:34434/api/sys/test
在Springboot中实现API版本控制
springmvc请求原理
springboot是基于spring的快速开发框架,以下是springmvc中http请求的处理流程
- Client Request(客户端请求)
- 客户端发起http请求到springboot应用
- DispatcherServlet
- DispathcerServlet接收到客户端发起的请求,将这些请求分发到对应的处理器
- DispatcherServlet首先会通过HandlerMapping查找请求对应的处理器
- HandlerMapping
- HandlerMapping根据请求的URL和HTTP方法找到合适的Handler
- HandlerAdapter
- 一旦找到合适的Handler(通常是一个Controller类中的方法),DispatcherServlet会使用HandlerAdapter来调用该Handler
- HandlerAdapter负责适配Handler的调用方式,使其可以被DispatcherServlet调用
- Controller
- Controller接收请求并处理业务逻辑。Controller通常会调用Service层来处理具体的业务逻辑
- Controller方法的返回值会被封装成一个ResponseEntity或视图名称,交还给DispatcherServlet处理
- Response
- DispatcherServlet会将结果封装成HTTP响应,并返回给客户端
对应SpringBoot的处理代码块为:DispatcherServlet.java->doDispatch()
- DispatcherServlet会将结果封装成HTTP响应,并返回给客户端
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new ServletException("Handler dispatch failed: " + err, err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new ServletException("Handler processing failed: " + err, err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
自定义HttpMapping实现版本控制
自定义一个HttpMapping,根据请求头中的不同版本号,请求到对应的业务接口上
-
自定义ApiVersion枚举,用来标识接口版本和该版本是否启用
package com.geekyous.core.config.system.version; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * api版本控制注解 */ @Target({ ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface ApiVersion { // 版本号,默认版本v1 String value() default "v1"; // 是否禁用 boolean disable() default false; }
-
定一个ApiVersionHandlerMapping,用来处理不同版本的url
package com.geekyous.core.config.system.version; import jakarta.servlet.http.HttpServletRequest; 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; /** * 自定义RequestMappingHandlerMapping,将不同版本号的API URL注册到Spring中 */ public class ApiVersionHandlerMapping extends RequestMappingHandlerMapping { @Override protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) { return buildFrom(AnnotationUtils.findAnnotation(handlerType, ApiVersion.class)); } @Override protected RequestCondition<?> getCustomMethodCondition(Method method) { return buildFrom(AnnotationUtils.findAnnotation(method, ApiVersion.class)); } private ApiVersionRequestCondition buildFrom(ApiVersion apiVersion) { // 添加了ApiVersion注解的,根注解信息返回 RequestCondition return apiVersion == null ? null : new ApiVersionRequestCondition(apiVersion); } /** * 自定RequestCondition,根据http请求的版本号,将请求映射到对应的版本的API上 * * @param apiVersion */ private record ApiVersionRequestCondition( ApiVersion apiVersion) implements RequestCondition<ApiVersionRequestCondition> { @Override public ApiVersionRequestCondition combine(ApiVersionRequestCondition other) { return new ApiVersionRequestCondition(other.apiVersion); } @Override public ApiVersionRequestCondition getMatchingCondition(HttpServletRequest request) { // 解析请求头中的版本号 String version = request.getHeader("X-VERSION"); // 版本号一致且启用返回 if (this.apiVersion.value().equals(version) && !this.apiVersion.disable()) { return this; } return null; } @Override public int compareTo(ApiVersionRequestCondition other, HttpServletRequest request) { return other.apiVersion.value().compareTo(this.apiVersion.value()); } } }
-
将自定义的ApiVersionHandlerMapping注册到Spring容器中
package com.geekyous.core.config.system; import com.geekyous.core.config.system.version.ApiVersionHandlerMapping; import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; @Configuration public class WebMvcRegistrationConfig implements WebMvcRegistrations { @Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { return new ApiVersionHandlerMapping(); } }
-
VersionTestController 测试不同版本的返回结果
package com.geekyous.controller; import com.geekyous.core.config.system.version.ApiVersion; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/sys/test") public class VersionTestController { /** * 添加ApiVersion注解,默认v1版本 * * @return 版本信息 */ @GetMapping() @ApiVersion public String v1() { return "v1"; } /** * 添加ApiVersion注解,标注版本为v2,但禁用该版本 * * @return 版本信息 */ @GetMapping() @ApiVersion(value = "v2", disable = true) public String v2() { return "v2"; } /** * 添加ApiVersion注解,标注版本为v3 * * @return 版本信息 */ @GetMapping() @ApiVersion("v3") public String v3() { return "v3"; } }
分别请求v1、v2、v3三个版本的接口,结果如下
- 请求
v1
版本:添加了ApiVersion注解,默认为v1版本,所以请求头中带上
X-VERSION:v1`,能够正常访问 - 请求
v2
版本:添加了ApiVersion注解,指定为v2版本,虽然header中带上
X-VERSION:v2`,但注解禁用了该版本,所以404 - 请求
v1
版本:添加了ApiVersion注解
,默认为v3版本,header中带上X-VERSION:v2
,能够正常访问
- 请求
-