Spring Boot ApiVersion版本增强


placeholder image
admin 发布于:2022-05-09 10:09:56
阅读:loading

背景介绍

最初见到ApiVersion还是在拉取了swagger-bootstrap-ui项目源代码后查看了其中的示例代码,发现了有个@ApiVersion的东西,搁现在已经记不起当时到底是发现了什么新大陆,反正是在一番好奇的想法中去百科了一大堆资料,东拼西凑的各种组合,最终就有了本文的实践结果。所以本次所讲的ApiVersion是指通过注解参数来给Controller的请求地址前缀增加版本编号的实现,请注意通过代码注解实现,并非采用手动编写地址的方式(实际上在工作的项目代码中也有遇到类似的场景,但是都是手动/v1的形式编写),毕竟专业的人采用专业的方式做专业的事情。

本文所述的ApiVersion的实现是通过覆盖webmvc的ApiRequestMappingHandlerMapping来实现扩展RequestMapping路径的目的,提供@ApiVersion注解来编写具体的Api版本号,分别可标记为class类层级、method方法层级和二者并存时,详细如下:

(1)当标记为class层级时,表示该类的requestMapping路径前统一增加以“v” + 版本号,该类中的所有方法前缀均增加次部分版本号,如:“/v1/user”;

(2)当标记为method层级时,表示该类中的当前方法的访问路径增加上述规则,同上;

(3)当标记在class和method同时被标记时除了当前类的所有方法增加上述规则时,会增加方法中的版本号,即“v” + 版本号 + "/" + 版本号,如:“/v1/1/user”;

引入一段常规代码示例进行初步展示,参考如下图所示:

image.png

上图代码是普通的Spring MVC的Controller代码,集成了swagger组件的Api文档,提供了两个Get类型的Restful接口,它们的接口地址都是“/api_version/integer”,正常情况该写法会导致导致服务启动失败,原因是因为它们的接口类型和接口地址相同,但需注意在35行增加了@ApiVersion,该注解的实现即是将方法的RequestMapping路径覆盖了,即路径为“/v3/integer”,所以可以正常启动。

运行结果

本次示例转换编写了类的维度、方法维度和类与方法并存时的三种场景的示例来演示接口的路径,参考如下图所示:

image.png

(类维度的@ApiVersion)

image.png

(类与方法两个维度的@ApiVersion)

代码实现参考

package cn.chendd.base.api.version.commponents;

import cn.chendd.base.api.version.annotations.ApiVersion;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.servlet.mvc.condition.*;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.lang.reflect.Method;

/**
 * ApiRequestMapping重写
 *
 * @author chendd
 * @date 2019/10/16 22:34
 */
public class ApiRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    /**
     * 加入版本控制
     */
    @Override
    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        RequestMappingInfo info = super.getMappingForMethod(method, handlerType);
        if (info != null) {
            return createApiVersionInfo(method, handlerType, info);
        }else{
            return info;
        }
    }

    @Override
    protected RequestCondition<ApiVesrsionCondition> getCustomTypeCondition(Class<?> handlerType) {
        ApiVersion apiVersion = (ApiVersion) AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
        return createCondition(apiVersion);
    }

    @Override
    protected RequestCondition<ApiVesrsionCondition> getCustomMethodCondition(Method method) {
        ApiVersion apiVersion = (ApiVersion) AnnotationUtils.findAnnotation(method, ApiVersion.class);
        return createCondition(apiVersion);
    }

    private RequestMappingInfo createApiVersionInfo(Method method, Class<?> handlerType, RequestMappingInfo info) {

        boolean hasType = false;
        ApiVersion typeAnnotation = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
        if (typeAnnotation != null) {
            hasType = true;
        }
        ApiVersion methodAnnotation = AnnotationUtils.findAnnotation(method, ApiVersion.class);
        if (methodAnnotation != null) {
            RequestCondition<?> methodCondition = getCustomMethodCondition(method);
            //
            String value = !hasType ? ("v" + methodAnnotation.value()) : methodAnnotation.value();
            info = createApiVersionInfo(value, methodCondition).combine(info);
        }
        //如果方法上增加有版本号,再判断类上是否存在版本号,如果存在则结果为:/类版本/方法版本
        //如果不需要实现版本路径叠加则将此处修改为 if - else 逻辑即可
        if (hasType) {
            RequestCondition<?> typeCondition = getCustomTypeCondition(handlerType);
            info = createApiVersionInfo("v" + typeAnnotation.value(), typeCondition).combine(info);
        }
        return info;
    }

    private RequestMappingInfo createApiVersionInfo(String value , RequestCondition<?> customCondition) {
        //多组地址映射路径
        String[] patterns = new String[]{ value };
        return new RequestMappingInfo(
                /*new PatternsRequestCondition(patterns, getUrlPathHelper(), getPathMatcher(), useSuffixPatternMatch(),
                        useTrailingSlashMatch(), getFileExtensions()),*/
                new PatternsRequestCondition(patterns , getUrlPathHelper() , getPathMatcher() , useTrailingSlashMatch()),
                new RequestMethodsRequestCondition(), new ParamsRequestCondition(),
                new HeadersRequestCondition(), new ConsumesRequestCondition(),
                new ProducesRequestCondition(), customCondition);
    }

    private RequestCondition<ApiVesrsionCondition> createCondition(ApiVersion apiVersion) {
        return apiVersion == null ? null : new ApiVesrsionCondition(apiVersion.value());
    }

}

源码下载

源码工程下载可转至https://gitee.com/88911006/chendd-blog-examples项目的ApiVersion分支;


 点赞


 发表评论

当前回复:作者

 评论列表


留言区