Java EE 7 和 JAX-RS 2.0

作者:Adam Bien

本文是介绍 Java EE 7 新特性和新功能的系列文章的一部分。您可以在 Java.net 了解有关 Java EE Platform 规范的更多信息。

使用 JAX-RS 2.0 的 Java EE 7 提供了几个非常有用的特性,进一步简化了开发过程,可以创建更复杂但更精益化的 Java SE/EE RESTful 应用程序。

2013 年 4 月发布

下载:

下载示例代码 (Zip)

大多数 Java EE 6 应用程序要求远程 API 和自由选择,它们使用的是一种多少带点 RESTful 风格的 JAX-RS 1.0 规范。使用 JAX-RS 2.0 的 Java EE 7 提供了几个非常有用的特性,进一步简化了开发过程,可以创建更复杂但更精益化的 Java SE/EE RESTful 应用程序。

Roast House

Roast House 是一个 Java 友好但更为简单的 JAX-RS 2.0 示例,它管理和烘焙咖啡豆。Roast House 本身表示为 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 管理的 bean。所有使用 @Provider 批注进行批注的扩展中也可以使用注入,从而简化了与现有代码的集成。JAX-RS 特定的组件也可以使用 ResourceContext 注入子资源中:

    @Context
    ResourceContext rc;

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

清单 2

有趣的是,javax.ws.rs.container.ResourceContext 不仅允许将 JAX-RS 信息注入现有实例中,还允许您使用 ResourceContext#getResource(Class<T> resourceClass) 方法访问资源类。JAX-RS 运行时根据当前上下文的值设置传递到 ResourceContext#initResource 方法的实例的注入点。RoasterResource 类(如清单 3 所示)中的 String id 字段接收父资源的路径参数的值:

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

清单 3

javax.ws.rs.container.AsyncResponse 参数类似于 Servlet 3.0 javax.servlet.AsyncContext 类,允许异步执行请求。在上例中,在处理期间挂起请求,通过调用 AsyncResponse#resume 方法将响应推送至客户端。roast 方法仍然同步执行,因此异步执行根本未产生任何异步行为。但是,EJB 的 @javax.ejb.Asynchronous 批注和 @Suspended AsyncResponse 相结合,可以异步执行业务逻辑并最终通知相关的客户端。任何 JAX-RS 根资源均可使用 @Stateless@Singleton 批注进行批注,并且实际上可以充当 EJB(参见清单 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);
    }
}

清单 4

@Suspended AsyncResponse 参数的 @Asynchronous 资源方法以触发即忘的方式执行。尽管处理请求的线程会立即释放,AsyncResponse 还是为客户端提供了一个方便的句柄。完成耗时的工作之后,结果可以很方便地推回到客户端。通常,您可能希望区分 JAX-RS 特定的行为和实际的业务逻辑。可以将所有业务逻辑轻松提取到一个专用边界 EJB,但 CDI 事件处理更适合覆盖触发即忘的情况。自定义事件类 RoastRequest 携带有效负载(Bean 类)作为处理输入,携带 AsyncResponse 用于提交结果(参见清单 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);
    }
}

清单 5

CDI 事件不仅实现了业务逻辑与 JAX-RS API 的分离,还大大简化了 JAX-RS 代码(参见清单 6):

public class RoasterResource {

    @Inject
    Event<RoastRequest> roastListeners;

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

清单 6

任何 CDI 管理的 bean 或 EJB 均可以发布-订阅方式接收 RoastRequest,并使用一个简单的观察器方法 void onRoastRequest(@Observes RoastRequest request){} 同步或异步处理有效负载。

使用 AsyncResponse 类,JAX-RS 规范就可以轻松将信息实时推送至 HTTP。从客户端的角度来看,服务器上的异步请求仍在阻塞中,因此是同步的。从 REST 设计的角度来看,所有长时间运行的任务均应立即返回 HTTP 状态码 202,以及关于处理完成后如何取得结果的附加信息。

返回方面

流行的 REST API 通常要求客户端计算消息的指纹并将其随请求一起发送。在服务器端,对指纹进行计算并将其与附加信息进行对比。如果二者不匹配,将拒绝消息。随着 JAX-RS 的出现以及 javax.ws.rs.ext.ReaderInterceptor javax.ws.rs.ext.WriterInterceptor 的引入,现在可以在服务器端甚至客户端拦截流量。服务器上的 ReaderInterceptor 接口实现包装了 MessageBodyReader#readFrom,在实际序列化之前执行。

PayloadVerifier 从标头提取签名,从流计算指纹,最终调用 ReaderInterceptorContext#proceed 方法,该方法调用链中的下一个拦截器或 MessageBodyReader 实例(参见清单 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();
    }
    //...    
}

清单 7

修改的内容产生不同的指纹,并导致使用 BAD_REQUEST (400) 响应代码引发 WebApplicationException

可以使用 WriterInterceptor 实现轻松自动执行所有指纹或传出请求的计算。WriterInterceptor 实现包装了 MessageBodyWriter#writeTo,在实体序列化到流中之前执行。对于指纹计算,需要“传输中的”实体的最终表示,因此传递一个 ByteArrayOutputStream 作为缓冲区,调用 WriterInterceptorContext#proceed() 方法,提取原始内容并计算指纹。参见清单 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);

    }
    //...
}

清单 8

最后,计算的签名作为标头添加到请求,将缓冲区写入原始流,并将整个请求发送到客户端。当然,一个类也可以同时实现这两个接口:

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

清单 9

与以前的 JAX-RS 发布中一样,将自动发现自定义扩展并使用 @Provider 批注进行注册。为了拦截 MessageBodyWriterMessageBodyReader 实例,只需使用 @Provider 批注对 ReaderInterceptorWriterInterceptor 进行批注,无需其他配置或 API 调用。

请求拦截

ContainerRequestFilterContainerResponseFilter 的实现拦截整个请求,不仅仅是读写实体的过程。这两个拦截器的功能不仅仅是记录原始 javax.servlet.http.HttpServletRequest 实例中包含的信息。TrafficLogger 类不仅能记录 HttpServletRequest 中包含的信息,而且能跟踪匹配特定请求的资源的信息,如清单 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();
    //...
    }
}

清单 10

因此,ContainerResponseFilter 的注册实现将获取 ContainerResponseContext 的实例,并能访问服务器生成的数据。它可以轻松访问状态代码和标头内容,例如 Location 标头。ContainerRequestContext 以及 ContainerResponseContext 是可以由筛选器修改的可变类。

无需任何额外配置,即可在 HTTP-资源匹配阶段之后执行 ContainerRequestFilter。此时,无法再修改传入请求来自定义资源绑定。如果您希望影响请求与资源之间的绑定,可以配置 ContainerRequestFilter,使其在资源绑定阶段之前执行。任何使用 javax.ws.rs.container.PreMatching 批注进行批注的 ContainerRequestFilter 都将在资源绑定之前执行,因此可以修改请求内容以获得期望的映射。@PreMatching 筛选器的一个常见用例是调整 HTTP 谓词以克服网络基础架构中的限制。 PUTOPTIONSHEADDELETE 等更“深奥”的方法可能会被防火墙过滤掉,或者不受某些 HTTP 客户端支持。@PreMatching ContainerRequestFilter 实现可以从标头(例如,“X-HTTP-Method-Override”)提取指示所需 HTTP 谓词的信息,并且可以将 POST 请求动态更改为 PUT(参见清单 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);
        }
    }
}

清单 11

配置

使用 @Provider 批注注册的所有拦截器和筛选器针对所有资源全局启用。在部署时,服务器扫描部署单元中的 @Provider 批注,并在激活应用程序之前自动注册所有扩展。可以将所有扩展打包成专用 JAR,并根据需要随 WAR 一起部署(在 WEB-INF/lib 文件夹中)。JAX-RS 运行时讲扫描 JAR 并自动注册扩展。向下部署独立的 JAR 也不错,但需要细粒度的扩展打包。JAR 中包含的所有扩展将立即激活。

为了选择性修饰资源,JAX-RS 引入了绑定批注。其机制类似于 CDI 限定符。由元批注 javax.ws.rs.NameBinding 表示的所有自定义批注可用于声明拦截点:

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

清单 12

Tracked 批注表示的所有拦截器或筛选器都可通过对 Application 的类、方法甚至子类应用相同的 Tracked 批注选择性的激活:

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

清单 13

自定义 NameBinding 批注可以与对应的筛选器或拦截器打包在一起,由应用程序开发人员选择性地应用于资源。尽管批注驱动的方法可以显著提高灵活性并允许较粗放的插件包装,但绑定仍是静态的。要想更改拦截器或筛选器链,需要重新编译应用程序并进行有效地重新部署。

除了对横切功能进行全局和批注驱动的配置,JAX-RS 2.0 还引入了一个新的 API 进行动态扩展注册。容器可以使用由 @Provider 批注进行批注的 javax.ws.rs.container.DynamicFeature 接口的实现作为钩子来动态注册拦截器和筛选器,无需重新编译。LoggerRegistration 扩展可以通过查询是否存在预定义的系统属性,有条件地注册 PayloadVerifier 拦截器和 TrafficLogger 筛选器,如清单 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());
        }
    }
}  

清单 14

客户端

JAX-RS 1.1 规范不包括客户端。尽管客户端 REST API 的专有实现(如 RESTEasy 或 Jersey)可以与任何 HTTP 资源通信(甚至 Java EE 都未实现),客户端代码还是直接依赖于特定的实现。JAX-RS 2.0 引入了一个新的标准化的客户端 API。由于使用标准化的引导,因此服务提供程序接口 (SPI) 是可替换的。API 是流动性的,类似于大多数专有 REST 客户端实现(参见清单 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));
    }
//..
}

清单 15

在上面的集成测试中,使用无参数的 ClientFactory.newClient() 方法获取默认的 Client 实例。引导过程本身使用内部 javax.ws.rs.ext.RuntimeDelegate 抽象工厂进行了标准化。获取 RuntimeDelegate 的办法要么是将其现有实例(例如,通过依赖注入框架)注入 ClientFactory,要么通过查找文件 META-INF/services/javax.ws.rs.ext.RuntimeDelegate${java.home}/lib/jaxrs.properties 中的提示并最终通过搜索 javax.ws.rs.ext.RuntimeDelegate 系统属性获取。如果找不到,将尝试初始化默认的 (Jersey) 实现。

javax.ws.rs.client.Client 的主要目的是可以流畅地访问 javax.ws.rs.client.WebTargetjavax.ws.rs.client.Invocation 实例。WebTarget 表示一个 JAX-RS 资源,Invocation 是一个随时可用的等待提交的请求。WebTarget 也是一个 Invocation 工厂。

CoffeBeansResourceTest#crud() 方法中,Bean 对象在客户端和服务器之间来回传递。如果选择 MediaType.APPLICATION_XML,则只需几个 JAXB 批注发送和接收 XML 文档中序列化的 DTO:

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

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

}

清单 16

类和属性的名称必须匹配,才能与服务器的表示成功编组,但 DTO 不一定非要与二进制兼容。在上例中,两个 Bean 类位于不同的软件包中,甚至实现的是不同的方法。将所需的 MediaType 传递到 WebTarget#request() 方法,再由该方法返回一个同步 Invocation.Builder 的实例。最后,调用一个以 HTTP 谓词(GETPOSTPUTDELETEHEADOPTIONSTRACE)命名的方法将发起同步请求。

新的客户端 API 还支持异步资源调用。如前所述,Invocation 实例将请求与提交分离。可以使用链接的 async() 方法调用发起异步请求,该方法返回一个 AsyncInvoker 实例。参见清单 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"));
    }

清单 17

上例中“准异步”的通信方式并没有多大好处,客户端还是必须阻塞并等待响应到达。但基于 Future 的调用对批处理非常有用:客户端可以一次发出多个请求,收集 Future 实例,稍后进行处理。

可通过回调注册实现真正的异步实现,如清单 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) {
            }
        });
    }

清单 18

对于每个返回 Future 的方法,也有一个对应的回调方法可用。InvocationCallback 接口的实现作为方法(在上例中,是 post())的最后一个参数被接受,并在使用有效负载或(在失败时)使用异常成功调用时异步收到通知。

可以通过内置模板化机制简化 URI 的自动化构造。预定义的占位符可以就在请求执行之前替换,这样就不必重复创建 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"));
    }

清单 19

下面是一个很小但很重要的细节:在客户端,在初始化时并未发现扩展;而必须使用 Client 实例对其显式进行注册:ClientFactory.newClient().register(PayloadVerifier.class)。不过,可以在客户端和服务器之间共享同一实体拦截器实现,从而简化测试、减少潜在的问题并提高工作效率。已经引入的 PayloadVerifier 拦截器可以重用,也无需在客户端进行任何更改。

总结:用不用 Java EE?

有趣的是,JAX-RS 甚至不要求成熟的应用服务器。实现指定的上下文类型之后,对 JAX-RS 2.0 兼容 API 就别无他求了。但与 EJB 3.2 组合带来了异步处理、池化(及由此产生的调节)和监视。通过与 Servlet 3+ 的紧密集成,可以通过 AsyncContext 支持高效地对 @Suspended 响应进行异步处理,且 CDI 运行时带来了事件化。而且与 Bean Validation 的集成也很紧密,可以用于验证资源参数。将 JAX-RS 2.0 与其他 Java EE 7 API 一起使用是最方便(=零配置)、工作效率最高(=完全无需重新创造)的向远程系统公开对象的方式。

另请参见

关于作者

顾问兼作者 Adam Bien 是 Java EE 6/7、EJB 3.X、JAX-RS 和 JPA 2.X JSR 专家组成员。他从 JDK 1.0 就开始使用 Java 技术,并使用了 servlet/EJB 1.0,目前是 Java SE 和 Java EE 项目的架构师和开发人员。他编辑了多本关于 JavaFX、J2EE 和 Java EE 的图书,并且是《Real World Java EE PatternsRethinking Best Practices》《Real World Java EE Night Hacks》两本书的作者。Adam 还是 Java Champion、Top Java Ambassador 2012 和 JavaOne 2009、2011 及 2012 Rock Star。Adam 组织了在慕尼黑机场的临时 Java (EE) 研讨会。

分享交流

请在 FacebookTwitterOracle Java 博客上加入 Java 社区对话!