如何使用 spring 管理 REST API 版本控制?

新手上路,请多包涵

我一直在寻找如何使用 Spring 3.2.x 管理 REST API 版本,但我没有找到任何易于维护的东西。我将首先解释我遇到的问题,然后是解决方案……但我确实想知道我是否在这里重新发明了轮子。

我想根据 Accept 标头管理版本,例如,如果请求具有 Accept 标头 application/vnd.company.app-1.1+json ,我希望 spring MVC 将其转发到处理该版本的方法。而且由于并非 API 中的所有方法都在同一版本中更改,我不想转到我的每个控制器并更改版本之间没有更改的处理程序的任何内容。我也不希望有逻辑来确定在控制器本身中使用哪个版本(使用服务定位器),因为 Spring 已经发现要调用哪个方法。

因此,采用 1.0 版的 API 到 1.8 版,其中在 1.0 版中引入了处理程序并在 v1.7 中进行了修改,我想按以下方式处理。假设代码在控制器中,并且有一些代码能够从标头中提取版本。 (以下在Spring中无效)

 @RequestMapping(...)
@VersionRange(1.0,1.6)
@ResponseBody
public Object method1() {
   // so something
   return object;
}

@RequestMapping(...) //same Request mapping annotation
@VersionRange(1.7)
@ResponseBody
public Object method2() {
   // so something
   return object;
}

这在 spring 中是不可能的,因为 2 种方法具有相同的 RequestMapping 注释并且 Spring 无法加载。这个想法是 VersionRange 注释可以定义一个开放或封闭的版本范围。第一种方式适用于1.0-1.6版本,第二种方式适用于1.7以上版本(包括最新的1.8版本)。我知道如果有人决定通过 99.99 版,这种方法就会失效,但我可以接受。

现在,由于如果不对 spring 的工作方式进行认真的返工,上述内容是不可能的,我正在考虑修改处理程序与请求匹配的方式,特别是编写我自己的 ProducesRequestCondition ,并且版本范围在那里。例如

代码:

 @RequestMapping(..., produces = "application/vnd.company.app-[1.0-1.6]+json)
@ResponseBody
public Object method1() {
   // so something
   return object;
}

@RequestMapping(..., produces = "application/vnd.company.app-[1.7-]+json)
@ResponseBody
public Object method2() {
   // so something
   return object;
}

这样,我可以在注释的生成部分中定义封闭或开放的版本范围。我现在正在研究这个解决方案,问题是我仍然必须替换一些核心 Spring MVC 类( RequestMappingInfoHandlerMappingRequestMappingHandlerMappingRequestMappingInfo ),which-db我不喜欢,因为每当我决定升级到更新版本的 spring 时,这意味着额外的工作。

我将不胜感激任何想法……尤其是任何以更简单、更易于维护的方式执行此操作的建议。


编辑

添加赏金。要获得赏金,请在不建议在控制器本身中包含此逻辑的情况下回答上述问题。 Spring 已经有很多逻辑来选择调用哪个控制器方法,我想借助它。


编辑 2

我在 github 中分享了原始的 POC(有一些改进): https ://github.com/augusto/restVersioning

原文由 Augusto 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 717
2 个回答

不管是否可以通过向后兼容的更改来避免版本控制(当您受某些公司准则的约束或者您的 API 客户端以错误的方式实现并且即使不应该也会中断时,这可能并不总是可能的)抽象的需求是一个有趣的一:

我如何做一个自定义请求映射,在不在方法主体中进行评估的情况下,对请求中的标头值进行任意评估?

本 SO 答案 中所述,您实际上可以拥有相同的 @RequestMapping 并使用不同的注释来区分运行时发生的实际路由。为此,您必须:

  1. 创建一个新注释 VersionRange
  2. 实施 RequestCondition<VersionRange> 。由于您将拥有类似于最佳匹配算法的东西,因此您将必须检查用其他 VersionRange 值注释的方法是否为当前请求提供更好的匹配。
  3. 根据注解和请求条件实现 VersionRangeRequestMappingHandlerMapping (如 如何实现@RequestMapping 自定义属性 一文中所述)。
  4. 配置 spring 以评估您的 VersionRangeRequestMappingHandlerMapping 在使用默认值 RequestMappingHandlerMapping --- 之前(例如,通过将其顺序设置为 0)。

这不需要对 Spring 组件进行任何 hacky 替换,而是使用 Spring 配置和扩展机制,因此即使您更新 Spring 版本它也应该可以工作(只要新版本支持这些机制)。

原文由 xwoker 发布,翻译遵循 CC BY-SA 3.0 许可协议

我刚刚创建了一个自定义解决方案。我将 @ApiVersion 注释与 @RequestMapping 注释结合使用 --- @Controller 类。

例子:

 @Controller
@RequestMapping("x")
@ApiVersion(1)
class MyController {

    @RequestMapping("a")
    void a() {}         // maps to /v1/x/a

    @RequestMapping("b")
    @ApiVersion(2)
    void b() {}         // maps to /v2/x/b

    @RequestMapping("c")
    @ApiVersion({1,3})
    void c() {}         // maps to /v1/x/c
                        //  and to /v3/x/c

}

执行:

ApiVersion.java 注解:

 @Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
    int[] value();
}

ApiVersionRequestMappingHandlerMapping.java (这主要是从 RequestMappingHandlerMapping 复制和粘贴):

 public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    private final String prefix;

    public ApiVersionRequestMappingHandlerMapping(String prefix) {
        this.prefix = prefix;
    }

    @Override
    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        RequestMappingInfo info = super.getMappingForMethod(method, handlerType);
        if(info == null) return null;

        ApiVersion methodAnnotation = AnnotationUtils.findAnnotation(method, ApiVersion.class);
        if(methodAnnotation != null) {
            RequestCondition<?> methodCondition = getCustomMethodCondition(method);
            // Concatenate our ApiVersion with the usual request mapping
            info = createApiVersionInfo(methodAnnotation, methodCondition).combine(info);
        } else {
            ApiVersion typeAnnotation = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
            if(typeAnnotation != null) {
                RequestCondition<?> typeCondition = getCustomTypeCondition(handlerType);
                // Concatenate our ApiVersion with the usual request mapping
                info = createApiVersionInfo(typeAnnotation, typeCondition).combine(info);
            }
        }

        return info;
    }

    private RequestMappingInfo createApiVersionInfo(ApiVersion annotation, RequestCondition<?> customCondition) {
        int[] values = annotation.value();
        String[] patterns = new String[values.length];
        for(int i=0; i<values.length; i++) {
            // Build the URL prefix
            patterns[i] = prefix+values[i];
        }

        return new RequestMappingInfo(
                new PatternsRequestCondition(patterns, getUrlPathHelper(), getPathMatcher(), useSuffixPatternMatch(), useTrailingSlashMatch(), getFileExtensions()),
                new RequestMethodsRequestCondition(),
                new ParamsRequestCondition(),
                new HeadersRequestCondition(),
                new ConsumesRequestCondition(),
                new ProducesRequestCondition(),
                customCondition);
    }

}

注入 WebMvcConfigurationSupport:

 public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        return new ApiVersionRequestMappingHandlerMapping("v");
    }
}

原文由 Benjamin M 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题