Java EE 7 and JAX-RS 2.0
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.
Listing 1
As in the previous JAX-RS specifications, a resource can be a
Interestingly the
Listing 3
The parameter
Listing 4
An
Listing 5
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
Any CDI managed bean or EJB could receive the
With the
The
Listing 7
Modified content results in different fingerprints and causes the raising of the
All the computation for fingerprints or for outgoing requests can be easily automated with an implementation of the
Listing 8
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
As in the previous JAX-RS releases, custom extensions are going to be automatically discovered and registered with the
Listing 10
Accordingly, a registered implementation of the
Without any additional configuration the
Listing 11
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
Listing 12
All interceptors or filters denoted with the
Listing 13
Custom
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
Listing 14
Listing 15
In the integration test above, the default
The main purpose of a
In the method
Listing 16
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
The new client API also supports asynchronous resource invocation. As mentioned earlier, an
Listing 17
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
A truly asynchronous implementation can be achieved with a callback registration, as shown in Listing 18:
Listing 18
For each method returning a
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
Listing 19
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:
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 aCoffeeBeansResource
. 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 ofjavax.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
theReaderInterceptorContext#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
theReaderInterceptor
and WriterInterceptor
have
to be annotated with
the @Provider
annotation—no additional
configuration or API calls are required.Request Interception
An implementation of aContainerRequestFilter
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
likePUT
, 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
orjavax.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
synchronousInvocation.Builder
. The final invocation of
a method named after the HTTP verbs
(GET
, POST
, PUT
, DELETE
, HEAD
, OPTIONS
,
orTRACE
) 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 anAsyncInvoker
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 theInvocationCallback
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
throughAsyncContext
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
- RESTEasy
- Jersey
- JAX-RS 2.0 specification
- Real World Java EE Patterns—Rethinking Best Practices
- HTTP RFC
- Java EE 6 Observer
- Digester
Reprinted with permission from the Oracle Technology Network, Oracle Corporation
0 comments:
Post a Comment