Spring MVC 实现REST风格API版本控制

Spring MVC 实现REST风格API版本控制

项目的开发会是一个迭代一直更新的过程,从软件工程的角度说,除非项目进入废弃,否则项目从开始到维护升级都是一个不断更新的项目。项目的版本更新和升级这个概念很好理解,但是实施起来切实是一个痛点。现在大部分的系统后端架构通常都是基于一个网关对外暴露API接口,这些API包括网页端API接口,APP端接口和小程序端接口等。

后端项目的升级可以通过我们自己来升级,但是对于用户而言,类似APP或者PC端程序用户而言,更新程序与后端接口达成一致性是很困难的,特别是你的项目群体特别大的时候,没有出现特别大的后端版本更新的时候,通常都只是提示用户更新而已。所以会造成部分用户更新至最新的版本,而部分用户还停留在旧版本中。所以一个接口操作会存在不同版本客户端请求。相比PC端应用程序和移动端应用程序,C/S架构的应用程序,可以做到及时更新,但是也会存在一个问题,

  • 对于当前的接口,Web端API更新到最新了,但是移动端或者PC端用户还没有更新上来。

所以系统的更新同样需要考虑接口的版本问题。本文章综合网络上的各种解决方案,写一个基于Spring MVC的REST风格的API版本方法。

上代码:

定义一个Api版本的注解


/**
 * @author shaoyayu
 * @date 2021/12/10
 * @apiNote api接口的注解
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
    int value();
}

定义一个类实现RequestCondition


/**
 * @author shaoyayu
 * @date 2021/12/10
 * @apiNote
 */
@Getter
@Setter
@Slf4j
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {

    private static final Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+)/");

    private int apiVersion;

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

    @Override
    public ApiVersionCondition combine(ApiVersionCondition apiVersionCondition) {
        return new ApiVersionCondition(apiVersionCondition.getApiVersion());
    }

    @Override
    public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
        try {
            Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI());
            if (m.find()){
                int version = Integer.parseInt(m.group(1));
                if (version>=this.apiVersion){
                    return this;
                }
            }
            return null;
        }catch (Exception e){
            log.info("api 版本转换异常:"+request.getRequestURI());
        }
        return null;
    }

    @Override
    public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
        return other.getApiVersion() - this.apiVersion;
    }
}

到这里已经完成一半了,但是需要一个继承RequestMappingHandlerMapping的实现类


/**
 * @author shaoyayu
 * @date 2021/12/10
 * @apiNote
 */
public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
    @Override
    protected RequestCondition<ApiVersionCondition> getCustomTypeCondition(Class<?> handlerType) {
        ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
        return createCondition(apiVersion);
    }
    @Override
    protected RequestCondition<ApiVersionCondition> getCustomMethodCondition(Method method) {
        ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
        return createCondition(apiVersion);
    }

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


剩下的就是把这个实现RequestMappingHandlerMapping的CustomRequestMappingHandlerMapping注入到容器中。

有两种方法实现注册。一通过配置类里面,注入


/**
 * @author shaoyayu
 * @date 2021/12/10
 * @apiNote
 */
@Configuration
public class WebConfiguration extends WebMvcAutoConfiguration {

    @Bean
    public RequestMappingHandlerMapping requestMappingHandlerMapping(){
        RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping();
        handlerMapping.setOrder(0);
        return handlerMapping;
    }

}

这种方式注入Spring Boot版本越高也是不推荐这种方法。

第二种方法,通过继承WebMvcConfigurationSupport,在createRequestMappingHandlerMapping方法中返回

/**
 * @author shaoyayu
 * @date 2021/12/10
 * @apiNote
 */
@Configuration
public class CustomWebMvcConfigurationSupport extends WebMvcConfigurationSupport {
    @Override
    protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
        RequestMappingHandlerMapping  handlerMapping = new CustomRequestMappingHandlerMapping();
        handlerMapping.setOrder(0);
        return handlerMapping;
    } 
}


个人推荐第二种方式。

测试

定义一个UserController


/**
 * @author shaoyayu
 * @date 2021/12/10
 * @apiNote SonarLint: Remove this empty class, write its code or make it an "interface".
 */
@Slf4j
@RestController
@RequestMapping("{version}/web/user")
public class UserController {

    @GetMapping("/hello")
    public Map<String,Object> hello(){
        Map<String, Object> map = new HashMap<>();
        map.put("code",200);
        map.put("ok",true);
        map.put("msg","v api interface");
        map.put("data",null);
        return map;
    }

    @GetMapping("/hello")
    @ApiVersion(1)
    public Map<String,Object> hello1(){
        Map<String, Object> map = new HashMap<>();
        map.put("code",200);
        map.put("ok",true);
        map.put("msg","v1 api interface");
        map.put("data",null);
        return map;
    }

    @GetMapping("/hello")
    @ApiVersion(2)
    public Map<String,Object> hello2(){
        Map<String, Object> map = new HashMap<>();
        map.put("code",200);
        map.put("ok",true);
        map.put("msg","v2 api interface");
        map.put("data",null);
        return map;
    }

}

测试的结果

  • 如访问/web/user/hello不带版本号,出现404的结果

  • 访问/v/web/user/hello会调用第一个方法

  • 访问/v1/web/user/helo会调用第二个方法。

  • 访问/v2/web/user/helo会调用第三个方法。

  • 访问/v3/web/user/hello会调用第三个方法。

综合的测试结果,

  1. 不带v的版本控制会出现404

  2. 版本号会向下访问比自己更低一级的版本

posted @ 2021-12-10 02:37  shaoyayu  阅读(329)  评论(0编辑  收藏  举报