Статьи

Java EE 7 и JAX-RS 2.0

Эта статья является частью серии, в которой представлены новые функции и возможности 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или @StatelessEJB. Кроме того, все корневые ресурсы, поставщики и 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

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.