RESTful API 版本控制

​ 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请求的处理流程
springmvc中http请求的处理流程

  1. Client Request(客户端请求)
    • 客户端发起http请求到springboot应用
  2. DispatcherServlet
    • DispathcerServlet接收到客户端发起的请求,将这些请求分发到对应的处理器
    • DispatcherServlet首先会通过HandlerMapping查找请求对应的处理器
  3. HandlerMapping
    • HandlerMapping根据请求的URL和HTTP方法找到合适的Handler
  4. HandlerAdapter
    • 一旦找到合适的Handler(通常是一个Controller类中的方法),DispatcherServlet会使用HandlerAdapter来调用该Handler
    • HandlerAdapter负责适配Handler的调用方式,使其可以被DispatcherServlet调用
  5. Controller
    • Controller接收请求并处理业务逻辑。Controller通常会调用Service层来处理具体的业务逻辑
    • Controller方法的返回值会被封装成一个ResponseEntity或视图名称,交还给DispatcherServlet处理
  6. Response
    • DispatcherServlet会将结果封装成HTTP响应,并返回给客户端
      对应SpringBoot的处理代码块为:DispatcherServlet.java->doDispatch()
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,根据请求头中的不同版本号,请求到对应的业务接口上

  1. 自定义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;
    }
    
  2. 定一个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());
            }
        }
    }
    
  3. 将自定义的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();
        }
    }
    
    1. 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三个版本的接口,结果如下

      1. 请求v1 版本:添加了ApiVersion注解,默认为v1版本,所以请求头中带上X-VERSION:v1`,能够正常访问
      2. 请求v2 版本:添加了ApiVersion注解,指定为v2版本,虽然header中带上X-VERSION:v2`,但注解禁用了该版本,所以404
      3. 请求v1 版本:添加了ApiVersion注解,默认为v3版本,header中带上X-VERSION:v2,能够正常访问

猜你喜欢

转载自blog.csdn.net/GuoyangGuo/article/details/140572793