Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Easy REST resource versioning in JAX-RS based implementations?

Best practice for REST resource versioning is putting version information into Accept/Content-Type headers of HTTP request leaving URI intact.

Here is the sample request/response to REST API for retrieving system information:

==>
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
}

Pay attention that version is specified in MIME type.

Here is another request/response for version 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
}

See http://barelyenough.org/blog/tag/rest-versioning/ for more explanation and examples.

Is it possible to implement this approach easily in Java-targeted JAX-RS based implementations, such as Jersey or Apache CXF?

The goal is to have several @Resource classes with the same @Path value, but serving the request based on actual version specified in MIME type?

I've looked into JAX-RS in general and Jersey in particlaur and found no support for that. Jersey doesn't give a chance to register two resources with the same path. Replacement for WebApplicationImpl class needs to implemented to support that.

Can you suggest something?

NOTE: It is required for multiple versions of the same resource needs to be available simultaneously. New versions may introduce incompatibale changes.

like image 774
Volodymyr Tsukur Avatar asked Feb 07 '11 16:02

Volodymyr Tsukur


2 Answers

JAX-RS dispatches to methods annotated with @Produces via the Accept header. So, if you want JAX-RS to do your dispatching, you'll need to leverage this mechanism. Without any extra work, you would have to create a method (and Provider) for every media type you wish to support.

There's nothing stopping you from having several methods based on media type that all call a common method to do that work, but you'd have to update that and add code every time you added a new media type.

One idea is to add a filter that "normalizes" your Accept header specifically for dispatch. That is, perhaps, taking your:

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

And converting that to, simply:

Accept: application/vnd.COMPANY.systeminfo+json

At the same time, you extract the version information for later use (perhaps in the request, or some other ad hoc mechanism).

Then, JAX-RS will dispatch to the single method that handles "application/vnd.COMPANY.systeminfo+json".

THAT method then takes the "out of band" versioning information to handle details in processing (such as selecting the proper class to load via OSGi).

Next, you then create a Provider with an appropriate MessageBodyWriter. The provider will be selected by JAX-RS for the application/vnd.COMPANY.systeminfo+json media type. It will be up to your MBW to figure out the actual media type (based again on that version information) and to create the proper output format (again, perhaps dispatching to the correct OSGi loaded class).

I don't know if an MBW can overwrite the Content-Type header or not. If not, then you can delegate the earlier filter to rewrite that part for you on the way out.

It's a little convoluted, but if you want to leverage JAX-RS dispatch, and not create methods for every version of your media type, then this is a possible path to do that.

Edit in response to comment:

Yea, essentially, you want JAX-RS to dispatch to the proper class based on both Path and Accept type. It is unlikely that JAX-RS will do this out of the box, as it's a bit of an edge case. I have not looked at any of the JAX-RS implementations, but you may be able to do what you want by tweaking one of the at the infrastructure level.

Possibly another less invasive option is to use an age old trick from the Apache world, and simply create a filter that rewrites your path based on the Accept header.

So, when the system gets:

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

You rewrite it to:

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

Then, in your JAX-RS class:

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

So, your clients get the correct view, but your classes get dispatched properly by JAX-RS. The only other issue is that your classes, if they look, will see the modified Path, not the original path (but your filter can stuff that in the request as a reference if you like).

It's not ideal, but it's (mostly) free.

This is an existing filter that might do what you want to do, if not it perhaps can act as an inspiration for you to do it yourself.

like image 176
Will Hartung Avatar answered Oct 07 '22 05:10

Will Hartung


With current version of Jersey, I would suggest an implementation with two different API methods and two different return values that are automatically serialised to the applicable MIME type. Once the requests to the different versions of the API are received, common code can be used underneath.

Example:

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( ... );

}

If method get(...) and get_v2(...) use common logic, I would suggest to put that in a common private method if it's API related (such as session or JWT handling) or else in a common public method of a Service Layer that you access via inheritance or Dependency Injection. By having two different methods with different return types, you ensure that the structure returned is of correct type for the different versions of the API.

Note that some old client may not specify Accept header at all. That means implicitly that they would accept any content type, thus any version of your API. In practice, this is most often not the truth. For this reason you should specify a weight to newer versions of the API using the qs extension of the MIME type as shown in the @Produces annotation in the example above.

If you are testing with restAssured it would look something like this:

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
    ;
}
like image 27
Andreas Lundgren Avatar answered Oct 07 '22 05:10

Andreas Lundgren