Going async with JAX-RS
JAX-RS provides client side asynchronous behavior to complement its server side counterpart. It’s powered by the AysncInvoker interface – the best part if that you do not need to get hold of it explicitly. The fluent JAX-RS client API makes it available by including the async() call in your builder calls
e.g. ClientBuilder.newClient().target(“https://test.com”).request().async().get
Like every other asynchronous offering, the JAX-RS client API offers you a couple of options to manage async calls
- DIY (do-it-yourself) – get back a Future instance and work with it to track the result
- Declarative callbacks – let the API know what you want it to do in case of a success or failure
Declarative callbacks
A callback is required to implement the InvocationCallback interface. It defines two methods
- completed: gets called when an invocation successfully finishes. The result response is passed as a parameter to the callback method.
- failed: it is invoked in case of a failure
Here is an example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Test | |
public void testSuccess() throws InterruptedException { | |
CountDownLatch l = new CountDownLatch(1); | |
ClientBuilder.newClient().target("https://api.github.com").path("gists").path("ffd6ecff2dfb2fefcf784f114be9b9f7") | |
.request().async().get(new InvocationCallback<String>() { | |
@Override | |
public void completed(String response) { | |
Logger.getAnonymousLogger().log(Level.INFO, "Response\n{0}", response); | |
l.countDown(); | |
} | |
@Override | |
public void failed(Throwable throwable) { | |
} | |
}); | |
boolean result = l.await(5, TimeUnit.SECONDS); | |
assertTrue("completed method not called", result); | |
} |
There is more to failure scenarios than meets the eye
The invocation of the failed method happens only in case of a failure – no doubts there. But there is a caveat – the criteria is based on the generic entity type (Java class type to be specific) of the response from the server.
Here is an example for the normal scenario
When the response type is anything other than javax.ws.rs.Response
In case of a server side error (e.g. HTTP 404), the failed method is invoked
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Test | |
public void failedMethodCalledAfterServerSideFailureForStringEntity() throws InterruptedException { | |
CountDownLatch l = new CountDownLatch(1); | |
ClientBuilder.newClient().target("http://google.com").path("first").request().async().get(new InvocationCallback<String>() { | |
@Override | |
public void completed(String response) { | |
} | |
@Override | |
public void failed(Throwable throwable) { | |
Logger.getAnonymousLogger().log(Level.INFO, "Error message: {0}", throwable.getMessage()); | |
l.countDown(); | |
} | |
}); | |
boolean result = l.await(3, TimeUnit.SECONDS); | |
assertTrue("Failed method not called", result); | |
} |
What if the generic type if javax.ws.rs.Response?
In such scenario, the completed method is invoked – that’s the catch! You can always investigate the actual HTTP response by calling the getStatus method (as demonstrated in the below example)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Test | |
public void completedMethodCalledAfterServerSideFailureForGenericEntity() throws InterruptedException { | |
CountDownLatch l = new CountDownLatch(1); | |
ClientBuilder.newClient().target("http://google.com").path("first").request().async().get(new InvocationCallback<Response>() { | |
@Override | |
public void completed(Response response) { | |
Logger.getAnonymousLogger().log(Level.INFO, "HTTP Respoonse status: {0}", response.getStatus()); | |
l.countDown(); | |
} | |
@Override | |
public void failed(Throwable throwable) { | |
} | |
}); | |
boolean result = l.await(3, TimeUnit.SECONDS); | |
assertTrue("Completed method not called", result); | |
} |
At the outset, it might look odd ….
but think about it – you were expecting a response from the service and you did get one (it does not matter if it was success/failure). From the framework’s perspective, only if the API call to the endpoint failed to go through i.e. there was a client side error during invocation, should the failed method should be called. This is an important distinction/caveat which one should make a note of…
Further reading
- Check out the JAX-RS 2.0 specification doc
- My eBook: REST assured with JAX-RS
- JAX-RS 2.0 article in the Java Magazine
Cheers!