Java 基于JAX-RS的实现中的轻松REST资源版本控制?

Java 基于JAX-RS的实现中的轻松REST资源版本控制?,java,rest,versioning,jersey,jax-rs,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.systeminf

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
}
==>
获取/api/system info HTTP/1.1
接受:application/vnd.COMPANY.systeminfo-v1+json
获取/api/system info HTTP/1.1
接受:application/vnd.COMPANY.systeminfo-v2+json

一种可能的解决方案是将一个@Path与

内容类型: application/vnd.COMPANY.systeminfo-{version}+json


然后,在给定@Path的方法中,您可以通过Accept头调用WebService版本的JAX-RS分派给带有@products注释的方法。因此,如果您希望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创建一个提供者。JAX-RS将为应用程序/vnd.COMPANY.systeminfo+json媒体类型选择提供程序。由MBW确定实际的媒体类型(再次基于该版本信息)并创建正确的输出格式(再次,可能分配到正确的OSGi加载类)将取决于您的MBW

我不知道MBW是否可以覆盖内容类型标题。如果没有,那么您可以在退出时委托先前的筛选器为您重写该部分

这有点复杂,但如果您想利用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得到了正确的调度。唯一的另一个问题是,如果您的类看起来像,那么它们将看到修改后的路径,而不是原始路径(但是如果您愿意,您的过滤器可以将其作为引用填充到请求中)

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


是一个现有的筛选器,它可能会执行您想要执行的操作,如果不是,它可能会激励您自己执行此操作。

如果您使用CXF,则可以构建一个新的序列化提供程序(基于现有基础结构),以生成所需特定格式的数据。声明其中的两个,一个用于您想要的特定格式,并使用
@products
注释让机器为您处理其余的协商,尽管支持标准JSON内容类型也可能是一个想法,这样普通客户端就可以处理它,而无需探究您的特殊性。唯一真正的问题是什么是进行序列化的最佳方式;我想你可以自己弄明白



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

对于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的任何版本。实际上,这往往不是事实。因此,您应该使用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( ... ); }
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
    ;
}
@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 {
}