JAX-RS 2.0 aka JSR 339 not also specifies the API to build up a RESTful webservice but also enhances the client side API to ease up the process of writing a client for a REST service.
In the following tutorial we’re building up a client for a ready-to-play REST service and explore the different new options e.g. how to handle requests in a synchronous or asynchronous way, how to add callback handlers for a request, how to specify invocation targets to build up requests for a later execution or how to filter the client-server communication using client request filters and client response filters.
Ready to Play
For the of you who would like to reproduce the following client examples, I’ve added a full RESTful webservice runnable via Maven and an embedded application server or a standalone application server instance (war-file available as download below).
To download the project and startup the REST server you may use the following command chain (downloading dependencies might take a while..):
git clone https://github.com/hascode/jaxrs2-client-tutorial.git && cd jaxrs2-client-tutorial && make rest-server
Now following some details regarding the REST service and the entity we’re using in the following examples – please feel free to skip to the client examples directly if you’re not interested in those details!
REST Service
This is the REST service used to run the following client examples. The injected BookRepository is simply an @Singleton, @Startup marked session bean that emulates storing/fetching book entities.
The service exports methods to save a book, delete a book, find a book by its identifier and fetch all available books.
When a book is persisted, its id is generated and set and every method in the REST-service that returns an entity or a list of entities returns JSON data.
package com.hascode.tutorial.jaxrs.server;
import java.util.List;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import com.hascode.tutorial.jaxrs.entity.Book;
@Stateless
@Path("/book")
public class BookStoreService {
@EJB
private BookRepository bookRepository;
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response saveBook(final Book book) {
Book bookPersisted = bookRepository.saveBook(book);
return Response.ok(bookPersisted).build();
}
@DELETE
@Path("/{id}")
public Response deleteBook(final @PathParam("id") String id) {
bookRepository.deleteBook(id);
return Response.ok().build();
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getAll() {
List<Book> books = bookRepository.getAll();
GenericEntity<List<Book>> bookWrapper = new GenericEntity<List<Book>>(
books) {
};
return Response.ok(bookWrapper).build();
}
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response getById(final @PathParam("id") String id) {
Book book = bookRepository.getById(id);
return Response.ok(book).build();
}
}
An additional note: I’ve modified the application server to use Jackson as JSON provider using the service discovery mechanism.
Book Entity
The following bean is used throughout the tutorial .. a book .. has an id, a title, a price and a published-date.
package com.hascode.tutorial.jaxrs.entity;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Calendar;
public class Book implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String title;
private BigDecimal price;
private Calendar published;
// getter+setter..
}
Creating and Binding a Client
We’re able to create a rest client and to bind it to a specific target URL and dedicated, parameterized paths using the following steps:
-
Obtain a client reference using the ClientBuilder: Client client = ClientBuilder.newClient();
-
Bind the target to the REST service’s URL using the target() method: client.target(“http://localhost:8080/myrestservice”);
-
Handle dynamic URL path parameters using path() and resolveTemplate(): client.target(..).path(“{id}”).resolveTemplate(“id”, someId);
-
Use the request() method to start building up the request followed by one of the methods e.g. post(), get(): client.target(..).request().get();
-
Each steps offers a variety of possible parameters and configuration options, I’ll be covering some of them like asynchronous requests, callback handler and registering filters and feature classes later.
Now let’s move on to some more concrete examples …
Client Examples
As I have put the client examples in test cases running with jUnit and Hamcrest, the following setup code is used for every following test case but ommitted in the article to keep it short.
private static final String REST_SERVICE_URL = "http://localhost:8080/tutorial/rs/book";
private static final String TITLE = "One big book";
private static final BigDecimal PRICE = new BigDecimal("20.0");
private static final GregorianCalendar PUBLISHED = new GregorianCalendar(
2013, 12, 24);
Client client = ClientBuilder.newClient().register(JacksonFeature.class);
public Book mockBook() {
Book book = new Book();
book.setTitle(TITLE);
book.setPrice(PRICE);
book.setPublished(PUBLISHED);
return book;
}
The only notable thing here is that I’ve added the Jackson framework to the client runtime and that we may obtain a client instance using the javax.ws.rs.client.ClientBuilder.
Maven Coordinates
I’ve used the following dependencies for the examples below to run:
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>2.5</version>
</dependency>
Basic Operations
In the following example we’re first sending a post request with a book entity serialized to the JSON format to save the book.
Afterwards we’re using the client’s path() and resolveTemplate() methods to match the server’s specification here to receive a single book entity by its identifier.
In the third step we receive a list of all available books____and finally we’re deleting the book.
@Test
public void crudExample() {
// 1. Save a new book
Book book = mockBook();
Book bookPersisted = client
.target(REST_SERVICE_URL)
.request()
.post(Entity.entity(book, MediaType.APPLICATION_JSON),
Book.class);
String bookId = bookPersisted.getId();
assertThat(bookId, notNullValue());
// 2. Fetch book by id
Book book2 = client.target(REST_SERVICE_URL).path("/{bookId}")
.resolveTemplate("bookId", bookId).request().get(Book.class);
assertThat(book2, notNullValue());
assertThat(book2.getTitle(), equalTo(TITLE));
assertThat(book2.getPrice(), equalTo(PRICE));
assertThat(book2.getPublished().getTime(), equalTo(PUBLISHED.getTime()));
// 3. Fetch all books
GenericType<List<Book>> bookType = new GenericType<List<Book>>() {
}; // generic type to wrap a generic list of books
List<Book> books = client.target(REST_SERVICE_URL).request()
.get(bookType);
assertThat(books.size(), equalTo(1));
// 4. Delete a book
client.target(REST_SERVICE_URL).path("/{bookId}")
.resolveTemplate("bookId", bookId).request().delete();
List<Book> books2 = client.target(REST_SERVICE_URL).request()
.get(bookType);
assertThat(books2.isEmpty(), equalTo(true));
}
Asynchronous Processing
Adding a simple async() to the request builder enables us to process the requests asynchronous using Java’s Future API and their possibilities.
In the following example we’re adding a book in the first client request, delete it afterwards and fetch a list of available books afterwards.
@Test
public void asyncExample() throws Exception {
Book book = mockBook();
Future<Book> fb = client
.target(REST_SERVICE_URL)
.request()
.async()
.post(Entity.entity(book, MediaType.APPLICATION_JSON),
Book.class);
Book bookPersisted = fb.get();
String bookId = bookPersisted.getId();
assertThat(bookId, notNullValue());
client.target(REST_SERVICE_URL).path("/{bookId}")
.resolveTemplate("bookId", bookId).request().async().delete()
.get();
Future<List<Book>> bookRequest = client.target(REST_SERVICE_URL)
.request().async().get(new GenericType<List<Book>>() {
});
List<Book> books2 = bookRequest.get();
assertThat(books2.isEmpty(), equalTo(true));
}
Invocation Callback
Another way to interfere with the server response during a client communication is to add an InvocationCallback handler to a request.
In the following example with the enormous indent the callback handler added to the requests prints information about the persisted book or – in the case of an error – a stacktrace.
@Test
public void invocationCallbackExample() throws Exception {
Book book = mockBook();
client.target(REST_SERVICE_URL)
.request()
.async()
.post(Entity.entity(book, MediaType.APPLICATION_JSON),
new InvocationCallback<Book>() {
@Override
public void completed(final Book bookPersisted) {
System.out.println("book saved: "
+ bookPersisted);
assertThat(bookPersisted.getId(),
notNullValue());
}
@Override
public void failed(final Throwable throwable) {
throwable.printStackTrace();
}
}).get();
client.target(REST_SERVICE_URL).request().async()
.get(new InvocationCallback<List<Book>>() {
@Override
public void completed(final List<Book> books) {
System.out.println(books.size() + " books received");
assertThat(books.size(), greaterThanOrEqualTo(1));
}
@Override
public void failed(final Throwable throwable) {
throwable.printStackTrace();
}
}).get();
}
Delayed Invocations / Request Building
The javax.ws.rs.client.Invocation class allows us to build up a request for later use – either synchronous or asynchronous.
In the following example we’re building up two invocations for later use – one to persist a book and the other to fetch all available books and we’re using both afterwards to persist a book and fetch all available books.
Use invoke() for a synchronous request and submit() for an asynchronous request – either methods may return either a javax.ws.rs.core.Response or the desired entity if you give the entity’s class as method parameter.
@Test
public void requestPreparationExample() throws Exception {
Book book = mockBook();
Invocation saveBook = client.target(REST_SERVICE_URL).request()
.buildPost(Entity.entity(book, MediaType.APPLICATION_JSON));
Invocation listBooks = client.target(REST_SERVICE_URL).request()
.buildGet();
Response response = saveBook.invoke();
Book b1 = response.readEntity(Book.class);
// alternative: Book b1 = saveBook.invoke(Book.class);
assertThat(b1.getId(), notNullValue());
// async invocation
Future<List<Book>> b = listBooks.submit(new GenericType<List<Book>>() {
});
List<Book> books = b.get();
assertThat(books.size(), greaterThanOrEqualTo(2));
}
Client Request Filter
JAX-RS allows us to intercept outgoing requests between client and server using a request filter.
We simply need to write an implementation of the interface javax.ws.rs.client.ClientRequestFilter and when creating the client, register this class using the client’s register() method.
The javax.ws.rs.client.ClientRequestContext object grants access to the contextual information we need here.
This is our client request filters that modifies the price of book entities for all outgoing requests with a POST operation and an entity present (non-ideal production use-case ;) ) by adjusting it to the tax rate.
package com.hascode.tutorial.client;
import java.io.IOException;
import java.math.BigDecimal;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import com.hascode.tutorial.jaxrs.entity.Book;
public class TaxAdjustmentFilter implements ClientRequestFilter {
public static final BigDecimal TAX_RATE = new BigDecimal("2.5");
@Override
public void filter(final ClientRequestContext rc) throws IOException {
String method = rc.getMethod();
if ("POST".equals(method) && rc.hasEntity()) {
Book book = (Book) rc.getEntity();
BigDecimal priceWithTaxes = book.getPrice().multiply(TAX_RATE);
book.setPrice(priceWithTaxes);
rc.setEntity(book);
}
}
}
Now in our test case we simply need to register the filter class for the client and when persisting a book we’re able to see that the price has been updated according to the tax rate.
@Test
public void clientRequestFilterExample() {
Book book = mockBook();
Client client = ClientBuilder.newClient()
.register(JacksonFeature.class)
.register(TaxAdjustmentFilter.class);
Book bookPersisted = client
.target(REST_SERVICE_URL)
.request()
.post(Entity.entity(book, MediaType.APPLICATION_JSON),
Book.class);
String bookId = bookPersisted.getId();
assertThat(bookId, notNullValue());
assertThat(bookPersisted.getPrice(),
equalTo(PRICE.multiply(TaxAdjustmentFilter.TAX_RATE)));
}
Client Response Filter
To gain control over the server’s response we’ve got a similar solution: the client response filter.
Again we simply need to implement javax.ws.rs.client.ClientResponseFilter and we’re able to modify or introspect the server’s response.
The following response filter simply prints out some HTTP headers to STDOUT:
package com.hascode.tutorial.client;
import java.io.IOException;
import java.util.List;
import java.util.Map.Entry;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientResponseContext;
import javax.ws.rs.client.ClientResponseFilter;
public class ClientResponseLoggingFilter implements ClientResponseFilter {
@Override
public void filter(final ClientRequestContext reqCtx,
final ClientResponseContext resCtx) throws IOException {
System.out.println("status: " + resCtx.getStatus());
System.out.println("date: " + resCtx.getDate());
System.out.println("last-modified: " + resCtx.getLastModified());
System.out.println("location: " + resCtx.getLocation());
System.out.println("headers:");
for (Entry<String, List<String>> header : resCtx.getHeaders()
.entrySet()) {
System.out.print("\t" + header.getKey() + " :");
for (String value : header.getValue()) {
System.out.print(value + ", ");
}
System.out.print("\n");
}
System.out.println("media-type: " + resCtx.getMediaType().getType());
}
}
Again we simply need to register our filter class for the client used:
@Test
public void clientResponseFilterExample() {
Book book = mockBook();
Client client = ClientBuilder.newClient()
.register(JacksonFeature.class)
.register(ClientResponseLoggingFilter.class);
client.target(REST_SERVICE_URL)
.request()
.post(Entity.entity(book, MediaType.APPLICATION_JSON),
Book.class);
}
Running the post request to the embedded GlassFish yields the following result:
status: 200
date: Sat Dec 28 18:50:16 CET 2013
last-modified: null
location: null
headers:
Date :Sat, 28 Dec 2013 17:50:16 GMT,
Transfer-Encoding :chunked,
Content-Type :application/json,
Server :GlassFish Server Open Source Edition 3.1,
X-Powered-By :Servlet/3.0 JSP/2.2 (GlassFish Server Open Source Edition 3.1 Java/Oracle Corporation/1.7),
media-type: application
Tutorial Sources
Please feel free to download the tutorial sources from my GitHub repository, fork it there or clone it using Git:
git clone https://github.com/hascode/jaxrs2-client-tutorial.git
REST-Server Download as war-File
If you want to use your own application server to run the RESTful webservice you may download the war-file from my repository’s download section on GitHub.org:
JAX-RS 1.0 and JAX-B
If you’re interested in examples for an older version of the specification, please feel free to have a look at my article "Creating a REST Client Step-by-Step using JAX-RS, JAX-B and Jersey".
Resources
Additional REST articles of mine
Please feel free to have a look at these tutorials of mine covering different aspects of handling or creating RESTful webservices.
-
Integrating Swagger into a Spring Boot RESTful Webservice with Springfox
-
Testing RESTful Web Services made easy using the REST-assured Framework
-
REST-assured vs Jersey-Test-Framework: Testing your RESTful Web-Services
-
Creating a REST Client Step-by-Step using JAX-RS, JAX-B and Jersey
-
Creating REST Clients for JAX-RS based Webservices with Netflix Feign
Article Updates
-
2015-08-06: Links to other REST articles of mine added.
-
2015-10-22: Link list updated.