Java EE 7 with JAX-RS 2.0 brings several useful features,
which further simplify development and lead to the creation of even
more-sophisticated, but lean, Java SE/EE RESTful applications. Adam
Bien introduces us to one of the key features of Java EE
7. Reprinted with permission from the Oracle Technology
Network, Oracle Corporation.
Sample Code
Most Java EE 6 applications, with the requirement for a remote
API and free choice, are using a more or less RESTful flavor of the
JAX-RS 1.0 specification. Java EE 7 with JAX-RS 2.0 brings several
useful features, which further simplify development and lead to the
creation of even more-sophisticated, but lean, Java SE/EE RESTful
applications.
Roast House
Roast House is a Java-friendly but simplistic JAX-RS 2.0
example, which manages and roasts some coffee beans. The roast
house itself is represented as
a
CoffeeBeansResource
. The
URI
"coffeebeans"
uniquely identifies
the
CoffeeBeansResource
(see
Listing
1).
Listing 1
//...
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
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.container.ResourceContext;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
@ApplicationScoped
@Path("coffeebeans")
public class CoffeeBeansResource {
@Context
ResourceContext rc;
Map<String, Bean> bc;
@PostConstruct
public void init() {
this.bc = new ConcurrentHashMap<>();
}
@GET
public Collection<Bean> allBeans() {
return bc.values();
}
@GET
@Path("{id}")
public Bean bean(@PathParam("id") String id) {
return bc.get(id);
}
@POST
public Response add(Bean bean) {
if (bean != null) {
bc.put(bean.getName(), bean);
}
final URI id = URI.create(bean.getName());
return Response.created(id).build();
}
@DELETE
@Path("{id}")
public void remove(@PathParam("id") String id) {
bc.remove(id);
}
@Path("/roaster/{id}")
public RoasterResource roaster(){
return this.rc.initResource(new RoasterResource());
}
}
As in the previous JAX-RS specifications, a resource can be
a
@Singleton
or
@Stateless
EJB.
In addition, all root resources, providers,
and
Application
subclasses can be deployed
as managed or CDI-managed beans. Injection is also available in all
extensions annotated with
the
@Provider
annotation, which simplifies
integration with existing code. JAX-RS–specific components can be
also injected into sub-resources using
the
ResourceContext
:
Listing 2
@Context
ResourceContext rc;
@Path("/roaster/{id}")
public RoasterResource roaster(){
return this.rc.initResource(new RoasterResource());
}
Interestingly
the
javax.ws.rs.container.ResourceContext
not
only allows you to inject JAX-RS information into an existing
instance, but also provides you access to the resource classes with
the
ResourceContext#getResource(Class<T>
resourceClass)
method. Injection points of instances
passed to
the
ResourceContext#initResource
method are
set with values from the current context by the JAX-RS runtime. The
field
String id
in
the
RoasterResource
class (shown in
Listing 3) receives the value of the path
parameter of the parent's resource:
Listing 3
public class RoasterResource {
@PathParam("id")
private String id;
@POST
public void roast(@Suspended AsyncResponse ar, Bean bean) {
try {
Thread.sleep(2000);
} catch (InterruptedException ex) {
}
bean.setType(RoastType.DARK);
bean.setName(id);
bean.setBlend(bean.getBlend() + ": The dark side of the bean");
Response response = Response.ok(bean).header("x-roast-id", id).build();
ar.resume(response);
}
}
The
parameter
javax.ws.rs.container.AsyncResponse
is
similar to the Servlet
3.0
javax.servlet.AsyncContext
class and
allows asynchronous request execution. In the above example, the
request is suspended for the processing duration and the response
is pushed to the client with the invocation of the
method
AsyncResponse#resume
. The
method
roast
is still executed
synchronously, so the asynchronous execution does not bring any
asynchronous behavior at all. However, the combination of
EJB's
@javax.ejb.Asynchronous
annotation and
the
@Suspended AsyncResponse
enables
asynchronous execution of business logic with eventual notification
of the interested client. Any JAX-RS root resource can be annotated
with
@Stateless
or
@Singleton
annotations
and can, in effect, function as an EJB (see
Listing
4):
Listing 4
import javax.ejb.Asynchronous;
import javax.ejb.Singleton;
@Stateless
@Path("roaster")
public class RoasterResource {
@POST
@Asynchronous
public void roast(@Suspended AsyncResponse ar, Bean bean) {
//heavy lifting
Response response = Response.ok(bean).build();
ar.resume(response);
}
}
An
@Asynchronous
resource method with
an
@Suspended AsyncResponse
parameter is
executed in a fire-and-forget fashion. Although the
request-processing thread is freed immediately,
the
AsyncResponse
still provides a
convenient handle to the client. After the completion of
time-consuming work, the result can be conveniently pushed back to
the client. Usually, you would like to separate JAX-RS– specific
behavior from the actual business logic. All business logic could
be easily extracted into a dedicated boundary EJB, but CDI eventing
is even better suited for covering the fire-and-forget cases. The
custom event class
RoastRequest
carries the
payload (
Bean
class) as processing input and
the
AsyncResponse
for the resulting
submission (see
Listing 5):
Listing 5
public class RoastRequest {
private Bean bean;
private AsyncResponse ar;
public RoastRequest(Bean bean, AsyncResponse ar) {
this.bean = bean;
this.ar = ar;
}
public Bean getBean() {
return bean;
}
public void sendMessage(String result) {
Response response = Response.ok(result).build();
ar.resume(response);
}
public void errorHappened(Exception ex) {
ar.resume(ex);
}
}
CDI events not only decouple the business logic from the JAX-RS
API, but also greatly simplify the JAX-RS code (see
Listing
6):
Listing 6
public class RoasterResource {
@Inject
Event<RoastRequest> roastListeners;
@POST
public void roast(@Suspended AsyncResponse ar, Bean bean) {
roastListeners.fire(new RoastRequest(bean, ar));
}
}
Any CDI managed bean or EJB could receive
the
RoastRequest
in a publish-subscribe
style and synchronously or asynchronously process the payload with
a simple observer method:
void onRoastRequest(@Observes
RoastRequest request){}
.
With the
AsyncResponse
class the JAX-RS
specification introduces an easy way to push information to HTTP in
real time. From the client perspective, the asynchronous request on
the server is still blocking and so synchronous. From the
REST-design perspective, all long-running tasks should return
immediately with the HTTP status code 202 along with additional
information about how to get the result after the processing
completes.
The Return of Aspects
Popular REST APIs often require their clients to compute a
fingerprint of the message and send it along with the request. On
the server side, the fingerprint is computed and compared with the
attached information. If both don't match, the message gets
rejected. With the advent of JAX-RS and the introduction
of
javax.ws.rs.ext.ReaderInterceptor
javax.ws.rs.ext.WriterInterceptor
, the traffic can be
intercepted on the server side and even on the client side. An
implementation of
the
ReaderInterceptor
interface on the
server wraps
the
MessageBodyReader#readFrom
and is
executed before the actual serialization.
The
PayloadVerifier
fetches the signature
from the header, computes the fingerprint from the stream, and
eventually invokes
the
ReaderInterceptorContext#proceed
method, which
invokes the next interceptor in the chain or
the
MessageBodyReader
instance (see
Listing 7).
Listing 7
public class PayloadVerifier implements ReaderInterceptor{
public static final String SIGNATURE_HEADER = "x-signature";
@Override
public Object aroundReadFrom(ReaderInterceptorContext ric) throws IOException,
WebApplicationException {
MultivaluedMap<String, String> headers = ric.getHeaders();
String headerSignagure = headers.getFirst(SIGNATURE_HEADER);
InputStream inputStream = ric.getInputStream();
byte[] content = fetchBytes(inputStream);
String payload = computeFingerprint(content);
if (!payload.equals(headerSignagure)) {
Response response = Response.status(Response.Status.BAD_REQUEST).header(
SIGNATURE_HEADER, "Modified content").build();
throw new WebApplicationException(response);
}
ByteArrayInputStream buffer = new ByteArrayInputStream(content);
ric.setInputStream(buffer);
return ric.proceed();
}
//...
}
Modified content results in different fingerprints and causes
the raising of
the
WebApplicationException
with the
BAD_REQUEST (400) response code.
All the computation for fingerprints or for outgoing requests
can be easily automated with an implementation of
the
WriterInterceptor
. An implementation of
the
WriterInterceptor
wraps
MessageBodyWriter#writeTo
and
is executed before the serialization of the entity into a stream.
For the fingerprint computation, the final representation of the
entity "on-the-wire" is needed, so we pass
a
ByteArrayOutputStream
as a buffer, invoke
the
WriterInterceptorContext#proceed()
method,
fetch the raw content and compute the fingerprint. See
Listing 8.
Listing 8
public class PayloadVerifier implements WriterInterceptor {
public static final String SIGNATURE_HEADER = "x-signature";
@Override
public void aroundWriteTo(WriterInterceptorContext wic) throws IOException,
WebApplicationException {
OutputStream oos = wic.getOutputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
wic.setOutputStream(baos);
wic.proceed();
baos.flush();
byte[] content = baos.toByteArray();
MultivaluedMap<String, Object> headers = wic.getHeaders();
headers.add(SIGNATURE_HEADER, computeFingerprint(content));
oos.write(content);
}
//...
}
Finally, the computed signature is added as a header to the
request, the buffer is written to the original stream, and the
whole request is sent to the client. Of course, a single class can
also implement both interfaces at the same time:
Listing 9
import javax.ws.rs.ext.Provider;
@Provider
public class PayloadVerifier implements ReaderInterceptor, WriterInterceptor {
}
As in the previous JAX-RS releases, custom extensions are going
to be automatically discovered and registered with
the
@Provider
annotation. For the interception
of
MessageBodyWriter
and
MessageBodyReader
instances,
only the implementations of
the
ReaderInterceptor
and
WriterInterceptor
have
to be annotated with
the
@Provider
annotation—no additional
configuration or API calls are required.
Request Interception
An implementation of
a
ContainerRequestFilter
and
ContainerResponseFilter
intercepts
the entire request, not only the process of reading and writing of
entities. The functionality of both interceptors is far more useful
than logging of the information contained in
raw
javax.servlet.http.HttpServletRequest
instance.
The class
TrafficLogger
is not only able to
log the information contained in
the
HttpServletRequest
, but also to trace the
information about the resources matching a particular request, as
shown in
Listing 10.
Listing 10
@Provider
public class TrafficLogger implements ContainerRequestFilter, ContainerResponseFilter {
//ContainerRequestFilter
public void filter(ContainerRequestContext requestContext) throws IOException {
log(requestContext);
}
//ContainerResponseFilter
public void filter(ContainerRequestContext requestContext, ContainerResponseContext
responseContext) throws IOException {
log(responseContext);
}
void log(ContainerRequestContext requestContext) {
SecurityContext securityContext = requestContext.getSecurityContext();
String authentication = securityContext.getAuthenticationScheme();
Principal userPrincipal = securityContext.getUserPrincipal();
UriInfo uriInfo = requestContext.getUriInfo();
String method = requestContext.getMethod();
List<Object> matchedResources = uriInfo.getMatchedResources();
//...
}
void log(ContainerResponseContext responseContext) {
MultivaluedMap<String, String> stringHeaders = responseContext.getStringHeaders();
Object entity = responseContext.getEntity();
//...
}
}
Accordingly, a registered implementation of
the
ContainerResponseFilter
gets an instance
of the
ContainerResponseContext
and is able to
access the data generated by the server. Status codes and header
contents, for example, the
Location
header,
are easily
accessible.
ContainerRequestContext
as well
as
ContainerResponseContext
are mutable
classes which can be modified by the filters.
Without any additional configuration
the
ContainerRequestFilter
is executed after
the HTTP-resource matching phase. At this point in time it is no
longer possible to modify the incoming request in order to
customize the resource binding. In case you would like to influence
the binding between the request and a resource,
a
ContainerRequestFilter
can be configured
to be executed before the resource binding phase.
Any
ContainerRequestFilter
annotated with
the
javax.ws.rs.container.PreMatching
annotation
is executed before the resource binding, so the HTTP request
contents can be tweaked for the desired mapping. A common use case
for the
@PreMatching
filters is adjusting
the HTTP verbs to overcome limits in the networking infrastructure.
More "esoteric" methods
like
PUT
,
OPTIONS
,
HEAD
,
or
DELETE
may be filtered out by firewalls
or not supported by some HTTP clients.
@PreMatching
ContainerRequestFilter
implementation could fetch the
information from the header (for example,
"
X-HTTP-Method-Override
") indicating the desired HTTP
verb and could change a
POST
request into
a
PUT
on the fly (see
Listing
11).
Listing 11
@Provider
@PreMatching
public class HttpMethodOverrideEnabler implements ContainerRequestFilter {
public void filter(ContainerRequestContext requestContext) throws IOException {
String override = requestContext.getHeaders()
.getFirst("X-HTTP-Method-Override");
if (override != null) {
requestContext.setMethod(override);
}
}
}
Configuration
All interceptors and filters registered with
the
@Provider
annotation are globally
enabled for all resources. At deployment time the server scans the
deployment units for
@Provider
annotations
and automatically registers all extensions before the activation of
the application. All extensions can be packaged into dedicated JARs
and deployed on demand with the WAR (in
the
WEB-INF/lib
folder). The JAX-RS runtime
would scan the JARs and automatically register the extensions.
Drop-in deployment of self-contained JARs is nice, but requires
fine-grained extension packaging. All extensions contained within a
JAR would be activated at once.
JAX-RS introduces binding annotations for selective decoration
of resources. The mechanics are similar to CDI qualifiers. Any
custom annotation denoted with the
meta-annotation
javax.ws.rs.NameBinding
can
be used for the declaration of interception points:
Listing 12
@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Tracked {
}
All interceptors or filters denoted with
the
Tracked
annotation can be selectively
activated by applying the
same
Tracked
annotation on classes, methods,
or even subclasses of the Application:
Listing 13
@Tracked
@Provider
public class TrafficLogger implements ContainerRequestFilter, ContainerResponseFilter {
}
Custom
NameBinding
annotations can be
packaged together with the corresponding filter or interceptor and
selectively applied to resources by the application developer.
Although the annotation-driven approach significantly increases
flexibility and allows coarser plug-in packages, the binding is
still static. The application needs to be recompiled and
effectively redeployed to change the interceptor or filter
chain.
In addition to the global and annotation-driven configuration of
cross-cutting functionality, JAX-RS 2.0 also introduces a new API
for dynamic extension registration. An implementation of
the
javax.ws.rs.container.DynamicFeature
interface
annotated with the
@Provider
annotation is used by
the container as a hook for the registration of interceptors and
filters dynamically, without the need for recompilation.
The
LoggerRegistration
extension
conditionally registers
the
PayloadVerifier
interceptor
and
TrafficLogger
filter by querying the existence
of predefined system properties, as shown in
Listing
14:
Listing 14
@Provider
public class LoggerRegistration implements DynamicFeature {
@Override
public void configure(ResourceInfo resourceInfo, FeatureContext context) {
String debug = System.getProperty("jax-rs.traffic");
if (debug != null) {
context.register(new TrafficLogger());
}
String verification = System.getProperty("jax-rs.verification");
if (verification != null) {
context.register(new PayloadVerifier());
}
}
}
The Client Side
The JAX-RS 1.1 specification did not cover the client. Although
proprietary implementations of a client REST API, such as RESTEasy
or Jersey, could communicate with any HTTP resource (not even
implemented with Java EE), the client code was directly dependent
on the particular implementation. JAX-RS 2.0 introduces a new,
standardized Client API. Using a standardized bootstrapping, the
Service Provider Interface (SPI) is replaceable. The API is fluent
and similar to the majority of the proprietary REST client
implementations (see
Listing 15).
Listing 15
import java.util.Collection;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
public class CoffeeBeansResourceTest {
Client client;
WebTarget root;
@Before
public void initClient() {
this.client = ClientBuilder.newClient().register(PayloadVerifier.class);
this.root = this.client.target("http://localhost:8080/roast-house/api/coffeebeans");
}
@Test
public void crud() {
Bean origin = new Bean("arabica", RoastType.DARK, "mexico");
final String mediaType = MediaType.APPLICATION_XML;
final Entity<Bean> entity = Entity.entity(origin, mediaType);
Response response = this.root.request().post(entity, Response.class);
assertThat(response.getStatus(), is(201));
Bean result = this.root.path(origin.getName()).request(mediaType).get(Bean.class);
assertThat(result, is(origin));
Collection<Bean> allBeans = this.root.request().get(
new GenericType<Collection<Bean>>() {
});
assertThat(allBeans.size(), is(1));
assertThat(allBeans, hasItem(origin));
response = this.root.path(origin.getName()).request(mediaType).delete(Response.class);
assertThat(response.getStatus(), is(204));
response = this.root.path(origin.getName()).request(mediaType).get(Response.class);
assertThat(response.getStatus(), is(204));
}
//..
}
In the integration test above, the
default
Client
instance is obtained using
the
parameterless
ClientFactory.newClient()
method.
The bootstrapping process itself is standardized with the
internal
javax.ws.rs.ext.RuntimeDelegate
abstract
factory. Either an existing instance
of
RuntimeDelegate
is injected (by, for
example, a Dependency Injection framework) into
the
ClientFactory
or it is obtained by
looking for a hint in the
files
META-INF/services/javax.ws.rs.ext.RuntimeDelegate
and
${java.home}/lib/jaxrs.properties
and
eventually by searching for
the
javax.ws.rs.ext.RuntimeDelegate
system
property. By a failed discovery, a default (Jersey) implementation
is attempted to initialize.
The main purpose of
a
javax.ws.rs.client.Client
is the enabling
of fluent access
to
javax.ws.rs.client.WebTarget
or
javax.ws.rs.client.Invocation
instances.
A
WebTarget
represents a JAX-RS resource and
an
Invocation
is a ready-to-use request
waiting for submission.
WebTarget
is also
an
Invocation
factory.
In the
method
CoffeBeansResourceTest#crud()
the
Bean
object
is passed back and forth between client and server. With the choice
of
MediaType.APPLICATION_XML
, only a few JAXB
annotations are needed to send and receive a DTO serialized in an
XML document:
Listing 16
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Bean {
private String name;
private RoastType type;
private String blend;
}
The names of the class and attributes have to match for
successful marshaling with the server's representation, but the DTO
does not have to be binary compatible. In the above example,
both
Bean
classes are located in different
packages and even implement different methods. A
desired
MediaType
is passed to
the
WebTarget#request()
method, which
returns an instance of a
synchronous
Invocation.Builder
. The final invocation of
a method named after the HTTP verbs
(
GET
,
POST
,
PUT
,
DELETE
,
HEAD
,
OPTIONS
,
or
TRACE
) initiates a synchronous request.
The new client API also supports asynchronous resource
invocation. As mentioned earlier,
an
Invocation
instance decouples the request
from the submission. An asynchronous request can be initiated with
the chained
async()
method invocation, which
returns an
AsyncInvoker
instance. See
Listing 17.
Listing 17
@Test
public void roasterFuture() throws Exception {
//...
Future<Response> future = this.root.path("roaster").path("roast-id").request().async().post(entity);
Response response = future.get(5000, TimeUnit.SECONDS);
Object result = response.getEntity();
assertNotNull(result);
assertThat(roasted.getBlend(),containsString("The dark side of the bean"));
}
There is not a lot of benefit in the "quasi-asynchronous"
communication style in the above example—the client still has to
block and wait until the response arrives. However,
the
Future
-based invocation is very useful for
batch-processing: the client can issue several requests at once,
gather the
Future
instances, and process
them later.
A truly asynchronous implementation can be achieved with a
callback registration, as shown in
Listing 18:
Listing 18
@Test
public void roasterAsync() throws InterruptedException {
//...
final Entity<Bean> entity = Entity.entity(origin, mediaType);
this.root.path("roaster").path("roast-id").request().async().post(
entity, new InvocationCallback<Bean>() {
public void completed(Bean rspns) {
}
public void failed(Throwable thrwbl) {
}
});
}
For each method returning a
Future
, there is
also a corresponding callback method available. An implementation
of the
InvocationCallback
interface is accepted as
the last parameter of the method (
post()
, in the
example above) and is asynchronously notified upon successful
invocation with the payload or—in a failure case—with an
exception.
An automated construction of URIs can be streamlined with the
built-in templating mechanism. Predefined placeholders can be
replaced shortly before the request execution and save repetitive
creation of
WebTarget
instances:
Listing 19
@Test
public void templating() throws Exception {
String rootPath = this.root.getUri().getPath();
URI uri = this.root.path("{0}/{last}").
resolveTemplate("0", "hello").
resolveTemplate("last", "REST").
getUri();
assertThat(uri.getPath(), is(rootPath + "/hello/REST"));
}
A small, but important detail: on the client side, extensions
are not discovered at initialization time; rather, they have to be
explicitly registered with the Client
instance:
ClientFactory.newClient().register(PayloadVerifier.class)
.
However, the same entity interceptor implementations can be shared
between client and server, which simplifies testing, reduces
potential bugs, and increases productivity. The already
introduced
PayloadVerifier
interceptor can
be reused without any change on the client side as well.
Conclusion: Java EE or Not?
Interestingly, JAX-RS does not even require a full-fledged
application server. After fulfilling the specified Context Types, a
JAX-RS 2.0–compliant API can be anything. However, the combination
with EJB 3.2 brings asynchronous processing, pooling (and so
throttling), and monitoring. Tight integration with Servlet 3+
comes with efficient asynchronous processing
of
@Suspended
responses
through
AsyncContext
support and CDI runtime
brings eventing. Also Bean Validation is well integrated and can be
used for the validation of resource parameters. Using JAX-RS 2.0
together with other Java EE 7 APIs brings the most convenient (=no
configuration) and most productive (=no re-invention) way of
exposing objects to remote systems.
See Also
Author Bio: Consultant and author Adam
Bien is an Expert Group member for the Java EE 6/7, EJB 3.X,
JAX-RS, and JPA 2.X JSRs. He has worked with Java technology since
JDK 1.0 and with servlets/EJB 1.0 and is now an architect and
developer for Java SE and Java EE projects. He has edited several
books about JavaFX, J2EE, and Java EE, and he is the author of
Real World Java EE
Patterns—Rethinking Best Practices and Real
World Java EE Night Hacks. Adam is also a Java Champion,
Top Java Ambassador 2012, and JavaOne 2009, 2011, and 2012 Rock
Star. Adam organizes occasional Java (EE) workshops at
Munich's Airport.
Reprinted with permission from the Oracle Technology
Network, Oracle Corporation