Spring Boot接口版本管理

gitee链接

Spring Boot版本:2.3.4.RELEASE

目的

项目迭代升级,接口要更新,并且要在接口路径不变的清空下兼容老接口,就可以做接口版本管理,像这样:

老接口:

{
	'headers': {
		'apiverison': 'v1', 
        'apiplatform': 'web',
		...
	},
    'url': '/user/login',
	'data': {
		'username': 'cc',
		'password': '123'
	},
	...
}
复制代码

新接口:

{
	'headers': {
		'apiverison': 'v2',
        'apiplatform': 'web',
		...
	},
    'url': '/user/login',
	'data': {
		'username': 'cc',
		'password': '123'
	},
	...
}
复制代码

对于前端来说,可以在headers中指定接口的版本,不需要修改接口的路径。

除了指定接口版本,还能指定接口的支持平台,比如web端、移动端。

实现

需要四个实现类,和web mvc的配置类

目录是这样的:

- com.cc
    - config
        - version
            ApiHandlerMapping
            ApiPlatform
            ApiVersion
            ApiVersionCondition
        WebMvcConfig
复制代码

@ApiVersion:

package com.cc.config.version;

import org.springframework.lang.Nullable;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD}) // 注解修饰的对象范围,TYPE:类,METHOD:方法
@Retention(RetentionPolicy.RUNTIME) // 注解保存到class和jvm内
public @interface ApiVersion {

    // 标识版本号
    int value();

    // 兼容平台
    int platform() default 0;
}
复制代码

ApiPlatform:

package com.cc.config.version;

/**
 * api接口调用平台声明
 * @author cc
 * @date 2021-11-19 11:23
 */
public interface ApiPlatform {
    /**
     * 默认
     */
    public static int DEFAULT = 0;

    /**
     * web端
     */
    public static int WEB = 1;

    /**
     * 移动端
     */
    public static int MOBILE = 2;
}
复制代码

ApiHandlerMapping:

package com.cc.config.version;

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;

public class ApiHandlerMapping extends RequestMappingHandlerMapping {
    // 对类修饰的注解
    @Override
    protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
        // 判断是否有@ApiVersion注解
        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);
    }

    // 创建基于@APIVersion的RequestCondition
    private RequestCondition<ApiVersionCondition> createCondition(ApiVersion apiVersion) {
        return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value());
    }
}
复制代码

ApiVersionCondition:

package com.cc.config.version;

import org.springframework.util.StringUtils;
import org.springframework.web.servlet.mvc.condition.RequestCondition;

import javax.servlet.http.HttpServletRequest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
    // 路径中版本的前缀,这里写正则:\d表示一位数字,\d+表示一位以上数字,如v1,v2
    private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+)");

    // header中的key
    private final static String HEADER_VERSION = "apiversion";
    private final static String HEADER_PLATFORM = "apiplatform";

    // api的版本
    private int apiVersion;

    // api的平台
    private int apiplatform;

    public ApiVersionCondition(int apiVersion, int apiplatform) {
        this.apiVersion = apiVersion;
        this.apiplatform = apiplatform;
    }

    // 将不同的筛选条件合并
    @Override
    public ApiVersionCondition combine(ApiVersionCondition apiVersionCondition) {
        // 采用最后定义优先原则,则方法上的定义覆盖类上面的定义
        return new ApiVersionCondition(apiVersionCondition.getApiVersion(), apiVersionCondition.getApiplatform());
    }

    // 根据request查找匹配到的筛选条件
    @Override
    public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
        /**
         * 正则匹配请求header参数中是否有版本号
         * 版本号应该以v开头,如:v1、v2,这个可以通过修改正则自定义
         */
        String apiversion = request.getHeader(HEADER_VERSION);
        String platformStr = request.getHeader(HEADER_PLATFORM);
        int apiplatform = 0;
        if (!StringUtils.isEmpty(platformStr)) {
            apiplatform = Integer.parseInt(platformStr);
        }

        if (!StringUtils.isEmpty(apiversion)) {
            Matcher m = VERSION_PREFIX_PATTERN.matcher(apiversion);
            if (m.find()) {
                int version = Integer.parseInt(m.group(1));

                // 版本匹配到了
                if (version == this.apiVersion) {

                    // 如果有传入平台platform参数,那么就找指定了平台的接口,找不到该接口就不通过
                    if (apiplatform > 0) {
                        if (this.apiplatform == apiplatform) {
                            return this;
                        } else {
                            return null;
                        }
                    }

                    // 如果该接口指定了平台,但是没有传platform或者传错,那么也不允许通过
                    if (this.apiplatform > 0 && this.apiplatform != apiplatform) {
                        return null;
                    }

                    return this;
                }

                // 没有可以匹配的接口
                return null;
            }
        }
        throw new RuntimeException("请检查header中的版本参数");
    }

    // 不同筛选条件比较,用于排序
    @Override
    public int compareTo(ApiVersionCondition apiVersionCondition, HttpServletRequest httpServletRequest) {
        return apiVersionCondition.getApiVersion() - this.apiVersion;
    }

    public int getApiVersion() {
        return apiVersion;
    }

    public void setApiVersion(int apiVersion) {
        this.apiVersion = apiVersion;
    }

    public int getApiplatform() {
        return apiplatform;
    }

    public void setApiplatform(int apiplatform) {
        this.apiplatform = apiplatform;
    }
}
复制代码

WebMvcConfig:

package com.cc.config;


import com.cc.config.version.ApiHandlerMapping;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.resource.ResourceUrlProvider;

/**
 * web mvc的配置类
 * @author cc
 * @date 2021-07-12 10:29
 */
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

    // 版本管理相关
    @Override
    public RequestMappingHandlerMapping requestMappingHandlerMapping(ContentNegotiationManager contentNegotiationManager, FormattingConversionService conversionService, ResourceUrlProvider resourceUrlProvider) {
        RequestMappingHandlerMapping handlerMapping = new ApiHandlerMapping();
        handlerMapping.setOrder(0);

        return handlerMapping;
    }
}
复制代码

使用

后端编写接口的时候只需要添加@ApiVersion注解即可:

package com.cc.controller;

import com.cc.config.version.ApiPlatform;
import com.cc.config.version.ApiVersion;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    @ApiVersion(1)
    @GetMapping("/login")
    public String login() {
        return "this api version is v1";
    }

    @ApiVersion(2)
    @GetMapping("/login")
    public String login2() {
        return "this api version is v2";
    }

    @ApiVersion(value = 2, platform = ApiPlatform.WEB)
    @GetMapping("/login")
    public String login21() {
        return "this api version is v3";
    }

    // 要避免这种相同版本和平台的接口,这种请求的时候会报错,因为和上面的接口重复了,系统就不知道你要请求的是哪个
//    @ApiVersion(value = 2, platform = ApiPlatform.WEB)
//    @GetMapping("/login")
//    public String login211() {
//        return "this api version is v4";
//    }
}
复制代码

controller里一共有三个接口,分别的调用方式是:

  1. 接口版本为v1,那么header是这样的:

    {
        'headers': {
            'apiverison': 'v1',
            ...
         },
        'url': '/login',
        ...
    }
    复制代码

    请求结果为:

    this api version is v1

  2. 接口版本为v2,header是这样的:

    {
        'headers': {
            'apiverison': 'v2',
            ...
         },
        'url': '/login',
        ...
    }
    复制代码

    请求结果为:

    this api version is v2

  3. 接口版本为v2,并且仅支持web端平台,在ApiPlatform里可以知道web端的标识是1,所以header是这样:

    {
        'headers': {
            'apiverison': 'v2',
            'apiplatform': '1',
            ...
         },
        'url': '/login',
        ...
    }
    复制代码

    请求结果为:

    this api version is v3

另外,@ApiVersion注解还能作用于类上,可以很方便的给类里所有的接口指定版本,并且因为最后定义优先原则,作用于函数上的注解会覆盖类上面的,所以像下面的代码:

package com.cc.controller;

import com.cc.config.version.ApiVersion;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@ApiVersion(1)
@RestController
public class TestController {
    @GetMapping("/t")
    public String t1() {
        return "t1";
    }

    @ApiVersion(2)
    @GetMapping("/t")
    public String t2() {
        return "t2";
    }
}
复制代码

t1函数的接口版本为类声明的v1,t2函数的接口版本为覆盖后的v2。

猜你喜欢

转载自juejin.im/post/7032168742066847781