介绍

  • 每次版本升级都强制用户更新升级,这种方式每次用户打开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;
    }

}
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐