I very much like the way in which Jackson and Jersey interact to make building a RESTful interface with objects transported as JSON really, really simple.
As an example, if we have on the server side a class like this:
@Path("/things")
public final class ThingService {
@GET
@Path("/thing")
@Produces(MediaType.APPLICATION_JSON)
public final Thing getThing(@PathParam("thingId") final int thingId) {
return dataLayer.fetchThingById(thingId);
}
}
then consuming the service is joyfully simple (note that this is a slightly fudged about example, and in reality more sophisticated construction of the Client instances would be recommended)
public final class ThingClient {
private final transient Client client;
private final transient WebTarget baseTarget;
public ThingClient(final String serviceUrl) {
ClientConfig cc = new ClientConfig().register(new JacksonFeature());
client = ClientBuilder.newClient(cc);
baseTarget = client.target(serviceUrl);
}
public final Thing getThing(final int thingId) {
return WebTarget target = baseTarget.path("thing")
.path(Integer.toString(thingId))
.request(MediaType.APPLICATION_JSON)
.get()
.readEntity(Thing.class);
}
}
Of course, it’s very likely your service will have a bunch of different end points, so you’ll want to pull some of the repeated boiler plate out into separate methods, perhaps something like this (where goodStatus checks that we’ve got some 2xx response from the server, and parseResponse constructs a suitable exception to throw if we got an error response):
public final class ThingClient {
private final transient Client client;
private final transient WebTarget baseTarget;
public ThingClient(final String serviceUrl) {
ClientConfig cc = new ClientConfig().register(new JacksonFeature());
client = ClientBuilder.newClient(cc);
baseTarget = client.target(serviceUrl);
}
public final Thing getThing(final int thingId) {
WebTarget target = baseTarget
.path("thing")
.path(Integer.toString(thingId));
return fetchObjectFromTarget(Thing.class, target);
}
public final OtherThing getOtherThing(final int otherId) {
WebTarget target = baseTarget
.path("otherThing")
.path(Integer.toString(otherId));
return fetchObjectFromTarget(OtherThing.class, target);
}
private <T> T fetchObjectFromTarget(final Class<T> returnType, final WebTarget target) {
Response response = fetchResponse(resourceWebTarget);
if (goodStatus(response)) {
return response.readEntity(returnType);
} else {
throw parseResponse(response);
}
}
private Response fetchResponse(final WebTarget target) {
return target.request(MediaType.APPLICATION_JSON).get();
}
}
This allows us to have a nice consonance between the client and the server, and you can even muck about and ensure the two are kept in line by deriving them from the same interface or base classes.
The one annoyance in this picture is really a matter of documentation. How do you consume a collection of objects?
Declaring the collection service is equally trivial
@Path("/things")
public final class ThingService {
@GET
@Path("/thing")
@Produces(MediaType.APPLICATION_JSON)
public final Thing getThing(@PathParam("thingId") final int thingId) {
return dataLayer.fetchThingById(thingId);
}
@GET
@Path("/all")
@Produces(MediaType.APPLICATION_JSON)
public final List<Thing> getAllThings() {
return dataLayer.fetchThings();
}
}
however the Jersey documentation is… opaque… when it comes to consuming this on the client side. It turns out that this is where the GenericType comes into play (at line 26)
public final class ThingClient {
private final transient Client client;
private final transient WebTarget baseTarget;
public ThingClient(final String serviceUrl) {
ClientConfig cc = new ClientConfig().register(new JacksonFeature());
client = ClientBuilder.newClient(cc);
baseTarget = client.target(serviceUrl);
}
public final Thing getThing(final int thingId) {
WebTarget target = baseTarget.path("thing").path(Integer.toString(thingId));
return fetchObjectFromTarget(Thing.class, target);
}
public final List<Thing> getThings() {
WebTarget target = baseTarget.path("all");
return fetchListFromTarget(Thing.class, target);
}
private <T> List<T> fetchListFromTarget(final Class<T> returnType, final WebTarget target) {
Response response = fetchResponse(resourceWebTarget);
if (goodStatus(response)) {
return response.readEntity(new GenericType<List<T>>() {});
} else {
throw parseResponse(response);
}
}
private <T> T fetchObjectFromTarget(final Class<T> returnType,
final WebTarget target) {
Response response = fetchResponse(resourceWebTarget);
if (goodStatus(response)) {
return response.readEntity(returnType);
} else {
throw parseResponse(response);
}
}
private Response fetchResponse(final WebTarget target) {
return target.request(MediaType.APPLICATION_JSON).get();
}
}
The documentation for GenericType is not great, but essentially it indicates an automagic wrap and unwrap of the collection.
(By the way, a tip of the hat to John Yeary for identifying this solution a few years ago).