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

150

我一直在寻找如何使用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;
}

由于这两种方法具有相同的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;
}

这样一来,我就可以在注释的“produces”部分中定义封闭或开放版本范围。我正在研究这个解决方案,但问题是我仍然需要替换一些核心的Spring MVC类(RequestMappingInfoHandlerMappingRequestMappingHandlerMappingRequestMappingInfo),我不喜欢这样做,因为这意味着每当我决定升级到新版本的Spring时都需要额外的工作。

我会感激任何想法...尤其是,任何简单易于维护的建议。


编辑

添加悬赏。为了获得悬赏,请回答上面的问题,但不要建议将这个逻辑放在控制器本身。Spring已经有很多逻辑来选择要调用哪个控制器方法,我想利用这一点。


编辑 2

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


1
@flup,我不理解你的评论。那只是说你可以使用头文件,正如我所说,Spring提供的开箱即用功能不足以支持经常更新的API。更糟糕的是,该答案中的链接使用了URL中的版本。 - Augusto
1
我们需要支持多个版本的API,这些差异通常是一些小改动,会导致某些客户端的调用不兼容(如果我们需要支持4个小版本,在其中一些端点上是不兼容的也不奇怪)。感谢将其放在URL中的建议,但我们知道这是朝错误的方向迈出的一步,因为我们有几个应用程序在URL中带有版本号,每次我们需要提高版本时都需要进行大量的工作。 - Augusto
1
@Augusto,实际上你并没有。只需要以不破坏向后兼容性的方式设计你的API更改。给我一些破坏兼容性的变化的示例,我会向你展示如何以非破坏性的方式进行这些变化。 - Alexey Andreev
1
你看过https://dev59.com/2mkv5IYBdhLWcg3w1kSr#10336769吗?它似乎暗示了你的陈述“在Spring中不可能实现,因为这两种方法具有相同的RequestMapping注释,Spring无法加载。”并不完全正确? - xwoker
1
xwoker与xworker :-) - xwoker
显示剩余8条评论
10个回答

84

无论是否可以通过进行向后兼容的更改来避免版本控制(当您受某些企业指南的约束或您的API客户端以错误方式实现并会导致破坏时,可能并非总是可能),抽象需求都是一个有趣的问题:

如何执行自定义请求映射,而无需在方法体中执行请求头的任意值的评估?

正如这个SO答案所述,您实际上可以拥有相同的@RequestMapping并使用不同的注释来区分实际运行时发生的路由。为此,您将需要:

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

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


感谢您将评论添加为答案xwoker。到目前为止,这是最好的一个。我已经根据您提到的链接实现了解决方案,效果还不错。最大的问题将在升级到新版本的Spring时显现出来,因为它需要检查mvc:annotation-driven背后逻辑的任何更改。希望Spring能够提供一个版本的mvc:annotation-driven,其中可以定义自定义条件。 - Augusto
@Augusto,半年过去了,这个方案对你来说如何运作?另外,我很好奇,你真的是按方法版本控制吗?此时此刻,我在想,按类/控制器级别的粒度进行版本控制是否更清晰明了? - Sander Verhagen
1
@SanderVerhagen 已经正常工作了,但我们对整个 API 进行版本控制,而不是每个方法或控制器(由于 API 集中在业务的一个方面,所以规模相对较小)。我们有一个相当大的项目,他们选择对每个资源使用不同的版本,并在 URL 上指定(因此您可以在 /v1/sessions 上拥有一个终端和另一个完全不同版本的资源,例如 /v4/orders)... 这样更加灵活,但它会增加客户端知道调用每个终端的哪个版本的压力。 - Augusto
3
很不幸,这与Swagger不兼容,因为在扩展WebMvcConfigurationSupport时,许多自动配置都被关闭了。 - Rick
我尝试了这个解决方案,但它实际上在2.3.2.RELEASE版本中无法工作。你有一些示例项目可以展示吗? - Patrick
抱歉,我很久没有研究过这个了。 - xwoker

71

我刚刚创建了一个自定义解决方案。我正在使用@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");
    }
}

5
我将 int[] 改为 String[],以便处理像“1.2”这样的版本,并且可以处理类似“latest”的关键字。 - Maelig
3
是的,这相当合理。对于将来的项目,我会出于以下一些原因采取不同的方式:1. URL代表资源。/v1/aResource/v2/aResource看起来像是不同的资源,但实际上它只是同一个资源的不同表示!2. 使用HTTP标头看起来更好,但你无法给别人一个URL,因为URL不包含标头。3. 使用URL参数,例如/aResource?v=2.1(顺便说一下:这是Google进行版本控制的方式)……我仍然不确定我会选择选项2还是3,但出于上述原因,我将不再使用选项1。 - Benjamin M
8
如果你想将自己的 RequestMappingHandlerMapping 注入到你的 WebMvcConfiguration 中,你应该重写 createRequestMappingHandlerMapping 而不是 requestMappingHandlerMapping!否则你会遇到奇怪的问题(我曾因为一个关闭的 session 在 Hibernate 的延迟初始化时遇到过问题)。 - stuXnet
1
这种方法看起来不错,但是似乎在junit测试案例(SpringRunner)中无法正常工作。您有没有可能已经让这种方法适用于测试案例了呢? - JDev
2
有一种方法可以使这个工作正常,不要扩展 WebMvcConfigurationSupport,而是扩展 DelegatingWebMvcConfiguration。这对我起作用了(请参见 https://dev59.com/tWEh5IYBdhLWcg3wjUKJ) - SeB.Fr
显示剩余4条评论

21
I have implemented a solution which can handle the problem of rest versioning perfectly.
Generally speaking, there are three major approaches to rest versioning.
  • Path-based approch, in which the client defines the version in URL:

    http://localhost:9001/api/v1/user
    http://localhost:9001/api/v2/user
    
  • Content-Type header, in which the client defines the version in Accept header:

    http://localhost:9001/api/v1/user with 
    Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json
    
  • Custom Header, in which the client defines the version in a custom header.

第一种方法的问题在于,如果你从v1更改到v2版本,可能需要将未更改的v1资源复制粘贴到v2路径中。

第二种方法的问题在于,一些工具(例如http://swagger.io/)无法区分具有相同路径但不同Content-Type的操作(请查看https://github.com/OAI/OpenAPI-Specification/issues/146)。

解决方案

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

假设我们为用户控制器有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”响应;否则,如果我请求v2、v3等版本,则必须获取“User V2”响应。

enter image description here

为了在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/v{version}/user
http://localhost:9001/api/v{version}/company

假设我们在公司“合同”中进行了更改,这会破坏客户。因此,我们实施了http://localhost:9001/api/v2/company,并要求客户端改为使用v2而不是v1。
因此,客户端的新请求如下:
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用户创建一个新的(相同的)端点!

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

enter image description here

GIT

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


1
我很乐意听取那些尝试过这个解决方案的人的反馈。 :) - payne
1
注意:如果您发送带有/v0或/v-1的请求,会发生无限循环。要修复它,您需要将if (previousVersion != 0) {设置为>0,并且需要在getPreviousVersion()中捕获异常并返回-1。 - Daniel Eisenreich
使用最新的Spring版本2.4.2,你需要调整创建新请求的方式为:https://gist.github.com/eisenreich/6ab40616a9e694bc2220c68ec3a01455 - Daniel Eisenreich
https://github.com/OAI/OpenAPI-Specification/issues/146 在 OpenAPI 3.x 规范中已经解决。 - Jonas Gröger

18

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

并且要处理客户端升级(你在评论中提到了),可以使用别名如“最新版”。或者有一个未经版本控制的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;
    }
}

根据最后的方法,你实际上可以实现类似于你想要的东西。

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

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


9
< p > < code > @RequestMapping < /code > 注释支持一个 < code > headers < /code > 元素,允许您缩小匹配请求的范围。特别是您可以在此处使用 < code > Accept < /code > 标头。< /p >
@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


3
我知道这个问题,但正如你所指出的,每一个版本都需要我去所有的控制器中添加一个版本,即使它们没有改变。你提到的范围只适用于完整类型,例如 application/*,而不是类型的某些部分。例如,在Spring中,以下写法是无效的:"Accept=application/vnd.company.app-1.*+json"。这与Spring类MediaType的工作方式有关。 - Augusto
@Augusto,你不一定需要这样做。采用这种方法,你并没有对“API”进行版本控制,而是对“Endpoint”进行版本控制。每个端点可以有不同的版本。对我来说,这是版本控制API最简单的方法,相比于API版本。Swagger也更加容易设置。这种策略被称为内容协商版本控制。 - Dherik

7

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

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

但是在尝试使其工作时,会遇到一些挑战:如何组织具有不同版本的代码?如何同时管理两个(或更多)版本?删除某些版本会产生什么影响?
我发现的最佳替代方案不是给整个API打上版本号,而是在每个端点上控制版本。这种模式称为使用Accept标头进行版本控制通过内容协商进行版本控制

这种方法允许我们对单个资源表示进行版本控制,而不是对整个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呢?

使用此策略设置Swagger的不同版本也非常容易。请参阅此答案 以获取更多详细信息。


4

使用继承来模拟版本控制怎么样?这正是我在项目中使用的方法,它不需要特殊的Spring配置,并且可以完美地实现我的需求。

@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
+1 分享代码是很棒的。然而,继承会紧密耦合它们。相反,控制器(Test1和Test2)应该只是一个传递...没有任何逻辑实现。所有逻辑应该放在服务类中,比如 someService。在这种情况下,只需使用简单的组合,而不要从其他控制器继承。 - Dan Hunex
1
@dan-hunex 看起来 Ceekay 使用继承来管理不同版本的 API。如果去掉继承,解决方案是什么?为什么紧密耦合在这个例子中是一个问题?在我看来,Test2 扩展 Test1 是因为它是它的改进(具有相同的角色和责任),不是吗? - jeremieca

3
在 produces 中,你可以使用否定。因此,在 method1 中使用 produces="!...1.7",在 method2 中使用正数。

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

当然,这并不像你想象中的那样理想,但我认为比其他自定义内容更易于维护,如果这对你的系统来说是罕见的问题。祝好运!


谢谢codesalsa,我正在尝试找到一种易于维护且不需要每次升级版本时更新每个端点的方法。 - Augusto

0

想要分享一下我使用Kotlin / Spring 5.3.x实现的URL版本控制,希望对某些人有所帮助。

我有一个注解类来定义起始和结束版本。如果未设置结束版本,则仅适用于单个版本。

const val VERSION_PREFIX = "v"

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class VersionedResource(
    val version: Int,
    val toVersion: Int = 0
)

fun VersionedResource.validate() {
    /* Do whatever validation you need */
}

fun VersionedResource.getRange(): IntRange {
    validate()
    return if (toVersion == 0) version..version
    else version..toVersion
}

然后,这将被一个自定义的RequestMappingHandlerMapping这样利用:

class VersionedRequestHandlerMapping : RequestMappingHandlerMapping() {

companion object {
    val logger: Logger = LoggerFactory.getLogger(this::class.java)
}

override fun getMappingForMethod(method: Method, handlerType: Class<*>): RequestMappingInfo? {
    val mappingInfo = super.getMappingForMethod(method, handlerType) ?: return null
    val versions = getAndValidateAnnotatedVersions(method, handlerType) ?: return mappingInfo

    val versionedPatterns = mutableSetOf<String>()
    mappingInfo.patternsCondition?.apply {
        patterns.forEach { path ->
            versions.forEach { version ->
                "/$VERSION_PREFIX$version$path"
                    .apply { versionedPatterns.add(this) }
                    .also { logger.debug("Generated versioned request-mapping: '$it'") }
            }
        }
    } ?: throw IllegalStateException(
        "Cannot create versioned request mapping patterns when there are no patterns from before."
    )

    return buildRequestMappingWithNewPaths(versionedPatterns, mappingInfo)
}

private fun getAndValidateAnnotatedVersions(
    method: Method,
    handlerType: Class<*>
): IntRange? {
    return (AnnotationUtils.findAnnotation(method, VersionedResource::class.java) // Prioritizes method before class
        ?: AnnotationUtils.findAnnotation(handlerType, VersionedResource::class.java))
        ?.run { getRange() }
}

private fun buildRequestMappingWithNewPaths(
    versionedPatterns: Set<String>,
    mappingInfo: RequestMappingInfo
): RequestMappingInfo {
    return RequestMappingInfo
        .paths(*versionedPatterns.toTypedArray())
        .methods(*mappingInfo.methodsCondition.methods.toTypedArray())
        .params(*mappingInfo.paramsCondition.expressions.map { it.toString() }.toTypedArray())
        .headers(*mappingInfo.headersCondition.expressions.map { it.toString() }.toTypedArray())
        .consumes(*mappingInfo.consumesCondition.expressions.map { it.toString() }.toTypedArray())
        .produces(*mappingInfo.producesCondition.expressions.map { it.toString() }.toTypedArray())
        .apply { mappingInfo.name?.let { mappingName(it) } }
        .build()
}
}

配置看起来像这样。

@Configuration
class VersionedResourceConfig : DelegatingWebMvcConfiguration() {

    @Autowired
    private lateinit var context: ApplicationContext

    @Autowired
    private lateinit var jacksonObjectMapper: ObjectMapper

    override fun createRequestMappingHandlerMapping(): RequestMappingHandlerMapping {
        return VersionedRequestHandlerMapping().apply {
            applicationContext = context
            setRemoveSemicolonContent(false)
            setDetectHandlerMethodsInAncestorContexts(true)
        }
    }

    // For some reason I needed to add this, since it was being overridden 
    override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>?>) {
        converters.add(MappingJackson2HttpMessageConverter(jacksonObjectMapper))
        super.configureMessageConverters(converters)
    }
}

现在我可以像这样定义一个有版本的资源

@RestController
@VersionedResource(version = 1, toVersion =  2)
@RequestMapping(path = ["/some/resource"]) // Should handle multiple paths as well
class SomeResource() {

    @GetMapping
    fun getSomething() {
        // Will be mapped to /v1/some/resource and /v2/some/resource
    }

    @VersionedResource(2)
    @GetMapping("stuff")
    fun getSomethingElse() {
        // Will be mapped only to /v2/some/resource/stuff (overrides class spec)
    }
}

1
只是想分享一下,您可以使用Kotlin来替代Spring。没有必要添加它所提供的所有魔法。对于Kotlin而言,有更好的替代方案,这些方案的核心是测试。请查看http4k。另外,随着时间的推移,我发现将版本号添加到URL中对开发人员来说更加简单易懂。 - Augusto

0

你可以使用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”友好的方法来完成这个任务,因为Spring已经选择了要调用哪个方法,而不需要使用AOP。在我看来,AOP增加了新的代码复杂性,我想避免它。 - Augusto
@Augusto,Spring拥有出色的AOP支持。你应该试一试。 :) - Konstantin Yovkov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接