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

Posted

技术标签:

【中文标题】如何使用 spring 管理 REST API 版本控制?【英文标题】:How to manage REST API versioning with spring? 【发布时间】:2013-12-10 11:40:10 【问题描述】:

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

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

因此采用了 1.0 到 1.8 版本的 API,其中处理程序在 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),因为这意味着无论何时都需要额外的工作我决定升级到更新版本的 spring。

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


编辑

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


编辑 2

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

【问题讨论】:

***.com/questions/11715874/… @flup 我不明白你的评论。这只是说您可以使用标头,正如我所说,spring 提供的开箱即用的功能不足以支持不断更新的 API。更糟糕的是,该答案上的链接使用了 URL 中的版本。 我们需要支持多个版本的 API,这些差异通常是一些细微的变化,会使某些客户端的某些调用不兼容(如果我们需要支持 4 个小版本,这并不奇怪,其中一些端点不兼容)。我很欣赏将它放在 url 中的建议,但我们知道这是朝着错误方向迈出的一步,因为我们有几个应用程序的版本在 URL 中,并且每次我们需要提升时都会涉及很多工作版本。 @Augusto,实际上你还没有。只需以不破坏向后兼容性的方式设计您的 API 更改。只需举出破坏兼容性的更改示例,我将向您展示如何以不破坏的方式进行这些更改。 您是否看过***.com/a/10336769/2615437,这似乎暗示您的陈述“这在spring 中是不可能的,因为这两种方法具有相同的RequestMapping 注释并且Spring 无法加载。”不完全正确? 【参考方案1】:

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

如何进行自定义请求映射,对请求中的标头值进行任意评估,而不在方法主体中进行评估?

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

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

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

【讨论】:

感谢您添加您的评论作为答案 xwoker。到目前为止是最好的一个。我已经根据您提到的链接实施了解决方案,而且还不错。升级到新版本的 Spring 时会出现最大的问题,因为它需要检查对 mvc:annotation-driven 背后逻辑的任何更改。希望 Spring 将提供一个 mvc:annotation-driven 版本,可以在其中定义自定义条件。 @Augusto,半年后,这对你来说效果如何?另外,我很好奇,您真的是按方法进行版本控制吗?在这一点上,我想知道在每个类/每个控制器级别粒度上的版本是否会更清晰? @SanderVerhagen 它正在工作,但我们对整个 API 进行版本控制,而不是按方法或控制器(API 非常小,因为它专注于业务的一个方面)。我们确实有一个更大的项目,他们选择为每个资源使用不同的版本并在 URL 上指定(这样你就可以在 /v1/sessions 上拥有一个端点,在完全不同的版本上拥有另一个资源,例如 /v4/orders) ...它更灵活一些,但它给客户端带来了更大的压力,让他们知道要调用每个端点的哪个版本。 不幸的是,这与 Swagger 不兼容,因为在扩展 WebMvcConfigurationSupport 时会关闭许多自动配置。 我尝试了这个解决方案,但它实际上不适用于 2.3.2.RELEASE。你有一些示例项目要展示吗?【参考方案2】:

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

示例:

@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");
    

【讨论】:

我将 int[] 更改为 String[] 以允许像“1.2”这样的版本,所以我可以处理像“latest”这样的关键字 是的,这很合理。对于未来的项目,出于某些原因,我会采用不同的方式:1. URL 代表资源。 /v1/aResource/v2/aResource 看起来像不同的资源,它只是同一资源的不同表示! 2. 使用 HTTP 标头看起来更好,但是你不能给别人一个 URL,因为 URL 不包含标头。 3. 使用 URL 参数,即/aResource?v=2.1(顺便说一句:这是 Google 进行版本控制的方式)。 ... 我仍然不确定我是否会选择 23 选项,但出于某种原因我再也不会使用 1上面提到的。 如果你想将你自己的RequestMappingHandlerMapping注入你的WebMvcConfiguration,你应该覆盖createRequestMappingHandlerMapping而不是requestMappingHandlerMapping!否则你会遇到奇怪的问题(我突然遇到了 Hibernates 延迟初始化的问题,因为会话关闭了) 该方法看起来不错,但不知何故似乎不适用于 junti 测试用例 (SpringRunner)。您有任何机会使用测试用例的方法 有一种方法可以完成这项工作,不要扩展WebMvcConfigurationSupport ,而是扩展DelegatingWebMvcConfiguration。这对我有用(见***.com/questions/22267191/…)【参考方案3】:

我仍然建议使用 URL 进行版本控制,因为在 URL 中,@RequestMapping 支持模式和路径参数,可以使用 regexp 指定格式。

为了处理客户端升级(您在评论中提到),您可以使用“latest”之类的别名。或者有使用最新版本的未版本化 api(是的)。

同样使用路径参数,您可以实现任何复杂的版本处理逻辑,如果您已经想要范围,那么您很可能想要更快的东西。

这里有几个例子:

@RequestMapping(
    "/**/public_api/1.1/method",
    "/**/public_api/1.2/method",
)
public void method1()


@RequestMapping(
    "/**/public_api/1.3/method"
    "/**/public_api/latest/method"
    "/**/public_api/method" 
)
public void method2()


@RequestMapping(
    "/**/public_api/1.4/method"
    "/**/public_api/beta/method"
)
public void method2()


//handles all 1.* requests
@RequestMapping(
    "/**/public_api/version:1\\.\\d+/method"
)
public void methodManual1(@PathVariable("version") String version)


//handles 1.0-1.6 range, but somewhat ugly
@RequestMapping(
    "/**/public_api/version:1\\.[0123456]?/method"
)
public void methodManual1(@PathVariable("version") String version)


//fully manual version handling
@RequestMapping(
    "/**/public_api/version/method"
)
public void methodManual2(@PathVariable("version") String version)
    int[] versionParts = getVersionParts(version);
    //manual handling of versions


public int[] getVersionParts(String version)
    try
        String[] versionParts = version.split("\\.");
        int[] result = new int[versionParts.length];
        for(int i=0;i<versionParts.length;i++)
            result[i] = Integer.parseInt(versionParts[i]);
        
        return result;
    catch (Exception ex) 
        return null;
    

基于最后一种方法,您实际上可以实现您想要的东西。

例如,您可以拥有一个仅包含带有版本处理的方法 stab 的控制器。

在该处理中,您可以(使用反射/AOP/代码生成库)在某些 spring 服务/组件或同一类中查找具有相同名称/签名和需要 @VersionRange 的方法,并通过所有参数调用它。

【讨论】:

【参考方案4】:

我已经实现了一个解决方案,可以完美地解决其余版本控制的问题。

一般来说,REST 版本控制有 3 种主要方法:

Path-based approch,其中客户端在 URL 中定义版本:

http://localhost:9001/api/v1/user
http://localhost:9001/api/v2/user

Content-Type头,客户端在Accept头中定义版本:

http://localhost:9001/api/v1/user with 
Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json

自定义标头,其中客户端在自定义标头中定义版本。

first 方法的问题是,如果您更改版本,比如说从 v1 -> v2,可能您需要复制粘贴没有的 v1 资源'未更改为 v2 路径

第二种方法的问题是,像http://swagger.io/这样的一些工具无法区分具有相同路径但不同Content-Type的操作(检查问题https://github.com/OAI/OpenAPI-Specification/issues/146)

解决方案

由于我经常使用其他文档工具,因此我更喜欢使用第一种方法。我的解决方案使用第一种方法处理问题,因此您无需将端点复制粘贴到新版本。

假设我们有用户控制器的 v1 和 v2 版本:

package com.mspapant.example.restVersion.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * The user controller.
 *
 * @author : Manos Papantonakos on 19/8/2016.
 */
@Controller
@Api(value = "user", description = "Operations about users")
public class UserController 

    /**
     * Return the user.
     *
     * @return the user
     */
    @ResponseBody
    @RequestMapping(method = RequestMethod.GET, value = "/api/v1/user")
    @ApiOperation(value = "Returns user", notes = "Returns the user", tags = "GET", "User")
    public String getUserV1() 
         return "User V1";
    

    /**
     * Return the user.
     *
     * @return the user
     */
    @ResponseBody
    @RequestMapping(method = RequestMethod.GET, value = "/api/v2/user")
    @ApiOperation(value = "Returns user", notes = "Returns the user", tags = "GET", "User")
    public String getUserV2() 
         return "User V2";
    
 

要求是,如果我为用户资源请求 v1,我必须接受 "User V1" 响应,否则如果我请求 v2v3 等我必须接受 "User V2" 响应。

为了在 spring 中实现这一点,我们需要覆盖默认的 RequestMappingHandlerMapping 行为:

package com.mspapant.example.restVersion.conf.mapping;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class VersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping 

    @Value("$server.apiContext")
    private String apiContext;

    @Value("$server.versionContext")
    private String versionContext;

    @Override
    protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception 
        HandlerMethod method = super.lookupHandlerMethod(lookupPath, request);
        if (method == null && lookupPath.contains(getApiAndVersionContext())) 
            String afterAPIURL = lookupPath.substring(lookupPath.indexOf(getApiAndVersionContext()) + getApiAndVersionContext().length());
            String version = afterAPIURL.substring(0, afterAPIURL.indexOf("/"));
            String path = afterAPIURL.substring(version.length() + 1);

            int previousVersion = getPreviousVersion(version);
            if (previousVersion != 0) 
                lookupPath = getApiAndVersionContext() + previousVersion + "/" + path;
                final String lookupFinal = lookupPath;
                return lookupHandlerMethod(lookupPath, new HttpServletRequestWrapper(request) 
                    @Override
                    public String getRequestURI() 
                        return lookupFinal;
                    

                    @Override
                    public String getServletPath() 
                        return lookupFinal;
                    );
            
        
        return method;
    

    private String getApiAndVersionContext() 
        return "/" + apiContext + "/" + versionContext;
    

    private int getPreviousVersion(final String version) 
        return new Integer(version) - 1 ;
    

实现读取 URL 中的版本并从 spring 请求解析 URL。如果此 URL 不存在(例如客户端请求 v3),那么我们尝试使用 v2 以此类推,直到我们找到资源的最新版本

为了查看此实施的好处,假设我们有两个资源:用户和公司:

http://localhost:9001/api/vversion/user
http://localhost:9001/api/vversion/company

假设我们对公司“合同”进行了更改,导致客户违约。所以我们实现了http://localhost:9001/api/v2/company,并要求客户在v1上改为v2。

所以来自客户端的新请求是:

http://localhost:9001/api/v2/user
http://localhost:9001/api/v2/company

代替:

http://localhost:9001/api/v1/user
http://localhost:9001/api/v1/company

这里最好的部分是,使用此解决方案,客户将从 v1 获取用户信息,从 v2 获取公司信息无需创建新的(相同的)来自用户 v2 的端点!

其他文档 正如我之前所说,我选择基于 URL 的版本控制方法的原因是,像 swagger 这样的一些工具不会以不同的方式记录具有相同 URL 但不同内容类型的端点。使用此解决方案,两个端点都会显示,因为它们的 URL 不同:

GIT

解决方案实施: https://github.com/mspapant/restVersioningExample/

【讨论】:

我希望得到尝试过此解决方案的人的反馈。 :) 注意:如果您使用 /v0 或 /v-1 发送请求,则会发生无限循环。要修复它,您需要将if (previousVersion != 0) 设置为&gt;0,并且您需要在getPreviousVersion() 中捕获异常并返回-1 使用最新的 Spring 版本 2.4.2 您需要将创建新请求调整为:gist.github.com/eisenreich/6ab40616a9e694bc2220c68ec3a01455 github.com/OAI/OpenAPI-Specification/issues/146 在 OpenAPI 3.x 规范中得到解决【参考方案5】:

@RequestMapping 注释支持headers 元素,允许您缩小匹配请求的范围。特别是您可以在此处使用Accept 标头。

@RequestMapping(headers = 
    "Accept=application/vnd.company.app-1.0+json",
    "Accept=application/vnd.company.app-1.1+json"
)

这与您所描述的不完全一样,因为它不直接处理范围,但该元素确实支持 * 通配符以及 !=。因此,至少对于所有版本都支持相关端点的情况,甚至是给定主要版本的所有次要版本(例如 1.*),您可以避免使用通配符。

我认为我以前没有真正使用过这个元素(如果我不记得了),所以我只是离开了文档

http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestMapping.html

【讨论】:

我知道这一点,但正如您所指出的,在每个版本上,我都需要访问我的所有控制器并添加一个版本,即使它们没有更改。您提到的范围仅适用于完整类型,例如 application/* 而不是类型的一部分。例如以下在 Spring "Accept=application/vnd.company.app-1.*+json" 中是无效的。这与spring类MediaType的工作原理有关 @Augusto 您不一定需要这样做。使用这种方法,您不是对“API”而是“端点”进行版本控制。每个端点可以有不同的版本。对我来说,这是对 API compared with API version 进行版本控制的最简单方法。招摇也更多simple to setup。这种策略称为通过内容协商进行版本控制。【参考方案6】:

我已经尝试使用 URI Versioning 对我的 API 进行版本控制,例如:

/api/v1/orders
/api/v2/orders

但在尝试完成这项工作时存在一些挑战:如何使用不同版本组织代码?如何同时管理两个(或更多)版本?删除某些版本有什么影响?

我发现最好的替代方法不是对整个 API 进行版本控制,而是控制每个端点上的版本。这种模式称为Versioning using Accept header 或Versioning through content negotiation:

这种方法允许我们对单个资源表示进行版本化 而不是对整个 API 进行版本控制,这为我们提供了更细粒度的 控制版本控制。它还创造了一个更小的足迹 代码库,因为我们不必分叉整个应用程序 创建一个新版本。这种方法的另一个优点是它 不需要实现由引入的 URI 路由规则 通过 URI 路径进行版本控制。

在 Spring 上的实现

首先,您创建一个具有produces 属性的控制器,该属性将默认应用于同一类中的每个端点。

@RestController
@RequestMapping(value = "/api/orders/", produces = "application/vnd.company.etc.v1+json")
public class OrderController 


之后,我们可以想象一个可能的场景,您有两个版本(v1v2)用于“创建订单”的端点:

@Deprecated
@PostMapping
public ResponseEntity<OrderResponse> createV1(
        @RequestBody OrderRequest orderRequest) 

    OrderResponse response = createOrderService.createOrder(orderRequest);
    return new ResponseEntity<>(response, HttpStatus.CREATED);


@PostMapping(
        produces = "application/vnd.company.etc.v2+json",
        consumes = "application/vnd.company.etc.v2+json")
public ResponseEntity<OrderResponseV2> createV2(
        @RequestBody OrderRequestV2 orderRequest) 

    OrderResponse response = createOrderService.createOrder(orderRequest);
    return new ResponseEntity<>(response, HttpStatus.CREATED);

完成!只需使用所需的 Http Header 版本调用每个端点:

Content-Type: application/vnd.company.etc.v1+json

或者,调用 v2:

Content-Type: application/vnd.company.etc.v2+json

关于您的担忧:

由于并非 API 中的所有方法都在同一个版本中更改,因此我 不想去我的每个控制器并改变任何东西 版本之间没有变化的处理程序

如前所述,此策略使用其实际版本维护每个控制器和端点。您只修改已修改且需要新版本的端点。

大摇大摆呢?

使用此策略设置不同版本的 Swagger 也很容易。 See this answer 了解更多详情。

【讨论】:

【参考方案7】:

在产生中你可以有否定。所以对于method1说produces="!...1.7"并且在method2中是肯定的。

produces 也是一个数组,所以对于 method1,你可以说 produces="...1.6","!...1.7","...1.8" 等(接受除 1.7 之外的所有内容)

当然不如您想象的范围那么理想,但我认为如果这在您的系统中并不常见,那么维护起来比其他自定义内容更容易。祝你好运!

【讨论】:

感谢codesalsa,我正在努力寻找一种易于维护的方法,并且每次我们需要升级版本时都不需要更新每个端点。【参考方案8】:

如果只使用继承来建模版本控制呢?这就是我在我的项目中使用的,它不需要特殊的弹簧配置,并且得到我想要的。

@RestController
@RequestMapping(value = "/test/1")
@Deprecated
public class Test1 
...Fields Getters Setters...
    @RequestMapping(method = RequestMethod.GET)
    @Deprecated
    public Test getTest(Long id) 
        return serviceClass.getTestById(id);
    
    @RequestMapping(method = RequestMethod.PUT)
    public Test getTest(Test test) 
        return serviceClass.updateTest(test);
    



@RestController
@RequestMapping(value = "/test/2")
public class Test2 extends Test1 
...Fields Getters Setters...
    @Override
    @RequestMapping(method = RequestMethod.GET)
    public Test getTest(Long id) 
        return serviceClass.getAUpdated(id);
    

    @RequestMapping(method = RequestMethod.DELETE)
    public Test deleteTest(Long id) 
        return serviceClass.deleteTestById(id);
    

这种设置允许很少的代码重复,并且能够以很少的工作将方法覆盖到新版本的 api 中。它还省去了使用版本切换逻辑使源代码复杂化的需要。如果您不在版本中编写端点代码,它将默认获取以前的版本。

与其他人所做的相比,这似乎更容易。有什么我想念的吗?

【讨论】:

+1 用于共享代码。然而,继承将它紧紧地耦合在一起。反而。控制器(Test1 和 Test2)应该只是通过......没有逻辑实现。一切逻辑都应该在服务类 someService 中。在这种情况下,只需使用简单的组合,永远不要从其他控制器继承 @dan-hunex 看来 Ceekay 使用继承来管理不同版本的 api。如果删除继承,解决方案是什么?为什么在这个例子中紧耦合是一个问题?在我看来,Test2 扩展了 Test1,因为它是对它的改进(具有相同的角色和相同的职责),不是吗?【参考方案9】:

你可以使用AOP,围绕拦截

考虑拥有一个接收所有/**/public_api/* 的请求映射,并且在此方法中什么都不做;

@RequestMapping(
    "/**/public_api/*"
)
public void method2(Model model)

之后

@Override
public void around(Method method, Object[] args, Object target)
    throws Throwable 
       // look for the requested version from model parameter, call it desired range
       // check the target object for @VersionRange annotation with reflection and acquire version ranges, call the function if it is in the desired range



唯一的限制是所有的都必须在同一个控制器中。

有关 AOP 配置,请查看http://www.mkyong.com/spring/spring-aop-examples-advice/

【讨论】:

感谢 hevi,我正在寻找一种更“弹簧”友好的方式来执行此操作,因为 Spring 已经在不使用 AOP 的情况下选择了要调用的方法。我认为 AOP 增加了我希望避免的新级别的代码复杂性。 @Augusto,Spring 有很好的 AOP 支持。你应该试试看。 :)

以上是关于如何使用 spring 管理 REST API 版本控制?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 rest-api 执行 Spring Cloud 任务

如何快速构建基于Spring4.0的Rest API

Api管理工具(spring-rest-docs)

为啥我登录后无法进入管理 REST API 请求映射 - Spring Boot

如何使用 WebClient 使用响应式 Spring Rest API

如何在 Spring-Data-Rest 中实现细粒度的访问控制?