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

2022-08-31 07:58:53

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

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

因此,将版本为1.0的API带到1.8,其中在1.0版本中引入了处理程序并在v1.7中进行了修改,我希望通过以下方式处理它。想象一下,代码位于控制器内部,并且有一些代码能够从标头中提取版本。(以下内容在春季无效)

@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种方法具有相同的注释并且Spring无法加载。这个想法是注释可以定义开放或关闭的版本范围。第一种方法在版本 1.0 到 1.6 之间有效,而第二种方法适用于版本 1.7 及更高版本(包括最新版本 1.8)。我知道如果有人决定通过版本99.99,这种方法会中断,但这是我可以忍受的。RequestMappingVersionRange

现在,由于如果不对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;
}

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

我将不胜感激任何想法...特别是,任何建议都以更简单,更易于维护的方式执行此操作。


编辑

添加赏金。要获得赏金,请回答上面的问题,而不是建议在控制器本身中采用此逻辑。Spring已经有很多逻辑来选择要调用的控制器方法,我想借用它。


编辑 2

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


答案 1

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

如何执行自定义请求映射,该映射对请求中的标头值进行任意评估,而无需在方法正文中进行评估?

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

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

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


答案 2

我刚刚创建了一个自定义解决方案。我将注释与类中的注释结合使用。@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");
    }
}

推荐