基于JAX-RS的实现中的简单REST资源版本控制?

时间:2011-02-07 16:58:52

标签: java rest versioning jersey jax-rs

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,特别是泽西,并且没有找到支持。泽西岛没有机会以相同的路径注册两个资源。需要实现WebApplicationImpl类的替换以支持它。

你能提出一些建议吗?

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

5 个答案:

答案 0 :(得分:6)

JAX-RS通过Accept标头调度到使用@Produces注释的方法。因此,如果您希望JAX-RS进行调度,则需要利用此机制。如果没有任何额外的工作,您必须为您希望支持的每种媒体类型创建一个方法(和Provider)。

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

一个想法是添加一个过滤器,专门为调度“规范化”您的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是一个现有的过滤器,它可以做你想做的事情,如果不是它也许可以作为你自己做的灵感。

答案 1 :(得分:1)

对于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
    ;
}

答案 2 :(得分:0)

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

  

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

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

答案 3 :(得分:0)

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


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

答案 4 :(得分:0)

如果它们使用/产生不同的媒体类型,则应该能够在同一路径下使用不同的类。因此,这应该适用于任何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 {
}