How to develop a Java REST client in 3 minutes

by Guido García on 2/03/2012

Some time ago I wrote a post about how to implement a REST API with Java (spanish). Today I am going to write about how to consume a REST API as a client. More specifically, I am going to use Digg API (probably not the best REST API out there) to search stories by a given keyword.

Show me the code

As it is a third party REST API, I am going to use the client framework provided by RESTEasy, that I find extremely easy to use.

You only have to add one dependency to your pom.xml:

  <dependency>
      <groupId>org.jboss.resteasy</groupId>
      <artifactId>resteasy-jaxrs</artifactId>
      <version>2.3.1.GA</version>
  </dependency>

And you are now ready to start coding. I think the Java code is self explanatory, so here it goes:

import org.jboss.resteasy.client.ClientRequest;
import org.jboss.resteasy.client.ClientResponse;

public class DiggClient {
    private static final String DIGG_SEARCH_ENDPOINT =
            "http://services.digg.com/{version}/search.search";

    public static void main(String[] args) throws Exception {
        ClientRequest req = new ClientRequest(DIGG_SEARCH_ENDPOINT);
        req
            .pathParameter("version", "2.0")
            .queryParameter("query", "health");

        ClientResponse<String> res = req.get(String.class);
        System.out.println(res.getEntity());
    }
}

Update: As pointed out by Andy MacKinlay in the comments, ClientRequest is deprecated in RESTEasy 3.x.

But I do not want to parse a String…

You might have noticed that the response is converted to a String. In most cases an API will return a XML or a JSON representation of the data as response to the request. In these cases it is great to automatically convert the response into the objects in your Java data model.

For example, these are the domain classes representing (partially) the Digg search results:

class SearchResult {
    private int total;
    private Story[] stories;
    // ... getters/setters
}

class Story {
    private String id;
    private String title;
    // ... getters/setters
}

Digg results are offered as JSON, so you only need to add a Maven dependency to include the providers RESTEasy uses to handle JSON:

  <dependency>
      <groupId>org.jboss.resteasy</groupId>
      <artifactId>resteasy-jackson-provider</artifactId>
      <version>2.3.1.GA</version>
  </dependency>

The resulting Java code remains almost the same:

import org.jboss.resteasy.client.ClientRequest;
import org.jboss.resteasy.client.ClientResponse;

public class DiggClient {
    private static final String DIGG_SEARCH_ENDPOINT =
            "http://services.digg.com/{version}/search.search";

    public static void main(String[] args) throws Exception {
        ClientRequest req = new ClientRequest(DIGG_SEARCH_ENDPOINT);
        req
            .pathParameter("version", "2.0")
            .queryParameter("query", "health");

        ClientResponse<SearchResult> res = req.get(SearchResult.class);
        SearchResult result = res.getEntity();
        System.out.println(result.getTotal() + " matching stories found");
    }
}

Note: you need to annotate your domain Java classes as @JsonIgnoreProperties(ignoreUnknown=true) if the server returns any field that is not present in your domain Java classes.

What if you are using your own API?

If you already have developed your JAX-RS resources, I would suggest to use a proxy-based approach. Take a look at the JAX-RS client factory in Apache CXF, or how to share interfaces between client and server with RESTEasy. This way you reuse more code and makes your code maintenance easier.

Comments about other alternatives (Jersey, etc) are welcome.

 
1005 Likes
Hold on

There are 7 comments in this article:

  1. 25/08/2012Guido says:

    Please notice that it is very important to release the connection calling res.releaseConnection() if the entity is not read using any of the res.getEntity() methods (for example if you call a REST endpoint that returns an empty body).

  2. 16/01/2013Marc says:

    A fluent alternative with async support, etc. etc.:

    SearchResult out = Get(uri).map(SearchResult.class);
    

    based on Apache HC.

    Project home: https://github.com/mfornos/glaze-http

    I’m looking for constructive feedback and criticism to improve the project.

    Thank you.

  3. 16/01/2013Marc says:

    To extend a bit the precedent comment, a complete REST CAS client with Glaze:

    package glaze.examples.misc;
    
    import static glaze.Glaze.Delete;
    import static glaze.Glaze.Get;
    import static glaze.Glaze.Post;
    import static glaze.client.Form.newForm;
    import static glaze.client.UriBuilder.uriBuilderFrom;
    import static org.apache.http.HttpHeaders.LOCATION;
    import static org.apache.http.entity.ContentType.TEXT_PLAIN;
    import glaze.client.Response;
    import glaze.client.handlers.CroakErrorHandler;
    import glaze.client.handlers.ErrorHandler;
    
    import org.apache.http.HttpEntity;
    
    /**
     * See https://wiki.jasig.org/display/CASUM/RESTful+API
     * 
     */
    public class CASClient
    {
    
       String svcUri = "http://127.0.0.1/service";
       String tgtUri = "https://127.0.0.1/cas/v1/tickets";
       String username = "username";
       String password = "password";
       String app = "myApp";
       ErrorHandler eh = new CroakErrorHandler();
    
       public CASClient()
       {
          // 1. Grab the Ticket Granting Ticket (TGT)
    
          HttpEntity credentials = newForm().add("username", username)
                                                .add("password", password)
                                                .add("app", app)
                                                .build();
    
          String ticketUri = Post(tgtUri).entity(credentials)
                                      .setAccept(TEXT_PLAIN)
                                      .withErrorHandler(eh)
                                      .send()
                                      .header(LOCATION);
    
          // 2. Grab a service ticket (ST) for a CAS protected service
    
          String ticket = Post(ticketUri)
                                  .entity(newForm().add("service", svcUri).build())
                                  .withErrorHandler(eh)
                                  .send()
                                  .asString();
    
          // 3. Grab the protected document
    
          // Note: here you can deserialize the response object using the method 'map' instead of 'send', 
          // negotiates the deserialization according to the response content-type
          Response document = Get(uriBuilderFrom(svcUri).addParameter("ticket", ticket).build()).send();
          System.out.println(document.asString());
    
          // 4. Logout
    
          Delete(ticketUri).send();
       }
    
       public static void main(String... args)
       {
          new CASClient();
       }
    
    }
    
  4. 16/01/2013Guido García says:

    Thanks for sharing, Marc. I’ll definitely take a look at your project, it seems pretty easy as well.

  5. 2/10/2013Andy MacKinlay says:

    Hi Guido,

    Great post. I’ve been trying to do something similar with the newer version of RESTEasy (3.0.4). The APIs you mention here are deprecated in that version. Inspired by your post, I’ve put together something similar based on the newer API: http://stackoverflow.com/a/19128399/1711188

  6. 2/10/2013Guido García says:

    Thanks Andy, RESTEasy 3 was not available when I wrote this. I’ll write a follow up article using the newer version, and probably comparing it with other libraries such as Jersey.

  7. 24/01/2014The long tail in this blog - My name is Guido says:

    […] single post drives 40% of the traffic to the blog. At the bottom, 70% of its posts represent 4% of the […]

Write a comment: