Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Kenneth A. Kousen - Making Java Groovy - 2014.pdf
Скачиваний:
50
Добавлен:
19.03.2016
Размер:
15.36 Mб
Скачать

RESTful web services

This chapter covers

The REST architectural style

Implementing REST in Java using JAX-RS

Using a Groovy client to access RESTful services

Hypermedia

RESTful web services dominate API design these days, because they provide a convenient mechanism for connecting client and server applications in a highly decoupled manner. Mobile applications especially use RESTful services, but a good RESTful design mimics the characteristics that made the web so successful in the first place.

After discussing REST in general, I’ll talk about the server side, then about the client side, and finally the issue of hypermedia. Figures 9.1, 9.2, and 9.3 show the different technologies in this chapter.

227

www.it-ebooks.info

228

CHAPTER 9 RESTful web services

JAX-RS (server side)

Response

@Produces, @Consumes

builder Annotations

@GET, @POST, @PUT,

methods

@DELETE @Path, @PathParam

@Context, @UriInfo

Resources Representations

(JSON, XML)

Figure 9.1 Server-side JAX-RS technologies in this chapter. JAX-RS 2.0 is annotation-based but includes builders for the responses. URIs are mapped to methods in resources, which are assigned using annotations. Resources are returned as representations using content negotiation from client headers.

JAX-RS (client side)

Java

Client,

Apache

web target

HttpClient

 

Java +

RESTClient

Groovy

JsonBuilder,

Groovy

JsonSlurper

Hypermedia

Figure 9.2 Client-side REST technologies in this chapter. Unlike JAX-RS 1.x, version 2.0 includes client classes. Apache also has a common client, which is wrapped by Groovy in the HttpBuilder project. Finally, you can use standard Groovy classes to parse requests and build responses manually.

Transitional

Structural

links

links

 

MessageBody

 

reader and writer

JsonBuilder,

JsonSlurper

Figure 9.3 Hypermedia approaches in this chapter. Hypermedia in JAX-RS is done through transitional links in the HTTP headers, structural links in the message body, or customized responses using builders and slurpers.

www.it-ebooks.info

The REST architecture

229

9.1The REST architecture

The term Representational State Transfer (REST) comes from the 2000 PhD thesis1 by Roy Fielding, a person with one of the all-time great resumes.2

In his thesis, Fielding defines the REST architecture in terms of addressable resources and their interactions. When restricted to HTTP requests made over the web (not a requirement of the architecture, but its most common use today), RESTful web services are based on the following principles:

Addressable resource—Items are accessible to clients through URIs.

Uniform interface—Resources are accessed and modified using the standard

HTTP verbs GET, POST, PUT, and DELETE.3

Content negotiation—The client can request different representations of resources, usually by specifying the desired MIME type in the Accept header of a request.

Stateless services—Interactions with resources are done through self-contained requests.

Web services based on these ideas are intended to be highly scalable and extensible, because they follow the mechanisms that make the web itself highly scalable and extensible.

Part of the scalability of a RESTful web service comes from the terms safe and idempotent:

Safe—Does not modify the state of the server

Idempotent—Can be repeated without causing any additional effects

GET requests are both safe and idempotent. PUT and DELETE requests are idempotent but not safe. They can be repeated (for example, if there’s a network error) without making any additional changes.4 POST requests are neither safe nor idempotent.

Another key concept is Hypermedia as the Engine of Application State, which has the truly unfortunate, unpronounceable acronym HATEOAS. Most REST advocates5 I know simply say “hypermedia” instead.

1“Architectural Styles and the Design of Network-based Software Architectures,” available online at www.ics.uci.edu/~fielding/pubs/dissertation/top.htm.

2Fielding is a cofounder of the Apache Software Foundation; was on the IETF working groups for the URI, HTTP, and HTML specifications; and helped set up some of the original web servers. I place him easily in the Top Ten of CS resumes, along with people like James Duncan Davidson (creator of the first versions of both Tomcat and Ant; he basically owned the 90s), Sir Timothy Berners-Lee (create the web knighthood FTW), and Haskell Curry (whose first name is the definitive functional programming language, and whose last name is a fundamental coding technique; if your name is your resume, you win).

3Some services support HEAD requests as GET requests that return headers with empty responses and OPTIONS requests as an alternate way to specify what types of requests are valid at a particular address. PATCH requests are proposed as a way to do a partial update.

4Sometimes it’s hard to picture DELETE requests as idempotent, but if you delete the same row multiple times, it’s still gone.

5Often known, believe it or not, as RESTafarians.

www.it-ebooks.info

230

CHAPTER 9 RESTful web services

The principles defined in this section are architectural and are thus independent of implementation language. In the next section I’ll address6 the Java-specific specification intended to implement RESTful services, JAX-RS.

9.2The Java approach: JAX-RS

The Java EE specification includes the Java API for RESTful Services. Version 1.18 is from JSR 311. The new version, 2.0, is an implementation of JSR 339 and was released in May of 2013.

In this section I’ll implement a set of CRUD methods on a simple POJO.7 The JAXRS part doesn’t depend on this, so I’ll discuss that separately. I’ll start with the basic infrastructure and then move to REST.

What do Java developers actually use for REST?

In this book I normally start with what Java developers use for a particular problem, then show how Groovy can help the Java implementations, and finally discuss what Groovy offers as an alternative. When I describe what Java developers typically use, I default to what the Java SE or EE specification provides.

That’s not the case with REST. In addition to the spec, JAX-RS, Java developers use several third-party alternatives. Among the most popular are Restlet (http://restlet.org/), RestEasy (www.jboss.org/resteasy), and Restfulie (http://restfulie.caelum.com.br/), and there are other alternatives as well.8 It’s hard at this point to know which, if any, is going to be the REST framework of choice for Java developers in a few years.9

Therefore, I’m basing this chapter on the JAX-RS specification, even though it’s not necessarily the most popular alternative. When the alternative is not blindingly obvious, the spec usually wins.10

The application in this section exposes a POGO called Person as a JAX-RS 2.0 resource. The application supports GET, POST, PUT, and DELETE operations and (eventually) supports hypermedia links.8910

The infrastructure for the project includes the POJO, Spock tests, and a DAO implementation based on an H2 database. While the implementations are interesting, they are ancillary to the real goal of discussing RESTful services and how Groovy can simplify their development. Therefore they will not be presented in detail in this chapter. As usual, the complete classes, including Gradle build files and tests, can be found in the book source code repository.

As a brief summary, the Person class is shown in the next listing.

6Sorry.

7Yes, that’s a URL-driven database, and yes, that violates hypermedia principles. I promise to get to that later.

8Spring REST doesn’t follow the JAX-RS specification. Apache CXF was designed for JAX-WS, but the latest version has JAX-RS support. Apache Wink is another JAX-RS 1.x implementation.

9If I had to bet, I’d go with Restlet. Most of the good REST people I know really like it.

10Except when it doesn’t. See, for example, JDO, which is still part of Java EE.

www.it-ebooks.info

The Java approach: JAX-RS

231

Listing 9.1 A Person POGO, used in the RESTful web service

class Person { Long id String first String last

String toString() { "$first $last"

}

}

The DAO interface for the Person objects includes finder methods, as well as methods to create, update, and delete a Person. It’s shown in the following listing.

Listing 9.2 The DAO interface with the CRUD methods for Person

import java.util.List;

public interface PersonDAO { List<Person> findAll(); Person findById(long id);

List<Person> findByLastName(String name); Person create(Person p);

Person update(Person p); boolean delete(long id);

}

The implementation of the DAO is done in Groovy using the groovy.sql.Sql class, just as in chapter 8 on databases. The only part that differs from that chapter is that the id attribute is generated by the database. Here’s how to use the Sql class to retrieve the generated ID:

Person create(Person p) {

String txt = 'insert into people(id, first, last) values(?, ?, ?)' def keys = sql.executeInsert txt, [null, p.first, p.last]

p.id = keys[0][0] return p

}

The executeInsert method returns the collection of generated values, and in this case the new ID is found as the first element of the first row.

The Spock test for the DAO is similar to those shown in chapter 6 on testing or chapter 8 on databases. The only new part is that the when/then block in Spock is repeated to insert and then delete a new Person. When Spock sees a repeat of the when/then pair, it executes them sequentially. Listing 9.3 shows this test, which inserts a row representing Peter Quincy Taggart,11 verifies that he’s stored properly, and then

11Remember him? Commander of the NSEA Protector? “Never give up, never surrender?” That’s Galaxy Quest, a Star Trek parody, but arguably one of the better Star Trek movies. Did you know that the designation of the Protector was NTE-3120, and that NTE stood for “Not The Enterprise”? By Grabthar’s hammer, that’s the kind of research you are obligated to do when you write a Groovy/Java integration book.

www.it-ebooks.info

232

CHAPTER 9 RESTful web services

deletes the row. Recall that the seriously cool old method in Spock evaluates its argument before executing the when block, so it can be compared to the rest of the expression evaluated after the when block is done.

Listing 9.3 The Spock test method to insert and delete a new Person

def 'insert and delete a new person'() {

Person taggart = new Person(first:'Peter Quincy', last:'Taggart')

when:

dao.create(taggart)

then:

dao.findAll().size() == old(dao.findAll().size()) + 1 taggart.id

when:

dao.delete(taggart.id)

then:

dao.findAll().size() == old(dao.findAll().size()) - 1

}

Now that the preliminaries are out of the way it’s time to look at the features provided by the JAX-RS API.

9.2.1JAX-RS resource and tests

Moving now to the RESTful part of the application, there are several features of the JAX-RS API involved in the implementation. Here I’ll use a PersonResource class to implement the CRUD methods.

COLLECTION AND ITEM RESOURCES Normally two resources are provided: one for a collection of person instances and one for an individual person. In this case both are combined to keep the sample short.

First, each method that’s tied to a particular type of HTTP request uses one of these annotations: @GET, @POST, @PUT, or @DELETE. For example, the findAll method can be implemented as follows:

@GET

public List<Person> findAll() { return dao.findAll();

}

A GET request returns the HTTP status code 200 for a successful request. The @Produces annotation identifies to the client the MIME type of the response. In this case I want to return JSON or XML:

@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})

The annotation accepts an array of MediaType instances, which are used for content negotiation based on the Accept header in the incoming request.

www.it-ebooks.info

The Java approach: JAX-RS

233

If I want to specify the response header, JAX-RS provides a factory class called Response using the builder design pattern. Here’s the implementation of the findById method that uses it:

@GET @Path("{id}")

@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response findById(@PathParam("id") long id) {

return Response.ok(dao.findById(id))

.build();

}

The ok method on the Response class sets the response status code to 200. It takes an object as an argument, which is added to the response. The @PathParam annotation also converts the input ID from a string to a long automatically.

Inserting a new instance is a bit more complicated, because the newly inserted instance needs its own URI. Because in this case the generated URI will contain an ID generated by the database, the resource method is tied to HTTP POST requests, which are neither safe nor idempotent.

IMPLEMENTATION DETAIL The create method returns a URL that includes the primary key from the database table. That detail is not something you want to expose to the client. Some unique identifier is required; here the ID is used for simplicity.

The new URI is added to the response as part of its Location header. The new URI is generated using the UriBuilder class from JAX-RS, based on the incoming URI:

UriBuilder builder = UriBuilder.fromUri(uriInfo.getRequestUri()).path("{id}");

The uriInfo reference in that expression refers to a UriInfo object injected from the application context. This is added as an attribute to the implementation:

@Context

private UriInfo uriInfo;

In general, the response from any insert method in a REST application is either “no content” or the entity itself. Here in the create method I decided to use the entity, because it includes the generated ID in case the client wants it.

Putting it all together, the create method is as follows:

@POST

@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response create(Person person) {

dao.create(person); UriBuilder builder =

UriBuilder.fromUri(uriInfo.getRequestUri()).path("{id}"); return Response.created(builder.build(person.getId()))

.entity(person)

.build();

}

www.it-ebooks.info

234

CHAPTER 9 RESTful web services

The @POST annotation sets the HTTP status code in the response to 201. The URL patterns for the resource are summarized as follows:

The base resource pattern is /people. A GET request at that URL returns all the Person instances. The plural form of Person is used for this reason.

A POST request at the same URL (/person) creates a new Person, assigns it a URL of its own, and saves it in the database.

A sub-resource at /people/lastname/{like} uses a URL template (the like parameter) to do an SQL-like query and find all Person instances who have a last name satisfying the clause.

A sub-resource using the URL template {id} supports a GET request that returns the Person instance with that ID.

PUT and DELETE requests at the {id} URL update and delete Person instances, respectively.

The following listing shows the complete PersonResource class for managing Person instances.

URI template parameter ID

Listing 9.4 Java resource class for Person POJO

@Path("/people")

public class PersonResource { @Context

private UriInfo uriInfo;

private PersonDAO dao = JdbcPersonDAO.getInstance();

@GET

@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public List<Person> findAll() {

return dao.findAll();

 

URI template

 

 

 

}

 

 

 

 

 

parameter like

 

 

 

 

 

@GET @Path("lastname/{like}")

 

 

 

 

 

 

 

 

 

 

@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})

 

public List<Person> findByName(@PathParam("like") String like) {

 

 

return dao.findByLastName(like);

 

 

 

Access

}

 

 

 

 

 

 

 

 

template

 

 

 

 

 

@GET @Path("{id}")

 

 

 

parameter

@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})

 

public Response findById(@PathParam("id") long id) {

 

 

 

 

 

 

 

 

return Response.ok(dao.findById(id))

 

 

 

 

.build();

 

 

 

 

}

 

 

 

 

 

@POST

@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response create(Person person) {

dao.create(person); UriBuilder builder =

UriBuilder.fromUri(uriInfo.getRequestUri()).path("{id}");

www.it-ebooks.info

The Java approach: JAX-RS

235

return Response.created(builder.build(person.getId()))

.entity(person)

.build();

}

@PUT @Path("{id}")

@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Person update(Person person) {

dao.update(person); return person;

}

@DELETE @Path("{id}")

@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response remove(@PathParam("id") long id) {

dao.delete(id);

return Response.noContent().build();

}

}

To verify that everything is working properly, I’ll again present a test class using Spock. Testing a RESTful API requires a server where the application can be deployed. The Jersey reference implementation includes a server called Grizzly for that.

The Spock test methods setupSpec and shutdownSpec are executed once each, before and after the individual tests, respectively. They therefore become the appropriate places to start and stop the server, as shown:

@Shared static HttpServer server

void setupSpec() {

server = GrizzlyHttpServerFactory.createHttpServer( 'http://localhost:1234/'.toURI(), new MyApplication())

}

void cleanupSpec() { server?.stop()

}

The createHttpServer method starts a server on the specified URI and deploys a RESTful application to it. The MyApplication class is very simple:

public class MyApplication extends ResourceConfig { public MyApplication() {

super(PersonResource.class, JacksonFeature.class);

}

}

The class MyApplication extends a JAX-RS class called ResourceConfig, which has a constructor that takes the desired resources and features as arguments. The JacksonFeature used here provides the mechanism to convert from PersonResource instances to JSON and back.12

12As soon as I mention JSON, I’m talking about representations, not resources. Again, I’ll discuss that in section 9.5 on hypermedia.

www.it-ebooks.info

236

CHAPTER 9 RESTful web services

Note the convenience of the safe-dereference operator, ?., used when shutting down the server. That will avoid a null pointer exception when the server fails to start properly.

The first actual test verifies that the server is up and running, using the isStarted method on the HttpServer class:

def 'server is running'() { expect: server.started

}

Again, the isStarted method is invoked using the standard Groovy idiom of accessing a property. There’s no reason you couldn’t call the method instead, though, if you prefer.

The rest13 of the test methods require a client to generate the HTTP request using the proper verb. GET requests are trivial with Groovy, because you can take advantage of the getText method that the Groovy JDK adds to the java.net.URL class. So the request to retrieve all the instances could be written as

'http://localhost:1234/people'.toURL().text

While that would work, the response would then need to be parsed to get the proper information. Often that isn’t a problem, but here I’m using an alternative.

The class RESTClient is part of the HttpBuilder (http://groovy.codehaus.org/ modules/http-builder/) project. I’ll discuss that further in section 9.4 on Groovy clients, but for now let me say it defines Groovy classes that wrap Java classes supplied by Apache’s HttpClient project. The test therefore contains an attribute of type RESTClient, as follows:

RESTClient client

new RESTClient('http://localhost:1234/', ContentType.JSON)

The client points to the proper endpoint, and the second argument specifies the content type for the Accept header in the request. A GET request using this client returns an object that can be interrogated for header properties as well as data:

def 'get request returns all people'() { when:

def response = client.get(path: 'people')

then:

response.status == 200 response.contentType == 'application/json' response.data.size() == 5

}

Other finder methods are tested similarly. To keep the tests independent, the insert and delete methods are tested together; first a person is inserted, then it’s verified, and then it’s deleted again. The test uses another feature of Spock: each block (when/ then/expect, and so on) can be given a string to describe its purpose. It’s not exactly behavior-driven development, but it’s as close as Spock comes at the moment.

13 Again, no pun intended.

www.it-ebooks.info

The Java approach: JAX-RS

237

The insert and delete test looks like the following:

def 'insert and delete a person'() {

given: 'A JSON object with first and last names' def json = [first: 'Peter Quincy', last: 'Taggart']

when: 'post the JSON object'

def response = client.post(path: 'people', contentType: ContentType.JSON, body: json)

then: 'number of stored objects goes up by one' getAll().size() == old(getAll().size()) + 1 response.data.first == 'Peter Quincy' response.data.last == 'Taggart'

response.status == 201 response.contentType == 'application/json' response.headers.Location ==

"http://localhost:1234/people/${response.data.id}"

when: 'delete the new JSON object' client.delete(path: response.headers.Location)

then: 'number of stored objects goes down by one' getAll().size() == old(getAll().size()) - 1

}

Given a JSON object representing a person, a POST request adds it to the system. The returned object holds the status code (201), the content type (application/json), the returned person object (in the data property), and the URI for the new resource in the Location header. Deleting the object is done by sending a DELETE request to the new URI and verifying that the total number of stored instances goes down by one.

Updates are done through a PUT request. To ensure that PUT requests are idempotent, the complete object needs to be specified in the body of the request. This is why PUT requests aren’t normally used for inserts; the client doesn’t know the ID of the newly inserted object, so POST requests are used for that instead.

The complete test is shown in the next listing.

Listing 9.5 A Spock test for the PersonResource with a convenient test server

class PersonResourceSpec extends Specification {

 

Client from

 

@Shared static HttpServer server

 

HttpBuilder project

RESTClient client =

 

 

 

 

 

 

 

 

 

 

 

 

new RESTClient('http://localhost:1234/', ContentType.JSON)

 

JSON

 

 

 

 

 

 

void setupSpec() {

 

 

 

 

representation

server = GrizzlyHttpServerFactory.createHttpServer(

 

 

 

 

 

 

 

 

 

 

'http://localhost:1234/'.toURI(), new MyApplication())

Shared

}

 

 

 

 

 

 

 

 

 

server

def 'server is running'() { expect: server.started

}

def 'get request returns all people'() { when:

def response = client.get(path: 'people')

www.it-ebooks.info

238

CHAPTER 9 RESTful web services

then:

response.status == 200 response.contentType == 'application/json' response.data.size() == 5

}

@Unroll

def "people/#id gives #name"() { expect:

def response = client.get(path: "people/$id")

name == "$response.data.first $response.data.last" response.status == 200

where:

 

 

 

id |

name

 

Spock data

 

1 | 'Jean-Luc Picard'

 

 

table

2| 'Johnathan Archer'

3| 'James Kirk'

4| 'Benjamin Sisko'

5| 'Kathryn Janeway'

}

def 'people/lastname/{like} searches for last names like'() { when:

def response = client.get(path: "people/lastname/a")

then: response.data.size() == 3

response.data*.last ==~ /.*[aA].*/

}

def 'insert and delete a person'() {

given: 'A JSON object with first and last names' def json = [first: 'Peter Quincy', last: 'Taggart']

when: 'post the JSON object'

def response = client.post(path: 'people', contentType: ContentType.JSON, body: json)

then: 'number of stored objects goes up by one' getAll().size() == old(getAll().size()) + 1 response.data.first == 'Peter Quincy' response.data.last == 'Taggart'

response.status == 201 response.contentType == 'application/json' response.headers.Location ==

"http://localhost:1234/people/${response.data.id}"

when: 'delete the new JSON object' client.delete(path: response.headers.Location)

then: 'number of stored objects goes down by one' getAll().size() == old(getAll().size()) - 1

}

Location header for inserted resource

def 'can update an existing person'() { given:

def kirk = client.get(path: 'people/3')

def json = [id: 3, first:'James T.', last: 'Kirk']

www.it-ebooks.info

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]