文章
Java
作者:Adam Bien
本文是介绍 Java EE 7 新特性和新功能的系列文章的一部分。您可以在 Java.net 了解有关 Java EE Platform 规范的更多信息。
使用 JAX-RS 2.0 的 Java EE 7 提供了几个非常有用的特性,进一步简化了开发过程,可以创建更复杂但更精益化的 Java SE/EE RESTful 应用程序。
2013 年 4 月发布
下载:
大多数 Java EE 6 应用程序要求远程 API 和自由选择,它们使用的是一种多少带点 RESTful 风格的 JAX-RS 1.0 规范。使用 JAX-RS 2.0 的 Java EE 7 提供了几个非常有用的特性,进一步简化了开发过程,可以创建更复杂但更精益化的 Java SE/EE RESTful 应用程序。
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 批注进行注册。为了拦截 MessageBodyWriter 和 MessageBodyReader 实例,只需使用 @Provider 批注对 ReaderInterceptor 和 WriterInterceptor 进行批注,无需其他配置或 API 调用。
ContainerRequestFilter 和 ContainerResponseFilter 的实现拦截整个请求,不仅仅是读写实体的过程。这两个拦截器的功能不仅仅是记录原始 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 谓词以克服网络基础架构中的限制。 PUT、OPTIONS、HEAD 或 DELETE 等更“深奥”的方法可能会被防火墙过滤掉,或者不受某些 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.WebTarget 或 javax.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 谓词(GET、POST、PUT、DELETE、HEAD、OPTIONS 或 TRACE)命名的方法将发起同步请求。
新的客户端 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 拦截器可以重用,也无需在客户端进行任何更改。
有趣的是,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) 研讨会。
请在 Facebook、Twitter 和 Oracle Java 博客上加入 Java 社区对话!