写点什么

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:299962

评论

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

RUOYI 框架教程 15|若依框架中 Mysql 操作 | 日期处理

Java_若依框架教程

Java 技术 Ruoyi 框架 若依

ShardingSphere X Google 编程之夏:同学,开源你怎么看?

SphereEx

开源社区 ShardingSphere 谷歌 编程之夏

解读业界5种主流的深度网络模型

华为云开发者联盟

模型 网络模型 模型优化 模型量化 深度网络

Docgeni 1.1.0 正式发布!

PingCode研发中心

标签 Docgeni 文档目录 进度展示 日志展示

嵌入式软件时序(1)— C语言是怎么编译出来的

SOA开发者

声网 2020 实时大会后的弱网对抗实践

声网

音视频 网络环境 视频编解码 弱网下的极限实时视频通信

[架构实战营]模块九作业

xyu

#架构实战营

万字长文,一篇吃透WebSocket:概念、原理、易错常识、动手实践

JackJiang

websocket 即时通讯 IM

实时音频抗弱网技术揭秘

百度开发者中心

最佳实践 经验分享 智能视频

Rtmp Message 与 Chunk格式

webrtc developer

RTMP

区块链通证经济的意义

CECBC

10月活动推荐:2021上汽集团“新四化”技术高峰论坛

SOA开发者

gitee上提交PR和issue流程和注意事项

Geek_6cdeb6

机器学习 深度学习 git

基于HarmonyOS分布式技术,他们让绘画体验更为出色

Geek_283163

鸿蒙

Java 面试的“完美圣经”,有了这些还愁面试吗?

Java 程序员 架构 面试 后端

车云一体的应用价值

SOA开发者

一个约定让全球数万AI爱好者相聚,它是如何做到的?

硬科技星球

为了让你搞定数据库选型,这些工程师重写了 26 万行代码

SphereEx

数据库 架构 架构设计 ShardingSphere SphereEx

RUOYI 框架教程 16|关于若依RuoYi.jar卡顿,僵死,假死,系统无反映解决方案

Java_若依框架教程

技术 Ruoyi 开发 框架 若依

我用 10000 张图片合成我们美好的瞬间

荣顶

JavaScript 大前端 canvas 图形处理

OpenCV学习(三):三重境界

轻口味

OpenCV图像处理 10月月更

还在苦恼网络协议?阿里大佬这份笔记带你从入门到精通!

Java 架构 面试 程序人生 编程语言

The Data Way Vol.5|这里有一场资本与开源的 battle

SphereEx

开源 播客 ShardingSphere SphereEx

Python代码阅读(第35篇):完全(深度)展开嵌套列表

Felix

Python 编程 Code Programing 阅读代码

MongoDB中文社区 Freetalk,一起来玩快闪!

MongoDB中文社区

mongodb

区块链通证经济和传统经济的区别,如何实现

CECBC

想提高运维效率,那就把MySQL数据库部署到Kubernetes 集群中

华为云开发者联盟

MySQL 运维 测试 MySQL数据库 Kubernetes 集群

用21张图,把Git 工作原理彻底说清楚

git 架构 面试 后端

明道云当选“中国电子商会数据资源服务创新专业委员会”理事单位

明道云

AUTOSAR基础篇之OS(上)

SOA开发者

观测云产品更新|新增主机网络性能监测、图表矩形树图、多监测关联查询等功能

观测云

功能更新

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