Three Forms of RESTEasy Client
Join the DZone community and get the full member experience.
Join For Free- JAX-RS annotated Interface proxying
- Lower level fluid HTTP API
- ClientRequestFactory - generates #1 and #2, and has a get template method
The ClientRequestFactory encapsulates RESTEasy client's most interest feature: client interceptors. Those interceptors come in three flavors:
- ClientExecutionInterceptor - intercept "execution" of a request. You can do things like client-side caching, performance montoring and request modifications with this interceptor.
- MessageBodyReaderInterceptor - intercepts the conversion process from raw bites to object
- MessageBodyWriteInterceptor - intercepts the conversion process from object to raw bites
I'm going to use the Flickr "REST" API To demonstrate the three forms of client and the interceptor framework. All (or at least most) of the code found in this blog can be downloaded (zip) from the RESTEasy SVN repo.
Arjen Poutsma wrote up a great blog post on how to use the API with Spring's RestTemplate a while back. That post can give you lots of detail on how the Flickr API works. I'm going to re-write that same example using RESTEasy's framework.
In my example, I'm going to have two types of calls to the Flickr API:
- Search for photos - "http://www.flickr.com/services/rest?method=flickr.photos.search&;api+key={api-key}&tags={tag}&per_page=8"
- Retrieve photos - "http://static.flickr.com/{server}/{id}_{secret}_m.jpg"
XML to Java Object
The search for photos returns an XML response that looks a bit like this:
<rsp stat="ok">
<photos page="1" pages="43645" perpage="8" total="349160">
<photo id="3344501816" owner="8407953@N03" secret="7849e5b9d1"
server="3546" farm="4" title="Dolphin Rider" ispublic="1"
isfriend="0" isfamily="0" />
<photo id="3280270877" owner="81035653@N00" secret="625d72bd9f"
server="3640" farm="4" title="The joy of the Mekong" ispublic="1"
isfriend="0" isfamily="0" />
<photo id="2987261000" owner="81035653@N00" secret="a66b646be5"
server="3269" farm="4" title="Mekong sunset" ispublic="1"
isfriend="0" isfamily="0" />
<photo id="2004396589" owner="81035653@N00" secret="2222cf91bb"
server="2296" farm="3" title="Portofino a village of rare beauty" ispublic="1"
isfriend="0" isfamily="0" />
<photo id="1752011018" owner="81035653@N00" secret="bef30f1360"
server="2322" farm="3" title="Dolphins spotted behind our ferry boat!" ispublic="1"
isfriend="0" isfamily="0" />
<photo id="542661469" owner="8379221@N07" secret="d1a21eb9bd"
server="1104" farm="2" title="20050815-vs-0110" ispublic="1"
isfriend="0" isfamily="0" />
<photo id="142731041" owner="89972557@N00" secret="4e9eff1694"
server="49" farm="1" title="dance of the dolphins" ispublic="1"
isfriend="0" isfamily="0" />
<photo id="142730518" owner="89972557@N00" secret="79013350b8"
server="45" farm="1" title="sandbar, unexposed" ispublic="1"
isfriend="0" isfamily="0" />
</photos>
</rsp>
All I really care about is the server, the id and the secret so that I can retrieve the photos. In RESTEasy, the easiest way to translates XML is to use JAXB. Here's how my FlickrResponse object maps to the XML above:
@XmlRootElement(name = "rsp")
public class FlickrResponse {
@XmlElementWrapper(name = "photos")
@XmlElement(name="photo")
public List photos;
}
class Photo {
@XmlAttribute
public String server, id, secret, title, owner;
public String getPublicURL() {
return UriBuilder.fromUri("http://www.flickr.com/photos/").path(
"/{owner}/{id}").build(owner, id).toString();
}
}
In ideal REST API, I wouldn't have to construct the photo URL myself, but with Flickr I had to do it. As you can see, it's not that painful to construct.
Reading a photo.
Now that I have my XML reader in place, I want to create a .jpg to ImageIcon converter. I'm going to be showing photos on a Swing application, and Swing needs an ImageIcon object. JAX-RS has a slick abstraction machanism to convert raw data into any object you'd like: MessageBodyReader and MessageBodyWriter. Since I only care about reading an image, I'm going to implement a MessageBodyReader. RESTEasy reuqires all readers and writers to be annotated with JAX-RS's @Provider annotation which tells JAX-RS systems that this is an infrastructure object.
My reader has to read in a byte array and convert it to an ImageIcon. RESTEasy has a utility method to do the byte reading and ImageIcon has a constructor that takes a byte array, so my work is pretty simple:
@Provider
@Produces("*/*")
public class ImageIconMessageBodyReader implements MessageBodyReader
{
public boolean isReadable(Class type, Type genericType,
Annotation[] annotations, MediaType mediaType)
{
return type.isAssignableFrom(ImageIcon.class);
}
public ImageIcon readFrom(Class type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap httpHeaders, InputStream entityStream)
throws IOException, WebApplicationException
{
return new ImageIcon(ReadFromStream.readFromStream(1024 * 4,
entityStream));
}
}
I wish that there was a more succinct API (perhaps using annotations) for creating this method, but for now you have to create a boat-load of method parameters to do some really straight forward work. Perhaps that can be tackled in JAX-RS 2.0
Performance Logging with RESTEasy
The next thing I'm going to tackle is a performance monitor using RESTEasy's ClientExecutionInterceptor and MessageBodyReaderInterceptor. I care about how long the execution took and how long it takes to transform a byte array into either MXL or an ImageIcon.
public class LoggingExecutionInterceptor implements ClientExecutionInterceptor,
MessageBodyReaderInterceptor
{
private final static Logger logger = LoggerFactory
.getLogger(LoggingExecutionInterceptor.class);
@SuppressWarnings("unchecked")
public ClientResponse execute(ClientExecutionContext ctx) throws Exception
{
long start = System.currentTimeMillis();
ClientResponse response = ctx.proceed();
String contentLength = (String) response.getMetadata().getFirst(
HttpHeaderNames.CONTENT_LENGTH);
logger.info(String.format("Read url %s in %d ms size %s.",
ctx.getRequest().getUri(),
System.currentTimeMillis() - start, contentLength));
return response;
}
public Object read(MessageBodyReaderContext ctx) throws IOException,
WebApplicationException
{
long start = System.currentTimeMillis();
Object result = ctx.proceed();
logger.info(String.format("Read mediaType %s as %s in %d ms.",
ctx.getMediaType().toString(), ctx.getType().getName(),
System.currentTimeMillis() - start));
return result;
}
}
I hope that this code is clear enough to get a sense of how the RESTEasy API works.
RESTEasy setup
RESTEasy requires just a bit of default set up. I also need to tell RESTEasy about the ImageIconMessageBodyReader and LoggingExecutionInterceptor.
private static ClientRequestFactory initializeRequests()
{
ResteasyProviderFactory instance = ResteasyProviderFactory.getInstance();
RegisterBuiltin.register(instance);
instance.registerProvider(ImageIconMessageBodyReader.class);
ClientRequestFactory clientRequestFactory = new ClientRequestFactory();
clientRequestFactory.getPrefixInterceptors().registerInterceptor(new LoggingExecutionInterceptor());
return clientRequestFactory;
}
Everything here should be relatively straight forward, except for getPrefixInterceptors(). Here's what's happening:
- RESTEasy registers all default @Provider classses specified in META-INF/services/javax.ws.rs.ext.Providers - including String converters and and JAXB converters
- Explicitely register the ImageIconMessageBodyReader
- Create a new ClientRequestFactory
- register my LoggingExecutionInterceptor so that any future client.get(..) calls, client.createProxy, or client.createRequest will use that interceptor
Searching for photos
I'm going to show you the three RESTEasy client types that can be used to search for photos:
We're going to use a static variable to encapsulate our URLs:
public static final String photoSearchUrl = "http://www.flickr.com/services/rest?method=flickr.photos.search&per_page=8&sort=interestingness-desc&api+key={api-key}&{type}={searchTerm}";
Note: you can apply for your own Flickr API Key.
Let's with the newfangled ClientRequestFactory to search for photos:
FlickrResponse photos = clientRequestFactory.get(photoSearchUrl,
FlickrResponse.class, apiKey, type, searchTerm);
That's all there really is to it. That uses the photoSearch URI template, gets a FlickrResponse, and fills in the template with the last three parameters.
Getting a photo
I'll demonstrate the "JAX-RS annotated Interface proxying" by showing how to retrieve a photo. Let's start with an interface:
public interface PhotoResource {
@GET
@Path("/{server}/{id}_{secret}_m.jpg")
ImageIcon read(@PathParam("server") String server,
@PathParam("id") String id, @PathParam("secret") String secret);
}
This tells RESTEasy that we'll be performing a GET, the URI template is a "/{server}/{id}_{secret}_m.jpg" and that method parameters will be used to populate the URI template's variables. Here's an example of how this interface is used:
// set the "base URI"
clientRequestFactory.setBase(new URI("http://static.flickr.com"))
...
PhotoResource photoResource = clientRequestFactory.createProxy(PhotoResource.class);
for (final Photo photo : photos.photos) {
photo.image = executor.submit(new Callable() {
public ImageIcon call() throws Exception {
ImageIcon ic = photoResource.read(photo.server, photo.id, photo.secret);
...
}
});
}
This code iterates over the photos from FlickrResponse and retrieves an ImageIcon based on the parameters passed into the proxied method.
"JAX-RS annotated Interface proxying" also allow you to perform POSTs/PUTs/DELETEs, set HTTP headers, set Cookies, set form parameters and even supply a URL. It can be used when you want to have a remove "service" interface and have complicated HTTP communication requirements.
For the sake of completeness, here's an example of using ClientRequest for those same calls:
ImageIcon ic = client.createRequest(photoUrlTemplate)
.pathParameters(photo.server, photo.id, photo.secret)
.get().getEntity(ImageIcon.class)
As you can see, ClientRequest does something similar in nature to the other two approaches, but is a bit more verbose. ClientRequests allows you to perform GET/POST/DELETE/PUT; set headers, query parameters, cookies form parameters and of course uri template parameters.
Full Flickr Application
Let's put those prevous calls into a fully functional application. I'm going to use the ClientRequestFactory.get() method for both XML and ImageIcons:
public class SimpleFlickrClient
{
public static final String photoSearchUrl =
"http://www.flickr.com/services/rest?method=flickr.photos.search&per_page=8&sort=interestingness-desc&api+key={api-key}&{type}={searchTerm}";
public static final String photoUrlTemplate =
"http://static.flickr.com/{server}/{id}_{secret}_m.jpg"
public static void main(String args[]) throws Exception
{
final String searchTerm = "dolphin";
ClientRequestFactory client = initializeRequests();
// apply for api key at - http://www.flickr.com/services/api/keys/apply
FlickrResponse photos = client.get(photoSearchUrl,
FlickrResponse.class, args[0], "text", searchTerm);
JFrame frame = new JFrame(searchTerm + " photos");
frame.setLayout(new GridLayout(2, photos.photos.size() / 2));
for (Photo photo : photos.photos)
{
JLabel image = new JLabel(client.get(photoUrlTemplate,
ImageIcon.class, photo.server, photo.id, photo.secret));
image.setBorder(BorderFactory.createTitledBorder(photo.title));
frame.add(image);
}
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
private static ClientRequestFactory initializeRequests()
{
ResteasyProviderFactory instance = ResteasyProviderFactory
.getInstance();
RegisterBuiltin.register(instance);
instance.registerProvider(ImageIconMessageBodyReader.class);
ClientRequestFactory client = new ClientRequestFactory();
client.getPrefixInterceptors().registerInterceptor(
new LoggingExecutionInterceptor());
return client;
}
}
Running this code (the fully mavenized application is downloadable) will produces the following image:
and a standard out logging that looks something like:
125 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Reading url http://www.flickr.com/services/rest?method=flickr.photos.search&per_page=8&sort=interestingness-desc&api+key=0c20edc102588ab938587bc286132b6e&text=dolphin
437 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Read url http://www.flickr.com/services/rest?method=flickr.photos.search&per_page=8&sort=interestingness-desc&api+key=0c20edc102588ab938587bc286132b6e&text=dolphin in 312 ms size 1410.
703 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Read mediaType text/xml;charset="utf-8" as org.jboss.resteasy.examples.flickr.FlickrResponse in 250 ms.
796 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Reading url http://static.flickr.com/3546/3344501816_7849e5b9d1_m.jpg
1265 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Read url http://static.flickr.com/3546/3344501816_7849e5b9d1_m.jpg in 469 ms size 22271.
1781 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Read mediaType image/jpeg as javax.swing.ImageIcon in 516 ms.
1797 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Reading url http://static.flickr.com/3640/3280270877_625d72bd9f_m.jpg
2265 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Read url http://static.flickr.com/3640/3280270877_625d72bd9f_m.jpg in 468 ms size 54941.
3031 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Read mediaType image/jpeg as javax.swing.ImageIcon in 766 ms.
3031 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Reading url http://static.flickr.com/3269/2987261000_a66b646be5_m.jpg
3453 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Read url http://static.flickr.com/3269/2987261000_a66b646be5_m.jpg in 422 ms size 25750.
3937 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Read mediaType image/jpeg as javax.swing.ImageIcon in 484 ms.
3937 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Reading url http://static.flickr.com/2296/2004396589_2222cf91bb_m.jpg
4390 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Read url http://static.flickr.com/2296/2004396589_2222cf91bb_m.jpg in 453 ms size 31783.
5000 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Read mediaType image/jpeg as javax.swing.ImageIcon in 610 ms.
5000 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Reading url http://static.flickr.com/2322/1752011018_bef30f1360_m.jpg
5422 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Read url http://static.flickr.com/2322/1752011018_bef30f1360_m.jpg in 422 ms size 28925.
6015 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Read mediaType image/jpeg as javax.swing.ImageIcon in 593 ms.
6015 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Reading url http://static.flickr.com/1104/542661469_d1a21eb9bd_m.jpg
6484 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Read url http://static.flickr.com/1104/542661469_d1a21eb9bd_m.jpg in 469 ms size 5961.
6625 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Read mediaType image/jpeg as javax.swing.ImageIcon in 141 ms.
6625 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Reading url http://static.flickr.com/49/142731041_4e9eff1694_m.jpg
7094 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Read url http://static.flickr.com/49/142731041_4e9eff1694_m.jpg in 469 ms size 16020.
7484 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Read mediaType image/jpeg as javax.swing.ImageIcon in 390 ms.
7484 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Reading url http://static.flickr.com/45/142730518_79013350b8_m.jpg
7953 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Read url http://static.flickr.com/45/142730518_79013350b8_m.jpg in 469 ms size 20054.
8359 [main] INFO org.jboss.resteasy.examples.resteasy.LoggingExecutionInterceptor - Read mediaType image/jpeg as javax.swing.ImageIcon in 406 ms.
Client Approach Comparison
The JAX-RS proxy approach is declarative, and the ClientRequest approach is imparative. The ClientRequest code is a bit more concise overall than it's proxied counterpart, but has the disadvantage of form and function, which the proxy separates the two. The ClientRequestFactory.get() method is simple and limited, but will likely cover 80%+ of cases. The overall use of ClientRequestFactory's ClientRequest and proxy creation methods will likely become invaluable as you add more interceptors.
LightweightBrowserCache
The CachingInterceptor is one of the most compelling existing REST interceptors. It performs fully RESTful client-side caching, similar to what you're browser would do. It uses HTTP caching headers as instructions of what should be cached and for how long. The Flickr client, unfortunately, can't really take advantage of it because the RESTEasy client implementation caches in memory, and the code executes the requests for XML and images happens only once per JVM.
However, the RESTEasy caching solution is pluggable. You can add coherence to your classpath, and modify the initializeRequests like so:
private static ClientRequestFactory initializeRequests()
{
ResteasyProviderFactory instance = ResteasyProviderFactory
.getInstance();
RegisterBuiltin.register(instance);
instance.registerProvider(ImageIconMessageBodyReader.class);
ClientRequestFactory client = new ClientRequestFactory();
client.getPrefixInterceptors().registerInterceptor(
new LoggingExecutionInterceptor());
<b>client.getPrefixInterceptors().registerInterceptor(new CacheInterceptor(
new LightweightBrowserCache(new MapCache(CacheFactory.getCache("flickr")))));</b>
return client;
}
CacheFactory.getCache("flickr") returns an instance of a Coherence "Enterprise HashMap" which performs complex system-wide caching without much configuration. Everything else is RESTEasy
To see this in action, all you'll have to do is:
- Start up one instance of this application.
- Wait until all of the images are loaded.
- Start up another instance of this application.
The first instance will take a bit longer to start up than before, since Coherence has to initialize; the caching time is practically 0. The second instance should run dramatically quicker because of the caching.
Conclusion
I hope you've gotten enough of a taste of the three client flavors that RESTEasy provides. I also hope that you "GET" the picture of how RESTEasy's client interceptors work. If you have any questions and suggestions about it, please let me know by commenting here, emailing me or twittering me!
Opinions expressed by DZone contributors are their own.
Comments