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

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类的替换以支持它。

你能提出什么建议吗?

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

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

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

一个想法是添加一个filter,专门为调度“规范化”您的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标头。 如果没有,那么您可以委派早期的filter在出路时为您重写该部分。

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

编辑以回应评论:

是的,基本上,您希望JAX-RS根据Path和Accept类型分派到正确的类。 JAX-RS不太可能开箱即用,因为它有点边缘。 我没有查看任何JAX-RS实现,但您可以通过调整基础结构级别之一来执行您想要的操作。

可能另一个侵入性较小的选择是使用来自Apache世界的古老技巧,并简单地创建一个基于Accept标头重写路径的filter。

所以,当系统获得:

 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可以正确地调度您的类。 唯一的另一个问题是,如果您的类看起来会看到修改后的路径,而不是原始路径(但是如果您愿意,您的filter可以将请求中的内容作为参考)。

它并不理想,但它(大多数)是免费的。

这是一个现有的filter,可能会做你想做的事情,如果没有它也许可以作为你自己做的灵感。

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

Content-Type:application / vnd.COMPANY.systeminfo- {version} + json

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

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


[编辑]:进一步深入研究CXF文档 ,可以@Consumes@Produces注释都被认为是进行选择的轴。 如果您希望有两种方法来处理不同媒体类型的响应生成,那么您肯定可以。 (如果您使用自定义类型,则必须添加序列化和/或反序列化提供程序,但您可以将大部分工作委派给标准提供程序。)我仍然要提醒您应该仍然要确保路径指示的资源在两种情况下都应该相同; 否则不是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处理)或者在一个公共的公共方法中您通过inheritance或dependency injection访问的服务层。 通过使用具有不同返回类型的两种不同方法,可以确保返回的结构对于不同版本的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 ; }