写点什么

Java RESTful Web Service 实战

  • 2016-08-20
  • 本文字数:11715 字

    阅读完需:约 38 分钟

编者按: 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() 方法。

图 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 所示。

图 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 所示。

图 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 所示。

图 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 所示。

图 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 所示。

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

2016-08-20 01:2910014

评论

发布
暂无评论
发现更多内容

AI缘起——达特茅斯会议

行者AI

人工智能

揭秘Spring家族之——AOP和IOC

Java架构师迁哥

Mybatis缓存机制详解

北游学Java

Java mybatis

学历不够,技术来凑,8年开发经验,逆袭拿到阿里P7岗

Java架构师迁哥

花了三个小时把一份GitHub上标星115k的《Java超全进阶教程》整理成了PDF文档。

Java架构之路

Java 程序员 架构 面试 编程语言

MySQL next-key lock 加锁范围总结

程序员小航

MySQL 索引 锁机制

Bzz云算力挖矿app开发,Bzz分币系统搭建

Hanoi 塔问题(Java实现)

若尘

数据结构 java编程 6月日更

【译】JavaScript 代码整洁之道-重构篇

KooFE

JavaScript 大前端 代码重构 6月日更 整洁代码

网络攻防学习笔记 Day39

穿过生命散发芬芳

网络攻防 6月日更

Go timer 是如何被调度的?

HHFCodeRv

Go 语言

云图说|OLAP开源引擎的一匹黑马,MRS集群组件之ClickHouse

华为云开发者联盟

Clickhouse MRS 华为云 云图说 OLAP开源引擎

阿里云,让「服务」成为一种先进生产力

ToB行业头条

云计算 阿里云

真香!SpringBoot+SpringCloud Alibaba全套脑图+学习笔记+大厂面试题

Java架构追梦

Java 架构 微服务 springboot SpringCloud

5.7w字?GitHub标星120K的Java面试知识点总结,真就物超所值了

Java 编程 程序员 面试 计算机

双向链表,还能这么实现

实力程序员

百度Geek们教你怎样成为复盘高手

百度Geek说

每日优鲜:AI 技术驱动下的社区新零售

蚂蚁集团移动开发平台 mPaaS

人工智能 算法 图像识别 codehub

企业管理软件开发新模式:抛开旧思维,轻松做系统

雯雯写代码

软件开发 企业管理

深度分享丨如何使用微细分仪打造金融场景下的战术级客户分群

索信达控股

大数据 金融科技 用户细分 客户数据平台 客户画像

上新!H3C Magic NX54双频5400M Wi-Fi 6路由器:549元

科技热闻

智能家居弱电布线设计注意事项

不脱发的程序猿

智能家居 弱点布线

带你遨游银河系的十种分布式数据库

悟空聊架构

数据库 分布式 分布式数据库 6月日更

公安警情研判分析系统搭建,警情可视化指挥调度

数仓发生数据倾斜不要慌,教你轻松获取表倾斜率

华为云开发者联盟

GaussDB MPP GaussDB(DWS) 数据倾斜 并行架构

Linux Shell 自动交互人机交互的 3 种方法

学神来啦

Linux 运维 Shell 虚拟机 linux运维

java中的NIO和IO到底是什么区别?20个问题告诉你答案

华为云开发者联盟

Java io nio buffer channel

视频监控系统供电方式及选择方法

不脱发的程序猿

视频监控系统 供电方式 智能监控

为什么开发5年的同事,还在学习23种设计模式?是他太菜?

Java架构师迁哥

美团主办国际顶会ICCV 2021研讨会,食品视觉领域顶级挑战赛开启报名

科技热闻

如何看懂常用原理图符号、如何阅读原理图

不脱发的程序猿

电路设计 原理图符号 阅读原理图

Java RESTful Web Service实战_Java_韩陆_InfoQ精选文章