Эта статья является частью серии, в которой представлены новые функции и возможности Java EE 7. Вы можете узнать больше о спецификации платформы Java EE на Java.net.
Java EE 7 с JAX-RS 2.0 предоставляет несколько полезных функций, которые еще больше упрощают разработку и приводят к созданию еще более сложных, но компактных приложений Java SE / EE RESTful.
Опубликовано в апреле 2013 г.
Загрузки:
: Пример кода (почтовый индекс)
Большинство приложений Java EE 6 с требованием удаленного API и свободного выбора используют более или менее RESTful разновидность спецификации JAX-RS 1.0. Java EE 7 с JAX-RS 2.0 предоставляет несколько полезных функций, которые еще больше упрощают разработку и приводят к созданию еще более сложных, но компактных приложений Java SE / EE RESTful.
Жареный Дом
Roast House — это дружественный к Java, но упрощенный пример JAX-RS 2.0, который управляет и обжаривает некоторые кофейные зерна. Сам жареный дом представлен в виде CoffeeBeansResource
. URI "coffeebeans"
уникально идентифицирует CoffeeBeansResource
(см. Листинг 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()); } }
Листинг 1
Как и в предыдущих спецификациях JAX-RS, ресурс может быть @Singleton
или @Stateless
EJB. Кроме того, все корневые ресурсы, поставщики и Application
подклассы могут быть развернуты как управляемые или управляемые CDI компоненты. Инъекция также доступна во всех расширениях, помеченных @Provider
аннотацией, что упрощает интеграцию с существующим кодом. Специфичные для JAX-RS компоненты также могут быть внедрены в подресурсы с помощью ResourceContext
:
@Context ResourceContext rc; @Path("/roaster/{id}") public RoasterResource roaster(){ return this.rc.initResource(new RoasterResource()); }
Перечисление 2
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:
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); } }
Listing 3
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):
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); } }
Listing 4
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):
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); } }
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):
public class RoasterResource { @Inject Event<RoastRequest> roastListeners; @POST public void roast(@Suspended AsyncResponse ar, Bean bean) { roastListeners.fire(new RoastRequest(bean, ar)); } }
Listing 6
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).
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(); } //... }
Listing 7
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.
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); } //... }
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:
import javax.ws.rs.ext.Provider; @Provider public class PayloadVerifier implements ReaderInterceptor, WriterInterceptor { }
Listing 9
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.
@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(); //... } }
Listing 10
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).
@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); } } }
Listing 11
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:
@NameBinding @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface Tracked { }
Listing 12
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:
@Tracked @Provider public class TrafficLogger implements ContainerRequestFilter, ContainerResponseFilter { }
Listing 13
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:
@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()); } } }
Listing 14
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).
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)); } //.. }
Listing 15
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:
@XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) public class Bean { private String name; private RoastType type; private String blend; }
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 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.
@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")); }
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 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:
@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) { } }); }
Listing 18
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:
@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")); }
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: 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
- RESTEasy
- Jersey
- JAX-RS 2.0 specification
- Real World Java EE Patterns—Rethinking Best Practices
- HTTP RFC
- Java EE 6 Observer
- Digester
About the Author
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.