Java RESTful Web Service 实战

阅读数:9010 2016 年 8 月 20 日 01:29

编者按: InfoQ 开设栏目“品味书香”,精选技术书籍的精彩章节,以及分享看完书留下的思考和收获,欢迎大家关注。本文节选自韩陆著《Java RESTful Web Service 实战》中的章节“REST API 设计”,介绍REST 请求流程等内容。

3.3 REST 请求流程

从 REST 请求处理的扩展点上,我们已经讲述了用于实体处理的 Provider 接口以及上下文处理和异常处理的 Provider。本章还将讲述两种在面向切面编程中非常重要的特殊 Provider:过滤器(3.4 节)和拦截器(3.5 节)。在进入这个主题之前,我们需要对 REST 请求处理的流程这条线有明确的认识,如图 3-5 所示,这样以来,才会知道这些点都处于流程的什么位置。只有这样才能清楚地实现对扩展点的开发和调试。

在图 3-5 中,请求流程中存在 3 种角色,分别是用户、REST 客户端和 REST 服务器。请求始于请求的发送,止于调用 Response 类的 readeEntity() 方法,获取响应实体。

1)用户提交请求数据,客户端接收请求,进入第一个扩展点:“客户端请求过滤器 ClientRequestFilter 实现类”的 filter() 方法。

2)请求过滤处理完毕后,流程进入第二个扩展点:“客户端写拦截器 WriterInterceptor 实现类”的 aroundWriteTo() 方法,实现对客户端序列化操作的拦截。

3)“客户端消息体写处理器 MessageBodyWriter”执行序列化,流程从客户端过渡到服务器端。

4)服务器接收请求,流程进入第三个扩展点:“服务器前置请求过滤器 Container-RequestFilter 实现类”的 filter() 方法。

Java RESTful Web Service实战

图 3-5 Jersey 的 REST 请求处理流程

5)过滤处理完毕后,服务器根据请求匹配资源方法,如果匹配到相应的资源方法,流程进入第四个扩展点:“服务器后置请求过滤器 ContainerRequestFilter 实现类”的 filter() 方法。

6)后置请求过滤处理完毕后,流程进入第五个扩展点:“服务器读拦截器 ReaderInterceptor 实现类”的 aroundReadFrom() 方法,拦截服务器端反序列化操作。

7)“服务器消息体读处理器 MessageBodyReader”完成对客户端数据流的反序列化。服务器执行匹配的资源方法。

8)REST 请求资源的处理完毕后,流程进入第六个扩展点:“服务器响应过滤器 ContainerResponseFilter 实现类”的 filter() 方法。

9)过滤处理完毕后,流程进入第七个扩展点:“服务器写拦截器 WriterInterceptor 实现类”的 aroundWriteTo() 方法,对服务器端序列化到客户端这个操作的拦截。

10)“服务器消息体写处理器 MessageBodyWriter”执行序列化,流程返回到客户端一侧。

11)客户端接收响应,流程进入第八个扩展点:“客户端响应过滤器 Client-ResponseFilter 实现类”的 filter() 方法。

12)过滤处理完毕后,客户端响应实例 response 返回到用户一侧,用户执行 response.readEntity() 流程进入第九个扩展点:“客户端读拦截器 ReaderInterceptor 实现类”的 aroundReadFrom() 方法,对客户端反序列化进行拦截。

13)“客户端消息体读处理器 MessageBodyReader”执行反序列化,将 Java 类型的对象最终作为 readEntity() 方法的返回值。到此,一次 REST 请求处理的完整流程完毕。这期间,如果出现异常或资源不匹配等情况,会从出错点开始结束流程。

3.4 REST 过滤器

从上一节的流程讲述中,我们了解 JAX-RS2 定义的 4 种过滤器扩展点(Extension Point)接口,供开发者实现业务逻辑,按请求处理流程的先后顺序为:客户端请求过滤器(ClientRequestFilter)-> 服务器请求过滤器(ContainerRequestFilter)-> 服务器响应过滤器(ContainerResponseFilter)-> 客户端响应过滤器(ClientResponseFilter)。本节将全面讲述 4 种过滤器的使用。

3.4.1 ClientRequestFilter

客户端请求过滤器(ClientRequestFilter)定义的过滤方法 filter() 包含一个输入参数,是客户端请求的上下文类 ClientRequestContext。从该上下文中可以获取请求信息,典型的示例包括获取请求方法 context.getMethod(),获取请求资源地址 context.getUri() 和获取请求头信息 context.getHeaders() 等。过滤器的实现类中可以利用这些信息,覆写该方法以实现该类特有的过滤功能。ClientRequestFilter 接口的实现类如图 3-6 所示。

Java RESTful Web Service实战

图 3-6 ClientRequestFilter 接口的实现类

图 3-6 展所示了 ClientRequestFilter 接口的实现类,包括 Jersey 内部提供的实现类和本书示例代码中的实现类。我们选择 HTTP 认证过滤器类 HttpAuthenticationFilter 作为例子,来感受上面的讲述,(HTTP 基本认证的内容请参考 10.1 节。)示例代码如下所示。

复制代码
@Override
public void filter(ClientRequestContext request) throws IOException {
if(!"true".equals(request.getProperty("org.glassfish.jersey.client.authentication.HttpAuthenticationFilter.reused"))) {
if(!request.getHeaders().containsKey("Authorization")) {
HttpAuthenticationFilter.Type operation = null;
if(this.mode == Mode.BASIC_PREEMPTIVE) {
this.basicAuth.filterRequest(request);
operation = HttpAuthenticationFilter.Type.BASIC;
...
if(operation != null) {
request.setProperty("org.glassfish.jersey.client.authentication.HttpAuthenticationFilter.operation", operation);
}

在这段代码中,HTTP 基本认证过滤器类在 filter() 方法中,判断请求头信息中是否包含"Authorization",如果不包含则通过 filterRequest() 方法添加请求头"Authorization"为 authentication,authentication 的内容是"Basic" + Base64.encodeAsString(usernamePassword)。这样以来,经过 HTTP 基本认证过滤器类过滤处理后,可以确保请求头信息中包含"Authorization"。

3.4.2 ContainerRequestFilter

针对过滤切面,服务器请求过滤器接口 ContainerRequestFilter 的实现类可以定义为预处理和后处理。默认情况下,采用后处理方式。即先执行容器接收请求操作,当服务器接收并处理请求后,流程才进入过滤器实现类的 filter() 方法。而预处理是在服务器处理接收到的请求之前就执行过滤。如果希望实现一个预处理的过滤器实现类,需要在类名上定义注解 @PreMatching。

服务器请求过滤器定义的过滤方法 filter() 包含一个输入参数,即容器请求上下文类 ContainerRequestContext。ContainerRequestFilter 接口的实现类,如图 3-7 所示。

Java RESTful Web Service实战

图 3-7 ContainerRequestFilter 接口的实现类

图 3-7 展示了 ContainerRequestFilter 接口的实现类,我们以 CsrfProtectionFilter 为例来说明,示例代码如下所示。

复制代码
package org.glassfish.jersey.server.filter;
@Priority(Priorities.AUTHENTICATION) //post-matching
public class CsrfProtectionFilter implements ContainerRequestFilter {
public static final String HEADER_NAME = "X-Requested-By";
// 关注点 1 忽略方法集合
private static final Set<String> METHODS_TO_IGNORE;
static {
HashSet<String> mti = new HashSet<>();
mti.add("GET");
mti.add("OPTIONS");
mti.add("HEAD");
METHODS_TO_IGNORE = Collections.unmodifiableSet(mti);
}
@Override
public void filter(ContainerRequestContext rc) throws IOException {
// 关注点 2 判断方法名称是否符合条件
if (!METHODS_TO_IGNORE.contains(rc.getMethod()) && !rc.getHeaders().containsKey(HEADER_NAME)) {
throw new BadRequestException();
}
}
}

在这段代码中,CsrfProtectionFilter 定义了一个特殊的头信息"X-Requested-By"和 CSRF 忽略监控的方法集合,见关注点 1。在过滤器的 filter() 方法中,首先从上下文中获取头信息 rc.getHeaders() 和请求方法信息 rc.getMethod(),然后判断头信息是否包含"X-Requested-By",方法信息是否是安全的请求方法,即"GET"、"OPTIONS"或"HEAD"。如果两个条件都不成立,过滤器会抛出一个运行时异常 BadRequestException,见关注点 2。通过 CsrfProtectionFilter 过滤器,可以确保请求是 CSRF 安全的。

阅读指南

CsrfProtectionFilter 类使用了注解 @Priority(Priorities.AUTHENTICATION) 来定义该类,明确了该过滤器具有最高的优先级。同时,以注解的文字告诉开发者,需要将其放在过滤器链的第一个位置。因此,在定义和使用过滤器时,需要考虑运行中过滤器的执行先后顺序,否则无法实现过滤器的功能或者使流程混乱。

3.4.3 ContainerResponseFilter

服务器响应过滤器接口 ContainerResponseFilter 定义的过滤方法 filter() 包含两个输入参数,一个是容器请求上下文类 ContainerRequestContext,另一个是容器响应上下文类 ContainerResponseContext。ContainerResponseFilter 接口的实现类如图 3-8 所示。

Java RESTful Web Service实战

图 3-8 ContainerResponseFilter 接口的实现类

图 3-8 展示了 ContainerResponseFilter 接口的实现类,我们以 EncodingFilter 为例来说明。该过滤器的作用是完成内容协商中编码匹配的工作(内容协商这个知识点请参考 3.6 节),示例代码如下所示。

复制代码
@Priority(Priorities.HEADER_DECORATOR)
public final class EncodingFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext request, ContainerResponseContext response) throws IOException {
...
List<String> varyHeader = ((ContainerResponse) response).getStringHeaders().get(HttpHeaders.VARY);
// 关注点 1:Vary 头信息
if (varyHeader == null || !varyHeader.contains(HttpHeaders.ACCEPT_ENCODING)) {
response.getHeaders().add(HttpHeaders.VARY, HttpHeaders.ACCEPT_ENCODING);
}
...
// 关注点 1:Content-Encoding 头信息
if (!IDENTITY_ENCODING.equals(contentEncoding)) {
response.getHeaders().putSingle(HttpHeaders.CONTENT_ENCODING, contentEncoding);
}
}

EncodingFilter 过滤器的 filter() 方法通过对请求头信息“Accept-Encoding”的分析,先后为响应头信息“Vary”和“Content-Encoding”赋值,以实现编码部分的内容协商。见关注点 1。

3.4.4 ClientResponseFilter

客户端响应过滤器(ClientResponseFilter)定义的过滤方法 filter() 包含两个输入参数,一个是客户端请求的上下文类 ClientRequestContext,另一个是客户端响应的上下文类 ClientResponseContext。ClientResponseFilter 接口的实现类,如图 3-9 所示。

Java RESTful Web Service实战

图 3-9 ClientResponseFilter 接口的实现类

图 3-9 展示了 ClientResponseFilter 接口的实现类,包括 Jersey 内部提供的实现类和本书示例代码中的实现类。我们以 HTTP 摘要认证过滤器类 HttpAuthenticationFilter 为例进行演示,(HTTP 摘要认证请参考 10.1 节。)示例代码如下所示。

复制代码
@Override
public void filter(ClientRequestContext request, ClientResponseContext response) throws IOException {
...
if(response.getStatus() == Status.UNAUTHORIZED.getStatusCode()) {
String operation = (String)response.getHeaders().getFirst("WWW-Authenticate");
if(operation != null) {
String success = operation.trim().toUpperCase();
if(success.startsWith("BASIC")) {
result = HttpAuthenticationFilter.Type.BASIC;
} else {
if(!success.startsWith("DIGEST")) {
return;
}
result = HttpAuthenticationFilter.Type.DIGEST;
}
}
authenticate = true;
} else {
authenticate = false;
}
if(this.mode != Mode.BASIC_PREEMPTIVE) {
if(this.mode == Mode.BASIC_NON_PREEMPTIVE) {
if(authenticate && result == HttpAuthenticationFilter.Type.BASIC) {
this.basicAuth.filterResponseAndAuthenticate(request, response);
}
} else if(this.mode == Mode.DIGEST) {
if(authenticate && result == HttpAuthenticationFilter.Type.DIGEST) {
this.digestAuth.filterResponse(request, response);
}
}
...
}

3.4.5 访问日志

3.4.1〜3.4.5 节完成了对 JAX-RS2 定义的 4 种过滤器的讲述,本节利用上述知识,演示如何综合运用过滤器,完成一个记录 REST 请求的访问日志。

1. 访问日志实现类

访问日志类 AirLogFilter 实现了上述的 4 种过滤器,旨在记录服务器和客户端的请求和响应运行时的信息。AirLogFilter 类定义如下所示。

复制代码
@PreMatching
public class AirLogFilter implements ContainerRequestFilter, ClientRequestFilter,
ContainerResponseFilter, ClientResponseFilter {

AirLogFilter 为每一种过滤器接口定义的 filter() 方法提供了实现。在客户端请求过滤中,输出请求资源地址信息和请求头信息;在容器请求过滤中,输出请求方法、请求资源地址信息和请求头信息;在容器响应过滤中,输出 HTTP 状态码和请求头信息;在客户端响应过滤中,输出 HTTP 状态码和请求头信息。4 个阶段的 filter() 示例代码如下所示。

复制代码
@Override
public void filter(ClientRequestContext context) throws
IOException {
long id = logSequence.incrementAndGet();
StringBuilder b = new StringBuilder();
// 关注点 1:获取请求方法和地址
printRequestLine(CLIENT_REQUEST, b, id, context.getMethod(), context.getUri());
// 关注点 2:获取请求头信息
printPrefixedHeaders(CLIENT_REQUEST, b, id, HeadersFactory.asStringHeaders (context.getHeaders()));
LOGGER.info(b.toString()); }

在这段代码中,AirLogFilter 类实现了客户端响应过滤。从客户端响应上下文实例中,可以获取到响应状态信息和响应头信息。分别见关注点 1 和关注点 2。

复制代码
@Override
public void filter(ContainerRequestContext context) throws IOException {
long id = logSequence.incrementAndGet();
StringBuilder b = new StringBuilder();
// 关注点 1:获取容器请求方法和请求地址信息
printRequestLine(SERVER_REQUEST, b, id, context.getMethod(), context.getUriInfo().getRequestUri());
// 关注点 2:获取请求头信息
printPrefixedHeaders(SERVER_REQUEST, b, id, context.getHeaders());
LOGGER.info(b.toString());
}

在这段代码中,AirLogFilter 类实现了容器请求过滤。从容器请求上下文实例中,可以获取到请求方法和请求资源地址信息,见关注点 1。同样,可以从中获取请求头信息,见关注点 2。

复制代码
@Override
public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) throws IOException {
long id = logSequence.incrementAndGet();
StringBuilder b = new StringBuilder();
// 关注点 1:获取容器响应状态
printResponseLine(SERVER_RESPONSE, b, id, responseContext.getStatus());
// 关注点 2:获取容器响应头信息
printPrefixedHeaders(SERVER_RESPONSE, b, id, HeadersFactory.asStringHeaders(responseContext.getHeaders()));
LOGGER.info(b.toString());
}

在这段代码中,AirLogFilter 类实现了容器响应过滤。从容器响应上下文实例中,可以获取到容器的响应状态信息和响应头信息。分别见关注点 1 和关注点 2。 2. 单元测试类

访问日志的单元测试示例如下所示。

复制代码
public class TIResourceJtfTest extends JerseyTest {
@Override
protected Application configure() {
ResourceConfig config = new ResourceConfig(BookResource.class);
return config.register(com.example.filter.log.AirLogFilter.class);
}
@Override
protected void configureClient(ClientConfig config) {
config.register(new AirLogFilter());
}

在这段代码中,为了使访问日志类生效,需要测试类 TIResourceJtfTest 在 Jersey 测试框架的服务器端和客户端,分别注册服务日志类 AirLogFilter。 单元测试的结果如下所示,在 4 种过滤器中分别打印了该阶段的日志信息。

复制代码
main - 1 * AirLog - Request received on thread main
1 / GET http://localhost:9998/books/1
1 / Accept: application/json
Grizzly-worker(1) - 2 * AirLog - Request received on thread Grizzly-worker(1)
2 > GET http://localhost:9998/books/1
2 > accept: application/json
...
Grizzly-worker(1) - 3 * AirLog - Response received on thread Grizzly-worker(1)
3 < 200
3 < Content-Type: application/json
main - 4 * AirLog - Response received on thread main
4 \ 200
4 \ Content-Type: application/json
...

3.5 REST 拦截器

拦截器和过滤器的相同点是都是一种在请求—响应模型中,用做切面处理的 Provider。两者的不同除了功能性上的差异(一个用于过滤消息,一个用于拦截处理)之外,形式上也不同。拦截器通常读写成对,而且没有服务器端和客户端的区分。Jersey 提供的拦截器类,如图 3-10 所示。

Java RESTful Web Service实战

图 3-10 读写拦截器的实现类

在图 3-10 中,Jersey 内部实现了几个典型应用的拦截器,它们是成对出现的。比如 GZiPEncoder 同时实现了读写拦截器,以实现使用 GZip 压缩格式压缩消息体的功能。

1. ReaderInterceptor

读拦截器接口 ReaderInterceptor 定义的拦截方法是 aroundReadFrom(),该方法包含一个输入参数,即读拦截器的上下文接口 ReaderInterceptorContext,从中可以获取头信息、输入流以及父接口 InterceptorContext 提供的媒体类型等上下文信息。接口方法示例如下。

复制代码
public Object aroundReadFrom(ReaderInterceptorContext context) throws java.io.IOException, javax.ws.rs.WebApplicationException;

2. WriterInterceptor

写拦截器接口 WriterInterceptor 定义的拦截方法是 aroundWriteTo(),该方法包含一个输入参数,写拦截器上下文接口 WriterInterceptorContext,从中可以获取头信息、输出流以及父接口 InterceptorContext 提供的媒体类型等上下文信息。接口方法示例如下所示。

复制代码
void aroundWriteTo(WriterInterceptorContext context) throws java.io.IOException, javax.ws.rs.WebApplicationException;

3. 编解码约束拦截器

编解码约束拦截器类 ContentEncoder 是一个位于 org.glassfish.jersey.spi 包中的拦截器,SPI 包下的工具是可插拔的。ContentEncoder 拦截器用于约束序列化和反序列化的过程中,编解码的内容协商,示例代码如下所示。

复制代码
@Override
public final Object aroundReadFrom(ReaderInterceptorContext context)
throws IOException, WebApplicationException {
String contentEncoding = context.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING);
// 关注点 1:判断是否包含 Content-Encoding 头信息
if (contentEncoding != null && getSupportedEncodings().contains(contentEncoding)) {
// 关注点 2:解码处理
context.setInputStream(decode(contentEncoding, context.getInputStream()));
}
// 关注点 3:继续拦截链的下一个拦截处理
return context.proceed();
}
@Override
public final void aroundWriteTo(WriterInterceptorContext context)
throws IOException, WebApplicationException {
String contentEncoding = (String) context.getHeaders().getFirst (HttpHeaders.CONTENT_ENCODING);
// 关注点 1:判断是否包含 Content-Encoding 头信息
if (contentEncoding != null && getSupportedEncodings().contains (contentEncoding)){
// 关注点 2:编码处理
context.setOutputStream(encode(contentEncoding, context.getOutputStream()));
}
// 关注点 3:继续拦截链的下一个拦截处理
context.proceed();
}

在这段代码中,分别给出了 ContentEncoder 拦截器的读、写拦截处理,只有当头信息包含“Content-Encoding”信息,编解码才被执行,见关注点 1。读取阶段进行解码,写入阶段进行编码,见关注点 2。上下文的 proceed() 方法用于执行拦截器链的下一个拦截器,见关注点 3。

3.6 绑定机制

在我们了解了面向切面的 Providers 的功能后,需要掌握它们是如何加载的,以及其作用域。这些容器级别的 Providers,通常使用编码的方式注册到 Application 中,但这不是唯一的办法。本节将详细讨论 Providers 的绑定机制。 默认情况下,过滤器和拦截器都是全局绑定的。也就是说,如下之一的过滤器或拦截器是全局有效的。

  • 通过手动注册到 Application 或者 Configuration;
  • 注解为 @Provider,被自动探测。

下面介绍其他的绑定机制。

3.6.1 名称绑定

过滤器或拦截器可以使用特定的注解来指定其作用范围,这种特定的注解被称为名称绑定。

1. 名称绑定注解

使用 @NameBinding 注解可以定义一个运行时的自定义注解,该注解用于定义类级别名称和类的方法名,代码示例如下所示。

复制代码
@NameBinding
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(value = RetentionPolicy.RUNTIME)
public @interface AirLog {
}

在这段代码中,自定义注解 AirLog 使用了 @NameBinding,在运行时该注解将被解析为一个名称绑定的注解。

2. 绑定 Provider

在定义了 @AirLog 注解后,既可以在 Provider 中使用该注解,示例代码如下所示。

复制代码
// 关注点 1:使用自定义注解 @AirLog
@AirLog
@Priority(Priorities.USER)
public class AirNameBindingFilter implements ContainerRequestFilter, ContainerResponseFilter {
private static final Logger LOGGER = Logger.getLogger(AirNameBindingFilter.class);
public AirNameBindingFilter() {
LOGGER.info("Air-NameBinding-Filter initialized");
}
@Override
// 关注点 2:filter 实现访问日志
public void filter(final ContainerRequestContext containerRequest) throws IOException {
LOGGER.debug("Air-NameBinding-ContainerRequestFilter invoked:" +
containerRequest.getMethod());
LOGGER.debug(containerRequest.getUriInfo().getRequestUri());
}
@Override
// 关注点 3:filter 实现访问日志
public void filter(ContainerRequestContext containerRequest,
ContainerResponseContext responseContext) throws IOException {
LOGGER.debug("Air-NameBinding-ContainerResponseFilter invoked:" +
containerRequest.getMethod());
LOGGER.debug("status=" + responseContext.getStatus());
}
}

在这段代码中,过滤器类 AirNameBindingFilter 使用了自定义注解 @AirLog,这样 AirNameBindingFilter 类就实现了名称绑定,见关注点 1。该类实现了容器的请求和响应过滤器接口,功能是记录访问日志,见关注点 2。

3. 绑定方法

接下来,我们在资源方法级别使用自定义注解 @AirLog,来实现在资源类的指定方法上启用 AirNameBindingFilter 过滤器,示例代码如下所示。

复制代码
@Path("books")
public class BookResource {
// 关注点 1:绑定方法
@AirLog
@GET
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Books getBooks() {
...
return books;
}
...

在这段代码中,资源类 BookResource 包含多个方法,我们只在 getBooks() 方法上使用了注解 @AirLog,而其他方法并没有绑定,见关注点 1。

4. 单元测试类

接下来,通过单元测试来校验名称绑定的设计和实现是否正确,示例代码如下所示。

复制代码
public class TestNamingBinding extends JerseyTest {
@Override
protected Application configure() {
// 关注点 1:AirAopConfig 内部注册了 AirNameBindingFilter
return new AirAopConfig();
}
@Test
// 关注点 2:测试 getBookByPath() 方法
public void testPathGetJSON() {
final WebTarget pathTarget = target(BASE_URI).path("1");
final Invocation.Builder invocationBuilder = pathTarget.request (MediaType.APPLICATION_JSON_TYPE);
final Book result = invocationBuilder.get(Book.class);
Assert.assertNotNull(result.getBookId());
}
@Test
// 关注点 3:测试 getBooks() 方法
public void testGetAll() {
final Invocation.Builder invocationBuilder = target(BASE_URI).request();
final Books result = invocationBuilder.get(Books.class);
Assert.assertNotNull(result.getBookList());
}
}

在这段代码中,测试类 TestNamingBinding 通过 AirAopConfig 注册了 AirNameBindingFilter 过滤器,见关注点 1。该类包含两个测试方法,分别是测试资源类 BookResource 的 getBooks() 和 getBookByPath() 两个方法,见关注点 2 和关注点 3。

我们可以从终端打印的信息来检验名称绑定的运行结果,示例如下。

复制代码
Air-NameBinding-ContainerRequestFilter invoked:GET
http://localhost:9998/books/
Air-NameBinding-ContainerResponseFilter invoked:GET
status=200

从上述测试结果中可以看到,只有在 testGetAll() 方法输出的日志中输出了 AirNameBindingFilter 类中定义的日志信息。这和预期的“只有使用注解 @AirLog 定义的方法,才会在请求流程中启用相应的 Provider”一致。

3.6.2 动态绑定

名称绑定需要通过自定义的注解名称来绑定 Provider 和扩展点方法或者类,相比而言,动态绑定无须新增注解,而是使用编码的方式,实现动态特征接口 javax.ws.rs.container.DynamicFeature,定义扩展点方法的名称、请求方法类型等匹配信息。在运行期,一旦 Provider 匹配当前处理类或方法,面向切面的 Provider 方法即被触发。

1. 定义绑定 Provider

复制代码
AirDynamicFeature 类实现了 DynamicFeature 接口,示例代码如下。
public class AirDynamicFeature implements DynamicFeature {
@Override
public void configure(final ResourceInfo resourceInfo, final FeatureContext context) {
boolean classMatched = BookResource.class.isAssignableFrom(resourceInfo.getResourceClass());
boolean methodNameMatched = resourceInfo.getResourceMethod().getName().contains("getBookBy");
boolean methodTypeMatched = resourceInfo.getResourceMethod().isAnnotationPresent(POST.class);
// 关注点 1:匹配成功才注册 AirDynamicBindingFilter
if (classMatched &&(methodNameMatched || methodTypeMatched)) {
context.register(AirDynamicBindingFilter.class);
}
}
}
public class AirDynamicBindingFilter implements ContainerRequestFilter {
@Override
public void filter(final ContainerRequestContext requestContext) throws IOException {
AirDynamicBindingFilter.LOGGER.debug("Air-Dynamic-Binding-Filter invoked");
}
}

在这段代码中,在 AirDynamicFeature 的配置方法中,启用了如下匹配规则。

1)类匹配:对 BookResource 类及其子类的匹配。 2)方法名称匹配:方法名包含 getBookBy() 的匹配。 3)请求方法类型匹配:与 POST 方法的匹配。

只有当匹配成功时,才会注册 AirDynamicBindingFilter。对于 Provider 的实现类,并没有特殊的要求。

2. 单元测试类

测试类 TestDynamicBinding 注册了动态绑定特征实现类 AirDynamicFeature,示例代码如下所示。

复制代码
public class TestDynamicBinding extends JerseyTest {
@Override
protected Application configure() {
ResourceConfig config = new ResourceConfig(BookResource.class);
config.register(AirDynamicFeature.class);
return config;
}
@Test
public void testPost() {
final Book newBook = new Book("Java Restful Web Service 使用指南 -" + System.nanoTime());
final Entity<Book> bookEntity = Entity.entity(newBook, MediaType.APPLICATION_JSON_TYPE);
final Book savedBook = target(BASE_URI).request(MediaType.APPLICATION_JSON_TYPE).post(bookEntity, Book.class);
Assert.assertNotNull(savedBook.getBookId());
}
}

运行测试方法,AirDynamicBindingFilter 的日志信息如预期输出,示例代码如下所示。

复制代码
Air-Dynamic-Binding-Filter initialized
Air-Dynamic-Binding-Filter invoked

书籍介绍

Java RESTful Web Service实战本书系统、深度讲解了如何基于 Java 标准规范实现 REST 风格的 Web 服务,由拥有 10 余年开发经验的阿里云大数据架构师撰写,第 1 版上市后广获赞誉,成为该领域的畅销书。第 2 版对全书进行了优化和重构,不仅根据新的技术版本对原有过时内容进行了更新,而且还根据整个技术领域的发展增添了新的内容。除此之外,还对第 1 版中存在的不足进行了优化,使得内容更加与时具进、更加有价值。不仅深刻解读了新的 JAX-RS 标准和其 API 设计,以及 Jersey 的使用要点和实现原理,还系统讲解了 REST 的基本理论,更重要的是从实践角度深度讲解了如何基于 Jersey 实现完整的、安全的、高性能的 REST 式的 Web 服务,书中包含大量示例代码,实战性强。

评论

发布