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


118

我一直在搜索如何使用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中这是不可能的,因为这两种方法具有相同的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已经有很多逻辑来选择要调用的控制器方法,我想在此基础上。


编辑2

我已经在github中共享了原始POC(经过一些改进):https : //github.com/augusto/restVersioning



1
@flup我不明白您的评论。这就说明您可以使用标头,并且正如我所说,spring提供的即用型功能不足以支持不断更新的API。更糟糕的是,该答案上的链接使用URL中的版本。
2013年

也许不是您想要的东西,但是Spring 3.2在RequestMapping上支持“ produces”参数。一个警告是版本列表必须明确。例如,produces={"application/json-1.0", "application/json-1.1"}等等
bimsapi

1
我们需要支持API的多个版本,这些差异通常是较小的更改,这会使来自某些客户端的某些调用不兼容(如果我们需要支持4个较小的版本(其中某些端点不兼容),这并不奇怪)。我很高兴将其放在url中的建议,但是我们知道这是朝错误的方向迈出了一步,因为我们有几个应用程序具有URL中的版本,并且每次我们需要碰触时都会涉及很多工作版。
Augusto

1
@Augusto,您实际上还没有。只需设计您的API即可更改不会破坏向后兼容性的方式。仅举一个破坏兼容性的更改示例,我向您展示如何以不中断的方式进行这些更改。
阿列克谢·安德烈耶夫

Answers:


62

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

我该如何做一个自定义请求映射,以对请求中的标头值进行任意评估,而无需在方法主体中进行评估?

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

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

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


感谢您添加您的评论作为答案。到目前为止是最好的之一。我已经根据您提到的链接实现了该解决方案,而且还不错。最大的问题将在升级到新版本的Spring时显现出来,因为它将需要检查对背后逻辑的任何更改mvc:annotation-driven。希望Spring提供mvc:annotation-driven一个可以定义自定义条件的版本。
奥古斯托

@奥古斯托,半年后,这对你有什么影响?另外,我很好奇,您真的按方法进行版本控制吗?在这一点上,我想知道是否在每个类/每个控制器级别的粒度上更清晰地显示版本?
Sander Verhagen 2014年

1
@SanderVerhagen可以正常工作,但是我们对整个API进行版本控制,而不是对每个方法或控制器进行版本控制(该API很小,因为它专注于业务的一个方面)。我们确实有一个更大的项目,他们选择每个资源使用不同的版本,并在URL上指定(因此,您可以在/ v1 / sessions上拥有一个终结点,而在完全不同的版本上拥有另一个资源,例如/ v4 / orders) ...稍微灵活一些,但是给客户端带来更大的压力,让他们知道要调用每个端点的版本。
2014年

1
不幸的是,这在Swagger中效果不佳,因为扩展WebMvcConfigurationSupport时许多自动配置已关闭。
瑞克

我尝试了此解决方案,但实际上它不适用于2.3.2.RELEASE。您有一些示例项目要显示吗?
帕特里克

54

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

4
我将int []更改为String []以允许使用“ 1.2”之类的版本,因此我可以处理诸如“ latest”之类的关键字
Maelig

3
是的,这很合理。对于将来的项目,出于某些原因,我将采用其他方法:1. URL代表资源。/v1/aResource/v2/aResource看起来像不同的资源,它是相同的资源只是一个不同的表现!2.使用HTTP标头看起来更好,但是您不能给某人一个URL,因为该URL不包含标头。3.使用URL参数,即/aResource?v=2.1(顺便说一句:这就是Google进行版本控制的方式)。...我仍然不确定是否要使用选项23,但出于上述原因,我将不再使用1
本杰明·M

5
如果您想将自己的内容RequestMappingHandlerMapping注入到自己的内容中WebMvcConfiguration,则应改写createRequestMappingHandlerMapping而不是requestMappingHandlerMapping!否则,您会遇到奇怪的问题(由于关闭的会话,我突然在休眠模式下遇到了Hibernates延迟初始化的问题)
stuXnet

1
该方法看起来不错,但在某种程度上似乎不适用于junti测试用例(SpringRunner)。您有机会使用测试用例的任何机会
JDev

1
有一种方法可以使这项工作有效,不要扩展WebMvcConfigurationSupport 而是要扩展DelegatingWebMvcConfiguration。这为我工作(见stackoverflow.com/questions/22267191/...
SeB.Fr

17

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

为了处理客户端升级(您在注释中提到),您可以使用“最新”之类的别名。或拥有使用最新版本的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的控制器。

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


14

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

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

  • 基于路径的方法,其中客户端在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
  • 自定义标头,其中客户端在自定义标头中定义版本。

问题第一个方法是,如果你改变的版本,让我们从V1说- > V2,也许你需要复制,粘贴并没有改变,以V2路径V1资源

问题第二个做法是,一些工具,如http://swagger.io/可以用相同的路径,但不同的内容类型(支票发放操作之间没有明显的https://github.com/OAI/OpenAPI-Specification/issues/ 146

解决方案

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

假设我们有User控制器的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为用户资源我必须采取“用户V1” repsonse,否则如果我请求V2V3等我必须采取“用户V2”响应。

在此处输入图片说明

为了在春季实现此功能,我们需要覆盖默认的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,直到找到该资源的最新版本

为了看到此实现的好处,我们有两个资源:User和Company:

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创建新的(相同)端点!

其余文档 正如我在选择基于URL的版本控制方法之前所说的那样,某些工具(如swagger)不会以相同的URL记录不同的端点,但是内容类型却不同。使用此解决方案,由于具有不同的URL,因此将显示两个端点:

在此处输入图片说明

GIT

解决方案的实现位于:https : //github.com/mspapant/restVersioningExample/


9

@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


2
我知道这一点,但是正如您所指出的,在每个版本上,即使它们没有更改,我都需要转到所有控制器并添加一个版本。您提到的范围仅适用于完整类型,例如application/*,不适用于该类型的一部分。例如,以下内容在Spring中是无效的"Accept=application/vnd.company.app-1.*+json"。这与MediaType
2013年

@Augusto,您不一定需要这样做。使用这种方法,您无需版本化“ API”,而是版本化“ Endpoint”。每个端点可以具有不同的版本。对我而言,与API版本相比 这是对API 进行版本控制的最简单方法。Swagger的设置也更简单。通过内容协商,此策略称为版本控制。
Dherik '19

3

仅使用继承为模型版本控制怎么样?那就是我在项目中使用的东西,它不需要特殊的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

2

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

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

但是,在尝试使之工作时会遇到一些挑战:如何组织具有不同版本的代码?如何同时管理两个(或多个)版本?删除某些版本有什么影响?

我发现的最佳替代方法不是对整个API 进行版本控制,而是在每个端点上控制版本。这种模式称为使用接受标头的版本控制通过内容协商的版本控制

这种方法使我们可以对单个资源表示形式进行版本控制,而不是对整个API进行版本控制,这使我们可以更精细地控制版本控制。它还在代码库中创建了一个较小的占用空间,因为在创建新版本时我们不必派生整个应用程序。这种方法的另一个优点是,它不需要实现通过URI路径版本控制引入的URI路由规则。

在Spring上实现

首先,创建一个具有基本Produces属性的Controller,默认情况下,该属性将应用于该类中的每个端点。

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

}

之后,创建一个可能的方案,其中有两个版本的端点可用于创建订单:

@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

或者,将其称为第二版:

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

关于您的后顾之忧:

而且由于并非同一版本中的API中的所有方法都发生了变化,所以我不想转到每个控制器并为在版本之间未发生变化的处理程序进行任何更改

如所解释的,此策略使用其实际版本维护每个Controller和终结点。您仅修改具有修改且需要新版本的端点。

昂首阔步?

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


1

在生产中,您可以否定。因此,对于方法1来说produces="!...1.7",方法2中说的是肯定的。

Produces也是一个数组,因此对于method1,您可以说produces={"...1.6","!...1.7","...1.8"}etc(接受1.7以外的所有值)

当然,它不如您所想到的范围理想,但是如果您的系统中不常见,我认为它比其他自定义内容更易于维护。祝好运!


谢谢codesalsa,我正在尝试找到一种易于维护的方法,不需要每次更新版本时都需要对每个端点进行更新。
2013年

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已经选择了不使用AOP即可调用的方法。我认为AOP增加了我要避免的新级别的代码复杂性。
2013年

@ Augusto,Spring具有强大的AOP支持。您应该尝试一下。:)
Konstantin Yovkov
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.