【问题标题】:Easy REST resource versioning in JAX-RS based implementations?基于 JAX-RS 的实现中的简单 REST 资源版本控制?
【发布时间】:2011-06-22 21:09:41
【问题描述】:

REST 资源版本控制的最佳实践是将版本信息放入 HTTP 请求的 Accept/Content-Type 标头中,保持 URI 不变。

以下是用于检索系统信息的 REST API 请求/响应示例:

==>
GET /api/system-info HTTP/1.1
Accept: application/vnd.COMPANY.systeminfo-v1+json

<==
HTTP/1.1 200 OK
Content-Type: application/vnd.COMPANY.systeminfo-v1+json
{
  “session-count”: 19
}

注意版本是在 MIME 类型中指定的。

这是版本 2 的另一个请求/响应:

==>
GET /api/system-info HTTP/1.1
Accept: application/vnd.COMPANY.systeminfo-v2+json

<==
HTTP/1.1 200 OK
Content-Type: application/vnd.COMPANY.systeminfo-v2+json
{
  “uptime”: 234564300,
  “session-count”: 19
}

更多解释和示例请参见http://barelyenough.org/blog/tag/rest-versioning/

是否可以在基于 Java 的 JAX-RS 实现(例如 Jersey 或 Apache CXF)中轻松实现此方法?

目标是让多个@Resource 类具有相同的@Path 值,但根据MIME 类型中指定的实际版本服务请求?

我对 JAX-RS 进行了全面调查,特别是对 Jersey 进行了调查,但没有发现对此的支持。 Jersey 没有机会注册具有相同路径的两个资源。需要实现对 WebApplicationImpl 类的替换以支持它。

你能提出一些建议吗?

注意:同一资源的多个版本需要同时可用。新版本可能会引入不兼容的更改。

【问题讨论】:

  • 这绝对不是 API 版本控制的最佳实践。最佳做法是没有版本,只进行兼容的更改。在我的书中,为每个明智的客户端应该自动处理的更改(向数据添加新标签/键)人为地创建新的 MIME 类型根本不是 RESTful。
  • 嗯,并不总是可以进行兼容的更改。此外,在我的情况下,需要同时支持多个 REST 资源版本。就必须保留资源标识而言,URI 必须更改相同。新版本是资源的新表示,即新的 MIME 类型。
  • @Jochen 理想情况下,您永远不必进行版本控制。这应该是目标。但是,如果确实有必要,那么我会说这是处理它的最佳方法。
  • Darrel:这正是 IETF 规范以及许多公共 Web 服务使用 URL 的方式。这是一个明显而简单的版本控制解决方案。那里没有任何邪恶。它可能不适用于此用例,但可供其他所有人使用。
  • @StaxMan 在 MIME 类型中执行版本的好处是,您可以将 URL 传递给在 API 的较新版本上的不同客户端,它仍然可以工作。 (这只是一个明显有好处的例子)

标签: java rest versioning jersey jax-rs


【解决方案1】:

如果它们使用/产生不同的媒体类型,您应该能够使用具有相同路径的不同类。所以这应该适用于任何 jax-rs 提供者:

@Path("/api/system-info")
@Consumes("application/vnd.COMPANY.systeminfo-v1+json")
@Produces("application/vnd.COMPANY.systeminfo-v1+json")
public class SystemInfoResourceV1 {
}

@Path("/api/system-info")
@Consumes("application/vnd.COMPANY.systeminfo-v2+json")
@Produces("application/vnd.COMPANY.systeminfo-v2+json")
public class SystemInfoResourceV2 {
}

【讨论】:

    【解决方案2】:

    对于当前版本的 Jersey,我建议使用两种不同的 API 方法和两种不同的返回值来实现,这些方法会自动序列化为适用的 MIME 类型。一旦收到对不同版本 API 的请求,就可以在下面使用通用代码。

    例子:

    import javax.ws.rs.*;
    import javax.ws.rs.core.MediaType;
    
    @GET
    @Path("/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public VersionOneDTO get(@PathParam("id") final String id) {
    
        return new VersionOneDTO( ... );
    
    }
    
    @GET
    @Path("/{id}")
    @Produces("application/vnd.COMPANY.systeminfo-v2+json;qs=0.9")
    public VersionTwoDTO get_v2(@PathParam("id") final String id) {
    
        return new VersionTwoDTO( ... );
    
    }
    

    如果 get(...)get_v2(...) 方法使用通用逻辑,我建议将其放在与 API 相关(例如会话或 JWT 处理)的通用私有方法中,或者放在服务层的通用公共方法中您通过继承或依赖注入访问。通过使用具有不同返回类型的两种不同方法,您可以确保返回的结构对于不同版本的 API 是正确的类型。

    请注意,某些旧客户端可能根本没有指定 Accept 标头。这意味着他们将隐含地接受任何内容类型,从而接受任何版本的 API。在实践中,这通常不是事实。因此,您应该使用 MIME 类型的 qs 扩展名指定较新版本的 API 的权重,如上例中的 @Produces 注释所示。

    如果您使用restAssured 进行测试,它看起来像这样:

    import static com.jayway.restassured.RestAssured.get;
    import static com.jayway.restassured.RestAssured.given;
    
    @Test
    public void testGetEntityV1() {
        given()
            .header("Accept", MediaType.APPLICATION_JSON)
        .when()
            .get("/basepath/1")
        .then()
            .assertThat()
            ... // Some check that Version 1 was called
        ;
    }
    
    @Test
    public void testGetEntityV1OldClientNoAcceptHeader() {
        get("/basepath/1")
            .then()
            .assertThat()
            ... // Some check that Version 1 was called
        ;
    }
    
    @Test
    public void testGetEntityV2() {
        given()
            .header("Accept", "application/vnd.COMPANY.systeminfo-v2+json")
        .when()
            .get("/basepath/1")
        .then()
            .assertThat()
            ... // Some check that Version 2 was called
        ;
    }
    

    【讨论】:

      【解决方案3】:

      如果您使用的是 CXF,您可以use the technique specified here 构建一个新的序列化提供程序(在现有基础架构的基础上构建),该提供程序以所需的特定格式生成数据。声明其中的几个,一个用于您想要的每种特定格式,并使用 @Produces 注释让机器为您处理其余的协商,尽管支持标准 JSON 内容类型也可能是一个想法这样普通客户就可以处理它,而无需了解您的特殊性。唯一真正的问题是什么是进行序列化的最佳方式;我想你可以自己解决这个问题……


      [编辑]:进一步挖掘CXF documentation 会发现@Consumes@Produces 注释都被认为是进行选择的轴。如果你想有两种方法来处理不同媒体类型的响应,你当然可以。 (如果您使用自定义类型,则必须添加序列化和/或反序列化提供程序,但您可以将大部分工作委托给标准提供程序。)我仍然想提醒您应该仍然确保路径指示的资源在两种情况下应该相同;否则就不是 RESTful。

      【讨论】:

      • 唐纳,感谢您的回复。您已经指出了序列化提供程序的提示,它是 output 准备工作的一部分。更重要的问题是如何使进程输入以及如何在同一路径下注册多个Resource类。
      • @Volodymyr:在同一个路径下注册几个资源类?那肯定不能是 RESTful 的!关键是您要公开同一底层资源的许多视图。那些不同的 JSON 表示必须只是查看一件事的不同方式。 (序列化器必须学会如何构建不同的视图,但这就是你达到这种复杂性所得到的。)
      • 对于输入(即@Consumes 注释),您只需要处理您所获得的内容。如果你把他们发回的东西扔在他们脸上,客户往往会讨厌它(当我有这样做的代码时,这对我正在编写配套客户端库的同事造成了很大的困扰……)
      • 资源应该是一样的,没错。我没有违反 REST。在我的例子中,每个 Resource 类都旨在提供不同的表示。在这种情况下,版本控制与表示相关联。但是新的表示(资源类)可以来自 OSGi 包。甚至类名也可能相同。这是一个非常动态的系统。我不想围绕它介绍我自己的框架,因为利用 JAX-RS 非常有用。
      【解决方案4】:

      JAX-RS 通过 Accept 标头分派给带有 @Produces 注释的方法。因此,如果您希望 JAX-RS 进行分派,则需要利用此机制。如果没有任何额外的工作,您将不得不为您希望支持的每种媒体类型创建一个方法(和提供程序)。

      没有什么可以阻止您使用基于媒体类型的几种方法,这些方法都调用一个通用方法来完成这项工作,但您必须在每次添加新媒体类型时更新它并添加代码。

      一个想法是添加一个过滤器,专门为调度“规范化”您的 Accept 标头。也就是说,也许,采取你的:

      Accept: application/vnd.COMPANY.systeminfo-v1+json
      

      并将其转换为:

      Accept: application/vnd.COMPANY.systeminfo+json
      

      同时,您提取版本信息以供以后使用(可能在请求中,或其他一些特殊机制中)。

      然后,JAX-RS 将分派给处理“application/vnd.COMPANY.systeminfo+json”的单一方法。

      然后,该方法采用“带外”版本信息来处理处理中的细节(例如选择适当的类以通过 OSGi 加载)。

      接下来,您将创建一个带有适当 MessageBodyWriter 的 Provider。 JAX-RS 将为 application/vnd.COMPANY.systeminfo+json 媒体类型选择提供程序。由您的 MBW 确定实际的媒体类型(再次基于该版本信息)并创建正确的输出格式(同样,可能分派到正确的 OSGi 加载类)。

      我不知道 MBW 是否可以覆盖 Content-Type 标头。如果没有,那么您可以委托较早的过滤器在退出时为您重写该部分。

      这有点令人费解,但如果您想利用 JAX-RS 调度,而不是为您的媒体类型的每个版本创建方法,那么这是一个可行的途径。

      根据评论进行编辑:

      是的,本质上,您希望 JAX-RS 根据 Path 和 Accept 类型分派到正确的类。 JAX-RS 不太可能开箱即用,因为它有点边缘情况。我没有看过任何 JAX-RS 实现,但是您可以通过在基础架构级别调整其中一个来做您想做的事情。

      另一个侵入性较小的选项可能是使用 Apache 世界中的一个古老技巧,并简单地创建一个过滤器,根据 Accept 标头重写您的路径。

      所以,当系统得到:

      GET /resource
      Accept: application/vnd.COMPANY.systeminfo-v1+json
      

      你把它改写成:

      GET /resource-v1
      Accept: application/vnd.COMPANY.systeminfo-v1+json
      

      然后,在您的 JAX-RS 类中:

      @Path("resource-v1")
      @Produces("application/vnd.COMPANY.systeminfo-v1+json")
      public class ResourceV1 {
          ...
      }
      

      因此,您的客户端会获得正确的视图,但您的类会被 JAX-RS 正确分派。唯一的另一个问题是,如果您的类看起来会看到修改后的路径,而不是原始路径(但如果您愿意,您的过滤器可以将其作为参考填充到请求中)。

      这并不理想,但它(大部分)是免费的。

      This 是一个现有的过滤器,它可能会做你想做的事情,如果不是,它可能会成为你自己做的灵感。

      【讨论】:

      • 非常感谢您的回答。这越来越接近我的需要。转换 MIME 将完成这项工作。但是我不希望 Resource 方法处理版本信息。整个 Resource 类应该代表特定的 Resource 版本,并且运行时中会有几个 Resource 类,例如捆绑 1.0 中的 RestService 和捆绑 2.0 中的 RestService,都具有 @Path('/rest')。如果可能,我想指示 Jersey 在不重写 WebApplicationImpl 的情况下区分两者。 (如果不可能,那么我将继续重写/扩展它)
      • 这可能是迄今为止最好的答案。理想情况下,我的类的@Path、@Produces 和资源类名称中不会有版本,因为版本应该取自 OSGi 包版本说明符。但同样,这是理想的情况。你在这里给了你非常有用的提示。谢谢!
      【解决方案5】:

      一种可能的解决方案是使用@Path 和

      内容类型: 应用程序/vnd.COMPANY.systeminfo-{版本}+json

      然后,在给定@Path 的方法中,您可以调用WebService 的版本

      【讨论】:

      • 问题是我不想在方法实现中移动特定于版本的逻辑。我希望在外面处理。
      • 无论哪种方式,您都必须“创建一个图层以选择要调用的方法”。您提议此层的方式将根据 @Consumes 内容调用特定方法。我发布的解决方案将只有一个 @Consumes 并选择在其中调用的方法。对我来说,它是一样的,一种处理注释和复制方法的方式。复制方法调用的另一种方式。
      • 嗯,问题是必须有多个资源类具有相同的@Path。所有这些类都应该由不同版本的 OSGi 包提供,它们都可能是活动的。资源类不能作为版本选择器逻辑的入口点。
      • 我认为对不兼容的版本使用相同的 URI(路径)基本上是个坏主意;即使这似乎是更方便的做事方式。与其试图解决这个问题,也许您可​​以重构一些东西以减少重复,并且只需将入口方法视为简单的包装器,委托给共享功能。
      • @StaxMan 不幸的是,委托给共享功能的简单包装器无法解决这里的问题
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-01-27
      • 1970-01-01
      • 1970-01-01
      • 2013-05-20
      • 1970-01-01
      • 1970-01-01
      • 2016-06-08
      相关资源
      最近更新 更多