Java EE 7 y JAX-RS 2.0

Por Adam Bien
Publicado en enero 2014

Este artículo es parte de una serie en la que se presentan las nuevas características y funcionalidades de Java EE 7. Puede obtener más información sobre la Especificación de la plataforma Java EE en Java.net. 

Java EE 7 con JAX-RS 2.0 aporta varias características útiles, que simplifican aún más el desarrollo y conducen a la creación de aplicaciones con arquitectura RESTful para Java SE/EE aún más sofisticadas pero livianas a la vez.

Descargas:

Código de muestra (ZIP)

La mayoría de las aplicaciones de Java EE 6, que requieren una API remota y ofrecen ///libertad de elección para su configuración, utilizan una especificación de JAX-RS 1.0 que se acerca bastante a las características de la arquitectura RESTful. Java EE 7 con JAX-RS 2.0 aporta varias características útiles, que simplifican aún más el desarrollo y conducen a la creación de aplicaciones con arquitectura RESTful para Java SE/EE aún más sofisticadas pero livianas a la vez.

Roast House

Roast House es un ejemplo sencillo de JAX-RS 2.0 apto para Java que administra y genera coffee beans (componentes de Java). Roast House está representado como un recurso CoffeeBeansResource. El URI "coffeebeans" identifica en forma unívoca el recurso CoffeeBeansResource (ver Listado 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());
    }
}
Listado 1

Como en las especificaciones anteriores de JAX-RS, un recurso puede ser un componente EJB único (@Singleton) o sin estado (@Stateless). Además, todos los proveedores, subclases de Application y recursos raíz pueden implementarse como beans gestionados o gestionados por CDI (inyección de dependencias y contextos). La inyección también está disponible en todas las extensiones que incluyen la anotación @Provider, lo que simplifica la integración con el código existente. Los componentes específicos para JAX-RS también pueden inyectarse en subrecursos mediante ResourceContext:

    @Context
    ResourceContext rc;

    @Path("/roaster/{id}")
    public RoasterResource roaster(){
        return this.rc.initResource(new RoasterResource());
    }
Listado 2

Como dato interesante, javax.ws.rs.container.ResourceContext no solo permite inyectar información de JAX-RS en una instancia existente, sino que brinda acceso a las clases de recursos con el método ResourceContext#getResource(Class resourceClass). Los puntos de inyección de las instancias que se pasan al método ResourceContext#initResource se configuran con valores tomados del contexto correspondiente durante la ejecución de JAX-RS. El campo String id de la clase RoasterResource (que se muestra en el Listado 3) recibe el valor del parámetro de ruta del recurso del ///elemento primario:

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);
    }
}
Listado 3

El parámetro javax.ws.rs.container.AsyncResponse es similar a la clase javax.servlet.AsyncContext de Servlet 3.0 y permite la ejecución de una petición asíncrona. En el ejemplo anterior, la petición se suspende durante el procesamiento y la respuesta se envía al cliente con la invocación del método AsyncResponse#resume. De todos modos, el método roast se ejecuta en forma síncrona, de manera tal que la ejecución asíncrona no acarrea ningún comportamiento asíncrono. Sin embargo, la combinación de la anotación @javax.ejb.Asynchronous del EJB y @Suspended AsyncResponse posibilita la ejecución asíncrona de lógica empresarial con una notificación eventual para el cliente interesado. Cualquier recurso raíz de JAX-RS puede incluir las anotaciones @Stateless o @Singleton y, en efecto, funcionar como un EJB (ver Listado 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);
    }
}
Listado 4

Un método de recurso @Asynchronous con un parámetro @Suspended AsyncResponse se ejecuta en forma fire-and-forget (no requiere acciones posteriores). Aunque el subproceso que se ocupa de las peticiones se libera de inmediato, AsyncResponse sigue siendo un controlador conveniente para el cliente. Al completarse una operación que lleva mucho tiempo, el resultado puede devolverse al cliente en forma conveniente. Por lo general, es deseable separar el comportamiento específico de JAX-RS de la lógica empresarial concreta. Toda la lógica empresarial puede extraerse con facilidad e incorporarse en un EJB dedicado con límites, pero la generación de eventos de CDI es aun más adecuada para contemplar los casos fire-and-forget. La clase de evento personalizada RoastRequest lleva la carga útil (clase Bean) como entrada de procesamiento y AsyncResponse para la entrega resultante (ver Listado 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);
    }
}
Listado 5

Los eventos de CDI no solo separan la lógica empresarial de la API de JAX-RS, sino que simplifican en gran medida el código JAX-RS (ver Listado 6):

public class RoasterResource {

    @Inject
    Event<RoastRequest> roastListeners;

    @POST
    public void roast(@Suspended AsyncResponse ar, Bean bean) {
        roastListeners.fire(new RoastRequest(bean, ar));
    }
}
Listado 6

Cualquier EJB o bean gestionado por CDI puede recibir la petición RoastRequest mediante publicación y suscripción, y procesar en forma síncrona o asíncrona la carga útil con un método sencillo de observador: void onRoastRequest(@Observes RoastRequest request){}.

Con la clase AsyncResponse la especificación de JAX-RS incorpora una forma sencilla de enviar información a HTTP en tiempo real. Desde la perspectiva del cliente, la petición asíncrona en el servidor sigue generando un bloqueo por lo que se comporta en forma síncrona. Desde la perspectiva del diseño REST, todas las tareas de ejecución prolongada deben devolver inmediatamente el código de estado HTTP 202 junto con información adicional sobre cómo obtener el resultado tras completarse el procesamiento.

Devolución de aspectos

Las API de REST más conocidas a menudo requieren que sus clientes computen un código de identificación (fingerprint) del mensaje y lo envíen junto con la petición. En el servidor, el código de identificación se computa y se compara con la información adjunta. Si no hay coincidencia, el mensaje se rechaza. Con JAX-RS y la incorporación de javax.ws.rs.ext.ReaderInterceptor javax.ws.rs.ext.WriterInterceptor, el tráfico puede interceptarse en el servidor e incluso en el cliente. Una implementación de la interfaz ReaderInterceptor en el servidor encapsula MessageBodyReader#readFrom y se ejecuta antes de que se efectúe la serialización.

PayloadVerifier recupera la firma del encabezado, computa el código de identificación del flujo de datos y, posteriormente, invoca el método ReaderInterceptorContext#proceed, que a su vez invoca al siguiente interceptor en la cadena o la instancia MessageBodyReader (ver Listado 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();
    }
    //...    
}
Listado 7

El contenido modificado da como resultado códigos de identificación diferentes y activa la excepción WebApplicationException con el código de respuesta BAD_REQUEST (400).

Todos los cómputos asociados a códigos de identificación o a peticiones salientes pueden automatizarse fácilmente con una implementación de WriterInterceptor. Una implementación de WriterInterceptor encapsula MessageBodyWriter#writeTo y se ejecuta antes de la serialización de la entidad en un flujo de datos. Para computar el código de identificación, se necesita la representación final de la entidad ///"en línea", por lo que se pasa ByteArrayOutputStream como búfer, se invoca el método WriterInterceptorContext#proceed(), se recupera el contenido en bruto y se computa el código de identificación. Vea el Listado 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);

    }
    //...
}
Listado 8

Finalmente, la firma computada se agrega a la petición como encabezado, el búfer se agrega al flujo de datos original, y la petición completa se envía al cliente. Por supuesto, una única clase también puede implementar ambas interfaces al mismo tiempo:

import javax.ws.rs.ext.Provider;
@Provider
public class PayloadVerifier implements ReaderInterceptor, WriterInterceptor {
}
Listado 9

Como en las ediciones anteriores de JAX-RS, las extensiones personalizadas se detectarán automáticamente y se registrarán con la anotación @Provider. Para que se intercepten las instancias MessageBodyWriter y MessageBodyReader, solo las implementaciones de ReaderInterceptor y WriterInterceptor deben incluir la anotación @Provider; no se requieren llamadas a interfaces API ni configuraciones adicionales.

Interceptación de peticiones

Una implementación de un ContainerRequestFilter y un ContainerResponseFilter intercepta la petición completa, no solo el proceso de lectura y escritura de entidades. La funcionalidad de ambos interceptores es mucho más útil que el registro de la información contenida en la instancia javax.servlet.http.HttpServletRequest en bruto. La clase TrafficLogger no solo puede registrar la información contenida en HttpServletRequest, sino que puede hacer un seguimiento de la información relativa a los recursos que se ajustan a una petición particular, como se muestra en el Listado 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();
    //...
    }
}
Listado 10

Así, una implementación registrada de ContainerResponseFilter obtiene una instancia del ContainerResponseContext y puede acceder a los datos generados por el servidor. Se puede acceder con facilidad a los códigos de estado y al contenido de encabezados, por ejemplo, del encabezado Location. ContainerRequestContext y ContainerResponseContext son clases mutables que pueden ser modificadas por los filtros.

Sin ninguna configuración adicional, ContainerRequestFilter se ejecuta después de la fase de establecimiento de coincidencias entre HTTP y los recursos. En este punto del proceso ya no es posible modificar la petición entrante para personalizar el enlace de recursos. En caso de que se desee influir en el enlace entre la petición y un recurso, puede configurarse un filtro ContainerRequestFilter que se ejecute antes de la fase de enlace de recursos. Los filtros ContainerRequestFilter que incluyan la anotación javax.ws.rs.container.PreMatching se ejecutan antes del enlace de recursos, por lo que los contenidos de la petición HTTP pueden modificarse en función de la asignación deseada. Un caso frecuente de uso de filtros @PreMatching es el ajuste de los verbos HTTP para superar limitaciones en la infraestructura de red. Es posible que métodos más extravagantes, como PUT, OPTIONS, HEAD o DELETE, sean filtrados por firewalls; además, algunos clientes HTTP podrían no admitirlos. La implementación de @PreMatching ContainerRequestFilter podría recuperar la información del encabezado (por ejemplo, "X-HTTP-Method-Override") con indicación del verbo HTTP deseado y reemplazar una petición POST por una PUT sobre la marcha (ver Listado 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);
        }
    }
}
Listado 11

Configuración

Todos los interceptores y filtros registrados con la anotación @Provider están habilitados globalmente para todos los recursos. En la etapa de implementación, el servidor revisa las unidades de implementación en busca de anotaciones @Provider y registra automáticamente todas las extensiones antes de la activación de la aplicación. Todas las extensiones pueden empaquetarse en archivos JAR dedicados e implementarse sobre pedido con el archivo WAR (en la carpeta WEB-INF/lib). Durante la ejecución de JAX-RS se revisarán los archivos JAR y se registrarán automáticamente las extensiones. La implementación "improvisada" de archivos JAR autocontenidos puede parecer práctica, pero exige empaquetar cuidadosamente las extensiones. Todas las extensiones contenidas en un archivo JAR se activarían a la vez.

JAX-RS incorpora el uso de anotaciones de enlace para marcar recursos en forma selectiva. La dinámica es similar a la de los calificadores de CDI. Las anotaciones personalizadas señaladas con la meta-anotación javax.ws.rs.NameBinding pueden usarse para la declaración de puntos de interceptación:

@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Tracked {
}
Listado 12

Todos los interceptores o filtros señalados con la anotación Tracked pueden activarse en forma selectiva aplicando la misma anotación Tracked a clases, métodos o incluso a subclases de la aplicación:

@Tracked
@Provider
public class TrafficLogger implements ContainerRequestFilter, ContainerResponseFilter {
}
Listado 13

Las anotaciones NameBinding personalizadas pueden empaquetarse junto con el interceptor o filtro correspondiente, y el desarrollador de la aplicación puede aplicarlas a recursos en forma selectiva. Si bien este enfoque basado en anotaciones aumenta significativamente la flexibilidad y permite paquetes de plug-in menos sofisticados, el enlace sigue siendo estático. La aplicación debe recompilarse y reimplementarse en forma efectiva para cambiar la cadena de filtros o interceptores.

Además de la configuración global basada en anotaciones de ///las funcionalidades transversales, JAX-RS 2.0 también presenta una nueva API para el registro dinámico de extensiones. El contenedor usa una implementación de la interfaz javax.ws.rs.container.DynamicFeature que incluye la anotación @Provider para posibilitar el registro dinámico de interceptores y filtros, sin necesidad de una recompilación. La extensión LoggerRegistration registra en forma condicional el interceptor PayloadVerifier y el filtro TrafficLogger consultando sobre la existencia de propiedades de sistema predefinidas, como se muestra en el Listado 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());
        }
    }
}  
Listado 14

En el cliente

La especificación de JAX-RS 1.1 no contemplaba al cliente. Si bien ciertas implementaciones propietarias de una API REST de cliente, como RESTEasy o Jersey, podían comunicarse con cualquier recurso HTTP (incluso sin que estuvieran implementados con Java EE), el código de cliente dependía directamente de la implementación particular. JAX-RS 2.0 incorpora una nueva API de cliente estandarizada. Si se emplea un ///arranque estandarizado, la Interfaz de proveedor de servicio (SPI) admite su reemplazo. La API es una interfaz fluida y similar a la mayoría de las implementaciones REST propietarias de cliente (ver Listado 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));
    }
//..
}
Listado 15

En la prueba de integración anterior, la instancia Client predeterminada se obtiene con el método ClientFactory.newClient() sin parámetros. El proceso de ///arranque en sí está estandarizado con el patrón de tipo abstract factory (fábrica abstracta) javax.ws.rs.ext.RuntimeDelegate interno. Se inyecta una instancia existente de RuntimeDelegate (por ejemplo, mediante un marco de inyección de dependencias) en ClientFactory, o bien se la obtiene buscando un indicio en los archivos META-INF/services/javax.ws.rs.ext.RuntimeDelegate y ${java.home}/lib/jaxrs.properties, y finalmente buscando la propiedad de sistema javax.ws.rs.ext.RuntimeDelegate. Si falla la detección, se intenta inicializar con una implementación predeterminada (Jersey).

El objetivo principal de javax.ws.rs.client.Client es posibilitar el acceso fluido a las instancias javax.ws.rs.client.WebTarget o javax.ws.rs.client.Invocation. Un WebTarget representa un recurso JAX-RS y una Invocation es una petición lista para usar en espera de la entrega. WebTarget también es un ///método fábrica de Invocation.

En el método CoffeBeansResourceTest#crud(), el objeto Bean se pasa una y otra vez entre cliente y servidor. Con la selección de MediaType.APPLICATION_XML, solo se necesitan unas pocas anotaciones JAXB para enviar y recibir un objeto DTO serializado en un documento XML:

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Bean {

    private String name;
    private RoastType type;
    private String blend;

}
Listado 16

Los nombres de la clase y los atributos deben coincidir para una serialización exitosa con la representación del servidor, pero no es necesario que el objeto DTO tenga compatibilidad binaria. En el ejemplo anterior, ambas clases Bean están ubicadas en paquetes diferentes e incluso implementan métodos diferentes. Se pasa un MediaType deseado al método WebTarget#request(), que devuelve una instancia de un Invocation.Builder síncrono. La invocación final de un método llamado igual que los verbos HTTP (GET, POST, PUT, DELETE, HEAD, OPTIONS o TRACE) inicia una petición síncrona.

La nueva API de cliente también admite la invocación asíncrona de recursos. Como se mencionó antes, una instancia Invocation separa la petición de la entrega. Una petición asíncrona puede iniciarse con la invocación del método async() encadenado, lo que devuelve una instancia AsyncInvoker. Vea el Listado 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"));
    }
Listado 17

El estilo de comunicación "pseudo-asíncrona" del ejemplo anterior no reporta muchos beneficios; el cliente aún debe bloquearse y esperar hasta que llegue la respuesta. Sin embargo, la invocación basada en Future es muy útil para el procesamiento por lotes: el cliente puede generar varias peticiones a la vez, reunir las instancias Future y procesarlas luego.

Puede lograrse una implementación verdaderamente asíncrona con un registro de callbacks (retrollamadas), como se muestra en el Listado 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) {
            }
        });
    }
Listado 18

Para cada método que devuelve un Future, también existe un método de callback. Se acepta una implementación de la interfaz InvocationCallback como el último parámetro del método (post(), en el ejemplo anterior) y se notifica en forma asíncrona al producirse una invocación exitosa con la carga útil o, en caso de error, con una excepción.

La generación automática de identificadores URI puede agilizarse con el mecanismo integrado de creación de plantillas. Los marcadores de posición predefinidos pueden reemplazarse poco antes de la ejecución de la petición y ahorrar la creación repetitiva de instancias WebTarget:

    @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"));
    }
Listado 19

Un detalle pequeño pero importante: en el cliente, las extensiones no se detectan durante la inicialización; deben registrarse explícitamente con la instancia del cliente: ClientFactory.newClient().register(PayloadVerifier.class). Sin embargo, las mismas implementaciones de interceptores de entidades pueden compartirse entre cliente y servidor, lo que simplifica las pruebas, reduce los errores potenciales y aumenta la productividad. El interceptor PayloadVerifier presentado también puede reutilizarse sin cambios en el cliente.

Conclusión: Java EE, ¿sí o no?

Un dato interesante es que JAX-RS ni siquiera requiere un servidor de aplicaciones completo. Si se satisfacen los tipos de contextos especificados, cualquier API puede ser compatible con JAX-RS 2.0. Sin embargo, la combinación con EJB 3.2 aporta las ventajas del procesamiento asíncrono, la agrupación (y en consecuencia la regulación) y el monitoreo. La integración perfecta con Servlet 3+ está acompañada del eficiente procesamiento asíncrono de respuestas @Suspended mediante la compatibilidad con AsyncContext, y la ejecución de CDI posibilita la generación de eventos. Además, la validación de beans está bien integrada y puede usarse para la validación de parámetros de recursos. El uso de JAX-RS 2.0 junto con otras API de Java EE 7 brinda la forma más conveniente (no requiere configuración) y más productiva (no requiere ninguna reinvención) de exponer objetos a sistemas remotos.

Información adicional

 


Adam Bien, consultor y autor, es miembro del grupo de expertos en Java EE 6/7, EJB 3.X, JAX-RS y JPA 2.X JSRs. Ha trabajado con tecnología Java desde JDK 1.0 y con servlets/EJB 1.0, y ahora es diseñador y desarrollador para proyectos de Java SE y Java EE. Ha editado varios libros sobre JavaFX, J2EE y Java EE, y es el autor de Real World Java EE Patterns—Rethinking Best Practices y de Real World Java EE Night Hacks (Ataques informáticos nocturnos reales en Java EE, en inglés). Adam también obtuvo los reconocimientos Java Champion, Top Java Ambassador en 2012, y JavaOne Rock Star en 2009, 2011 y 2012. Ocasionalmente Adam organiza talleres sobre Java (EE) en el aeropuerto de Munich.