JAX-RS 2.0 REST Client Features by Example

December 30th, 2013 by

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 easen 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://bitbucket.org/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 Bitbucket repository, fork it there or clone it using Git:

git clone https://bitbucket.org/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 Bitbucket.org:

https://bitbucket.org/hascode/jaxrs2-client-tutorial/downloads

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

Tags: , , , , , , , , , ,

Leave a Reply

Please leave these two fields as-is:

Protected by Invisible Defender. Showed 403 to 81,009 bad guys.

Search
Categories