Jersey
1、Jersey的AOP机制
Jersey自身支持AOP,可以不依赖于Spring等支持AOP的框架。
Jersey支持AOP的功能来自于GlashFish项目集的HK2项目。Jersey通用包jersey-.common
依赖HK2(轻量级DI架构),包括hk2-api
和hk2-locator
。其中hk2-locator依赖于javax.inject包、asm-all-repackaged包和cglib包。从这些包名不难看出,hk2-locator包是个纯粹玩AOP的。
javax.inject包出自Java依赖注入规范(JSR-330),cglib包是Spring用户熟识的动态代码生成工具,是Spring两种AOP实现方式的一种,其底层依赖于ASM(htp:/asm.ow2.org)。ASM来自于开源软件国际联盟OW2(http://www.ow2.org),OW2的前身是来自法国的ObjectWeb和来自中国的Orient Ware两个中间件开源组织。
Jersey提供的REST过滤器和拦截器为开发者提供了很贴心的切面扩展点,开发者无须像在Spring中为了针对某个类的方法进行AOP扩展,而写配置文件。在Jersey中,只要实现相应扩展点的接口,即可实现REST请求流程中特定事件点的拦截、扩展,其他工作由底层的HK2帮我们做。典型的应用包括请求和响应的过滤和读写拦截。
2、Providers
javax.ws.rs.ext.Providers
是JAX-RS2定义的一种辅助接口,其实现类用于辅助REST框架完成过滤和读写拦截等功能。使用注解@Provider
来标注这些实现类,可以被JAX-RS2的运行时自动探测、加载。Provider实例可以通过@Context
注解被依赖注入到其他实例中。
Providers接口定义了4个方法,分别用来获取MessageBodyReader
、MessageBodyWriter
、ExceptionMapper
和ContextResolver
实例。
2.1、实体Providers
Jersey之所以可以支持那么多种表述的类型,即响应实体的传输格式,是因为其底层实体Providers具备的对不同格式的处理能力。Jersey内部提供了非常丰富的MessageBodyReader
接口和MessageBodyWriter
接口实现类,用于处理不同格式的表述,比如字节数组、ML、文件和流等。
1)、MessageBodyReader
消息体读处理器接口MessageBodyReader<T>
用于将传输流转换为Java类型的对象。MessageBodyReader接口定义了一个泛型,接口的实现类为这个泛型定义一个具体类型,该类型即是该实现类所支持的转换类型。
实现类被业务系统启用有两种方式:
- 一是使用注解@Provider定义实现类,业务系统在启动时自动探测并加载。
- 另一种方式是通过编码注册到Application类或其子类中,业务系统在启动时,加载Application类或其子类时一并加载。
MessageBodyReader<T>
接口定义了两个方法。第一个方法isReadable()
是用来判断实现类是否支持将当前请求的数据类型反序列化。
//type:要生成的实例的类
//genericType:要生成的实例的类型
//annotations:将使用生成的实例初始化的方法声明上的注释数组
//mediaType:HTTP实体的媒体类型,如果在请求中没有指定媒体类型,则使用application/octet-stream
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType);
MessageBodyReader<T>
接口定义的第二个方readFrom()是用于处理反序列化,是处理读取流并转换为Java类型对象的核心方法。该方法定义如下。
//type:要从实体流中读取的类型
//genericType:要生成的实例的类型
//annotations:将使用生成的实例初始化的方法声明上的注释数组
//mediaType:HTTP实体的媒体类型
//httpHeaders:请求头
//entityStream:HTTP实体的输入流
public T readFrom(
Class<T> type,
Type genericType,
Annotation[] annotations,
MediaType mediaType,
MultivaluedMap<String, String> httpHeaders,
InputStream entityStream)
throws java.io.IOException, javax.ws.rs.WebApplicationException;
2)、MessageBodyWriter
消息体写处理器接口MessageBody Writer<T>
用于将Java类型的对象转换为流,它是序列化的过程和MessageBodyReader<T>
接口实现的反序列化的逆过程。两个接口的设计原理是相同的。对应地,MessageBody Writer<T>
定义了两个方法isWriteable()
和
writeTo()
。
其实,解析一种传输类型的Provider类通常会同时实现MessageBodyReader和MessageBodyWriter这两个接口。
isWriteable()方法用于检测实现类是否支持序列化当前请求的类型,如果不可写,Jersey框架会放弃使用这个实现类来处理当前的表述。
//type:要写的实例类
//genericType:要写入的实例的类型
//annotations:附加到消息实体实例的注释数组
//mediaType:HTTP实体的媒体类型
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType);
writeTo()方法是将请求对象写入流的序列化过程。
//type:要从实体流中读取的类型
//genericType:要生成的实例的类型
//annotations:将使用生成的实例初始化的方法声明上的注释数组
//mediaType:HTTP实体的媒体类型
//httpHeaders:请求头
//entityStream:HTTP实体的输出流
public void writeTo(
T t, Class<?> type,
Type genericType,
Annotation[] annotations,
MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream)
throws java.io.IOException, javax.ws.rs.WebApplicationException;
3)、MessageBodyWorkers
MessageBodyReader和MessageBodyWriter的实现类非常多,编写选择哪个实现类作为当前请求的读写处理器的
算法是非常繁重的工作,MessageBodyWorkers接口旨在抽象这一遴选工作,其实现类可以通过@Context依赖注入到使用MessageBodyWorkers的类中MessageBodyFactory
是MessageBodyWorkers接口的实现类。
2.2、上下文Providers
除了处理实体的Provider,处理上下文的Provider也非常重要。ContextResolver<T>
接口是用于提供资源类和其他Provider上下文信息的接口。
1)、ContextResolver
ContextResolver<T>
定义了一个方法getContext()
,输入参数是表述对象的类型,输出是上下文泛型。
public interface ContextResolver<T> {
T getContext(Class<?> type);
}
2)、自定义
根据对象的不同,返回不同的JettisonJaxbContext实例,以实现不同的JSON解析效果。
@Provider
public class JettisonJsonContextResolver implements ContextResolver<JAXBContext> {
private final JAXBContext c1;
private final JAXBContext c2;
public JettisonJsonContextResolver() throws JAXBException {
Class[] clz = new Class[]{Student1.class, Student2.class};
this.c1 = new JettisonJaxbContext(JettisonConfig.DEFAULT, clz);
this.c2 = new JettisonJaxbContext(JettisonConfig.badgerFish().build(), clz);
}
@Override
public JAXBContext getContext(Class<?> type) {
if (type == Student1.class) {
System.out.println("mapped");
return c1;
} else {
System.out.println("badgerfish");
return c2;
}
}
}
3、REST请求流程
请求流程中存在3种角色,分别是用户
、REST客户端
和REST服务端
。
请求始于请求的发送,止于调用Response类的readEntity()
方法,获取响应实体。
- 用户提交请求数据,客户端接收请求,进入第一个扩展点:
“客户端请求过滤器ClientRequestFilter实现类”的filter()方法
。 - 请求过滤处理完毕后,流程进人第二个扩展点:
“客户端写拦截器WriterInterceptor实现类”的aroundWriteTo()方法
,实现对客户端序列化操作的拦截。 - “客户端消息体写处理器MessageBodyWriter”执行序列化,流程从客户端过渡到服务器端。
- 服务器接收请求,流程进入第三个扩展点:
“服务器前置请求过滤器ContainerRequestFilter实现类"的filter()方法
。 - 过滤处理完毕后,服务器根据请求匹配资源方法,如果匹配到相应的资源方法,流程进入第四个扩展点:
“服务器后置请求过滤器ContainerRequestFilter实现类”的filter() 方法
。 - 后置请求过滤处理完毕后,流程进入第五个扩展点:
“服务器读拦截器ReaderInterceptor实现类”的aroundReadFrom()方法
,拦截服务器端反序列化操作。 - “服务器消息体读处理器MessageBodyReader”完成对客户端数据流的反序列化。服务器执行匹配的资源方法。
- REST请求资源的处理完毕后,流程进入第六个扩展点:
“服务器响应过滤器ContainerResponseFilter实现类”的filter()方法
。 - 过滤处理完毕后,流程进入第七个扩展点:
“服务器写拦截器WriterInterceptor实现类”的aroundWriteTo()方法
,对服务器端序列化到客户端这个操作的拦截。 - “服务器消息体写处理器MessageBodyWriter”执行序列化,流程返回到客户端一侧。
- 客户端接收响应,流程进入第八个扩展点:
“客户端响应过滤器ClientResponseFilter实现类”的filter()方法
。 - 过滤处理完毕后,客户端响应实例response返回到用户一侧,用户执行response.readEntity()流程进人第九个扩展点:
“客户端读拦截器ReaderInterceptor实现类”的aroundReadFrom()方法
,对客户端反序列化进行拦截。 - “客户端消息体读处理器MessageBodyReader”执行反序列化,将Java类型的对象最终作为readEntity()方法的返回值。到此,一次REST请求处理的完整流程完毕。这期间,如果出现异常或资源不匹配等情况,会从出错点开始结束流程。
4、REST过滤器
JAX-RS2定义的4种过滤器扩展点(Extension
Poit)接口,供开发者实现业务逻辑,按请求处理流程的先后顺序为:
- 客户端请求过滤器(
ClientRequestFilter
) - 服务器请求过滤器(
ContainerRequestFilter
) - 服务器响应过滤器(
ContainerResponseFilter
) - 客户端响应过滤器(
ClientResponseFilter
)
4.1、ClientRequestFilter
public interface ClientRequestFilter {
public void filter(ClientRequestContext requestContext) throws IOException;
}
客户端请求过滤器(ClientRequestFilter)定义的过滤方法filter()包含一个输入参数,是客户端请求的上下文类ClientRequestContext。
从该上下文中可以获取请求信息:
//获取请求方法
requestContext.getMethod();
//获取请求资源地址
requestContext.getUri();
//获取请求头信息
requestContext.getHeaders();
//获取Cookie
requestContext.getCookies();
...
示例:自定义Http Base认证过滤器
@Provider
public class HttpAuthenticationFilter implements ClientRequestFilter {
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
MultivaluedMap<String, Object> headers = requestContext.getHeaders();
if (!headers.containsKey("Authorization")) {
String username = "tom";
String password = "123456";
String authentication = "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes());
headers.add("Authorization", authentication);
}
}
}
4.2、ContainerRequestFilter
public interface ContainerRequestFilter {
public void filter(ContainerRequestContext requestContext) throws IOException;
}
针对过滤切面,服务器请求过滤器接口ContainerRequestFilter的实现类可以定义为预处理
和后处理
。默认情况下,采用后处理方式。
- 后处理:即先执行容器接收请求操作,当服务器接收并处理请求后,流程才进入过滤器实现类的filter()方法。
- 预处理:是在服务器处理接收到的请求之前就执行过滤。如果希望实现一个预处理的过滤器实现类,需要在类名上定义注解
@PreMatching
。
示例:自定义CSRF保护过滤器
//优先级
@Priority(Priorities.AUTHENTICATION)
public class CsrfProtectionFilter implements ContainerRequestFilter {
public static final String HEADER_NAME = "X-Requested-By";
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 requestContext) throws IOException {
if (!METHODS_TO_IGNORE.contains(requestContext.getMethod()) && !requestContext.getHeaders().containsKey(HEADER_NAME)) {
throw new BadRequestException();
}
}
}
在这段代码中,CsrfProtectionFilter定义了一个特殊的头信息"X-Requested-By"和CSRF忽略监控的方法集合。在过滤器的filter()方法中,首先从上下文中获取头信息和请求方法信息,然后判断头信息是否包含"X-Requested-By",方法信息是否是安全的请求方法,即"GET"、“OPTIONS"或"HEAD”。如果两个条件都不成立,过滤器会抛出一个运行时异常BadRequestException。通过CsrfProtectionFilter过滤器,可以确保请求是CSRF安全的。
4.3、ContainerResponseFilter
public interface ContainerResponseFilter {
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
throws IOException;
}
服务器响应过滤器接口ContainerResponseFilter定义的过滤方法filter()包含两个输人参数,一个是容器请求上下文类ContainerRequestContext
,另一个是容器响应上下文类ContainerResponseContext
。
示例:EncodingFilter,完成内容协商中编码匹配的工作
public void filter(ContainerRequestContext request, ContainerResponseContext response) throws IOException {
if (!response.hasEntity()) {
return;
}
// add Accept-Encoding to Vary header
List<String> varyHeader = response.getStringHeaders().get(HttpHeaders.VARY);
if (varyHeader == null || !varyHeader.contains(HttpHeaders.ACCEPT_ENCODING)) {
response.getHeaders().add(HttpHeaders.VARY, HttpHeaders.ACCEPT_ENCODING);
}
// if Content-Encoding is already set, don't do anything
if (response.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING) != null) {
return;
}
// retrieve the list of accepted encodings
List<String> acceptEncoding = request.getHeaders().get(HttpHeaders.ACCEPT_ENCODING);
// if empty, don't do anything
if (acceptEncoding == null || acceptEncoding.isEmpty()) {
return;
}
// convert encodings from String to Encoding objects
List<ContentEncoding> encodings = Lists.newArrayList();
for (String input : acceptEncoding) {
String[] tokens = input.split(",");
for (String token : tokens) {
try {
ContentEncoding encoding = ContentEncoding.fromString(token);
encodings.add(encoding);
} catch (ParseException e) {
// ignore the encoding that could not parse
// but log the exception
Logger.getLogger(EncodingFilter.class.getName()).log(Level.WARNING, e.getLocalizedMessage(), e);
}
}
}
// sort based on quality parameter
Collections.sort(encodings);
// make sure IDENTITY_ENCODING is at the end (since it accepted if not explicitly forbidden
// in the Accept-Content header by assigning q=0
encodings.add(new ContentEncoding(IDENTITY_ENCODING, -1));
// get a copy of supported encoding (we'll be modifying this set, hence the copy)
SortedSet<String> acceptedEncodings = Sets.newTreeSet(getSupportedEncodings());
// indicates that we can pick any of the encodings that remained in the acceptedEncodings set
boolean anyRemaining = false;
// final resulting value of the Content-Encoding header to be set
String contentEncoding = null;
// iterate through the accepted encodings, starting with the highest quality one
for (ContentEncoding encoding : encodings) {
if (encoding.q == 0) {
// ok, we are down at 0 quality
if ("*".equals(encoding.name)) {
// no other encoding is acceptable
break;
}
// all the 0 quality encodings need to be removed from the accepted ones (these are explicitly
// forbidden by the client)
acceptedEncodings.remove(encoding.name);
} else {
if ("*".equals(encoding.name)) {
// any remaining encoding (after filtering out q=0) will be acceptable
anyRemaining = true;
} else {
if (acceptedEncodings.contains(encoding.name)) {
// found an acceptable one -> we are done
contentEncoding = encoding.name;
break;
}
}
}
}
if (contentEncoding == null) {
// haven't found any explicit acceptable encoding, let's see if we can just pick any of the remaining ones
// (if there are any left)
if (anyRemaining && !acceptedEncodings.isEmpty()) {
contentEncoding = acceptedEncodings.first();
} else {
// no acceptable encoding can be sent -> return NOT ACCEPTABLE status code back to the client
throw new NotAcceptableException();
}
}
// finally set the header - but no need to set for identity encoding
if (!IDENTITY_ENCODING.equals(contentEncoding)) {
response.getHeaders().putSingle(HttpHeaders.CONTENT_ENCODING, contentEncoding);
}
}
EncodingFilter过滤器的filter()方法通过对请求头信息“Accept-Encoding”的分析,先后为响应头信息“Vary”和“Content-Encoding”赋值,以实现编码部分的内容协商。
4.4、ClientResponseFilter
public interface ClientResponseFilter {
public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext)
throws IOException;
}
客户端响应过滤器(ClientResponseFilter)定义的过滤方法filter()包含两个输人参数,一个是客户端请求的上下文类ClientRequestContext,另一个是客户端响应的上下文类
ClientResponseContext。
示例:HTTP摘要认证过滤器类HttpAuthenticationFilter
@Override
public void filter(ClientRequestContext request) throws IOException {
if ("true".equals(request.getProperty(REQUEST_PROPERTY_FILTER_REUSED))) {
return;
}
if (request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
return;
}
Type operation = null;
if (mode == HttpAuthenticationFeature.Mode.BASIC_PREEMPTIVE) {
basicAuth.filterRequest(request);
operation = Type.BASIC;
} else if (mode == HttpAuthenticationFeature.Mode.BASIC_NON_PREEMPTIVE) {
// do nothing
} else if (mode == HttpAuthenticationFeature.Mode.DIGEST) {
if (digestAuth.filterRequest(request)) {
operation = Type.DIGEST;
}
} else if (mode == HttpAuthenticationFeature.Mode.UNIVERSAL) {
Type lastSuccessfulMethod = uriCache.get(getCacheKey(request));
if (lastSuccessfulMethod != null) {
request.setProperty(REQUEST_PROPERTY_OPERATION, lastSuccessfulMethod);
if (lastSuccessfulMethod == Type.BASIC) {
basicAuth.filterRequest(request);
operation = Type.BASIC;
} else if (lastSuccessfulMethod == Type.DIGEST) {
if (digestAuth.filterRequest(request)) {
operation = Type.DIGEST;
}
}
}
}
if (operation != null) {
request.setProperty(REQUEST_PROPERTY_OPERATION, operation);
}
}
4.5、自定义日志过滤器
日志过滤器:
@Provider
//前置处理
@PreMatching
public class AirLogFilter implements ContainerRequestFilter, ClientRequestFilter, ContainerResponseFilter, ClientResponseFilter {
private static final Log LOGGER = LogFactory.getLog(AirLogFilter.class);
private static final String NOTIFICATION_PREFIX = "* ";
private static final String SERVER_REQUEST = "> ";
private static final String SERVER_RESPONSE = "< ";
private static final String CLIENT_REQUEST = "/ ";
private static final String CLIENT_RESPONSE = "\\ ";
private static final AtomicLong logSequence = new AtomicLong(0);
@Override
public void filter(ClientRequestContext context) throws IOException {
long id = logSequence.incrementAndGet();
StringBuilder b = new StringBuilder();
//获取请求方法和地址
printRequestLine(CLIENT_REQUEST, b, id, context.getMethod(), context.getUri());
//获取请求头信息
printPrefixedHeaders(CLIENT_REQUEST, b, id,/*HeadersFactory*/HeaderUtils.asStringHeaders(context.getHeaders()));
LOGGER.info(b.toString());
}
@Override
public void filter(ClientRequestContext requestContext,
ClientResponseContext responseContext) throws IOException {
long id = logSequence.incrementAndGet();
StringBuilder b = new StringBuilder();
printResponseLine(CLIENT_RESPONSE, b, id, responseContext.getStatus());
printPrefixedHeaders(CLIENT_RESPONSE, b, id, responseContext.getHeaders());
LOGGER.info(b.toString());
}
@Override
public void filter(ContainerRequestContext context) throws IOException {
long id = logSequence.incrementAndGet();
StringBuilder b = new StringBuilder();
printRequestLine(SERVER_REQUEST, b, id, context.getMethod(), context.getUriInfo().getRequestUri());
printPrefixedHeaders(SERVER_REQUEST, b, id, context.getHeaders());
LOGGER.info(b.toString());
}
@Override
public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) throws IOException {
long id = logSequence.incrementAndGet();
StringBuilder b = new StringBuilder();
//获取容器响应状态
printResponseLine(SERVER_RESPONSE, b, id, responseContext.getStatus());
printPrefixedHeaders(SERVER_RESPONSE, b, id,/*HeadersFactory*/HeaderUtils.asStringHeaders(responseContext.getHeaders()));
LOGGER.info(b.toString());
}
private StringBuilder prefixId(StringBuilder b, long id) {
b.append(Long.toString(id)).append(" ");
return b;
}
private void printRequestLine(final String prefix,
StringBuilder b, long id, String method, URI uri) {
prefixId(b, id).append(NOTIFICATION_PREFIX)
.append("AirLog - Request received on thread ")
.append(Thread.currentThread().getName()).append("\n");
prefixId(b, id).append(prefix).append(method)
.append(" ").append(uri.toASCIIString()).append("\n");
}
private void printResponseLine(final String prefix,
StringBuilder b, long id, int status) {
prefixId(b, id).append(NOTIFICATION_PREFIX)
.append("AirLog - Response received on thread ")
.append(Thread.currentThread().getName()).append("\n");
prefixId(b, id).append(prefix)
.append(Integer.toString(status)).append("\n");
}
private void printPrefixedHeaders(final String prefix, StringBuilder b, long id, MultivaluedMap<String, String> headers) {
for (Map.Entry<String, List<String>> e : headers.entrySet()) {
List<?> val = e.getValue();
String header = e.getKey();
if (val.size() == 1) {
prefixId(b, id).append(prefix).append(header)
.append(": ").append(val.get(0)).append("\n");
} else {
StringBuilder sb = new StringBuilder();
boolean add = false;
for (Object s : val) {
if (add) {
sb.append(',');
}
add = true;
sb.append(s);
}
prefixId(b, id).append(prefix).append(header)
.append(": ").append(sb.toString()).append("\n");
}
}
}
}
服务端注册日志过滤器:
@Component
@ApplicationPath("/ws")
public class JerseyConfig extends ResourceConfig {
public JerseyConfig() {
//注册单个资源类
register(BookResourceImpl.class);
//注册过滤器
register(AirLogFilter.class);
}
}
客户端:
@Test
public void testLog() {
Client client = ClientBuilder.newClient();
WebTarget webTarget = client.target("http://127.0.0.1:8080/ws");
Response response = webTarget.register(AirLogFilter.class)
.path("book")
.request()
.get();
System.out.println(response.readEntity(String.class));
}
服务端输出:
1 > GET http://127.0.0.1:8080/ws/book
1 > user-agent: Jersey/2.32 (HttpUrlConnection 1.8.0_211)
1 > host: 127.0.0.1:8080
1 > accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
1 > connection: keep-alive
2 * AirLog - Response received on thread http-nio-8080-exec-1
2 < 200
2 < Content-Type: text/html
客户端输出:
1 / GET http://127.0.0.1:8080/ws/book
21:31:49.684 [main] INFO pers.zhang.filter.AirLogFilter - 2 * AirLog - Response received on thread main
2 \ 200
2 \ Keep-Alive: timeout=60
2 \ Connection: keep-alive
2 \ Content-Length: 4
2 \ Date: Tue, 26 Dec 2023 13:31:49 GMT
2 \ Content-Type: text/html
150M
5、REST拦截器
拦截器和过滤器的相同点是都是一种在请求——响应模型中,用做切面处理的Provider。两者的不同除了功能性上的差异(一个用于过滤消息,一个用于拦截处理)之外,形式上也不同。拦截器通常读写成对,而且没有服务器端和客户端的区分。
5.1、ReaderInterceptor
public interface ReaderInterceptor {
public Object aroundReadFrom(ReaderInterceptorContext context) throws java.io.IOException, javax.ws.rs.WebApplicationException;
}
读拦截器接口ReaderInterceptor定义的拦截方法是aroundReadFrom(),该方法包含一个输人参数,即读拦截器的上下文接口ReaderInterceptorContext,从中可以获取头信息、输人流以及父接口InterceptorContext提供的媒体类型等上下文信息。
5.2、WriterInterceptor
public interface WriterInterceptor {
void aroundWriteTo(WriterInterceptorContext context) throws java.io.IOException, javax.ws.rs.WebApplicationException;
}
写拦截器接口WriterInterceptor定义的拦截方法是aroundWriteTo(),该方法包含一个输入参数,写拦截器上下文接口WriterInterceptorContext,从中可以获取头信息、输出流以及父接口InterceptorContext提供的媒体类型等上下文信息。
5.3、编解码约束拦截器
编解码约束拦截器类ContentEncoder是一个位于org.glassfish.jersey.spi
包中的拦截器,SPI包下的工具是可插拔的。ContentEncoder拦截器用于约束序列化和反序列化的过程中,编解码的内容协商。
@Priority(Priorities.ENTITY_CODER)
@Contract
public abstract class ContentEncoder implements ReaderInterceptor, WriterInterceptor {
private final Set<String> supportedEncodings;
protected ContentEncoder(String... supportedEncodings) {
if (supportedEncodings.length == 0) {
throw new IllegalArgumentException();
}
this.supportedEncodings = Collections.unmodifiableSet(Arrays.stream(supportedEncodings).collect(Collectors.toSet()));
}
public final Set<String> getSupportedEncodings() {
return supportedEncodings;
}
public abstract InputStream decode(String contentEncoding, InputStream encodedStream) throws IOException;
public abstract OutputStream encode(String contentEncoding, OutputStream entityStream) throws IOException;
@Override
public final Object aroundReadFrom(ReaderInterceptorContext context) throws IOException, WebApplicationException {
String contentEncoding = context.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING);
//判断是否包含Content-Encoding头信息
if (contentEncoding != null && getSupportedEncodings().contains(contentEncoding)) {
//解码处理
context.setInputStream(decode(contentEncoding, context.getInputStream()));
}
//继续连接链的下一个拦截处理
return context.proceed();
}
@Override
public final void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
// must remove Content-Length header since the encoded message will have a different length
String contentEncoding = (String) context.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING);
//判断是否包含Content-Encoding头信息
if (contentEncoding != null && getSupportedEncodings().contains(contentEncoding)) {
//编码处理
context.setOutputStream(encode(contentEncoding, context.getOutputStream()));
}
//继续连接链的下一个拦截处理
context.proceed();
}
}
在这段代码中,分别给出了ContentEncoder拦截器的读、写拦截处理,只有当头信息包含“Content-Encoding”信息,编解码才被执行。读取阶段进行解码,写入阶段进行编码。上下文的proceed()方法用于执行拦截器链的下一个拦截器。
6、绑定机制
这些容器级别的Providers,通常使用编码的方式注册到Application中,但这不是唯一的办法。默认情况下,过滤器和拦截器都是全局绑定的。也就是说,如下之一的过滤器或拦截器是全局有效的:
- 通过手动注册到Application或者Configuration
- 注解为@Provider,被自动探测
6.1、名称绑定
过滤器或拦截器可以使用特定的注解来指定其作用范围,这种特定的注解被称为名称绑定。
1)、名称绑定注解
使用@NameBinding注解可以定义一个运行时的自定义注解,该注解用于定义类级别名称和类的方法名。
@NameBinding
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface AirLog {
}
2)、绑定Provider
在定义了@AirLog注解后,既可以在Provider中使用该注解,示例代码如下所示:
@AirLog
@Priority(Priorities.USER)
public class AirNameBindingFilter implements ContainerRequestFilter, ContainerResponseFilter {
private static final Log logger = LogFactory.getLog(AirNameBindingFilter.class);
public AirNameBindingFilter() {
logger.info("Air-NameBinding-Filter initialized");
}
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
logger.info("Air-NameBinding-ContainerRequestFilter invoked:" + requestContext.getMethod());
logger.info(requestContext.getUriInfo().getRequestUri());
}
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
logger.info("Air-NameBinding-ContainerResponseFilter invoked:" + requestContext.getMethod());
logger.info("status=" + responseContext.getStatus());
}
}
不要忘记注册:
register(AirNameBindingFilter.class);
3)、绑定方法
在资源方法级别使用自定义注解@AirLog,来实现在资源类的指定方法上启用AirNameBindingFilter过滤器,示例代码如下所示。
@Path("books")
public class BooksResource {
@AirLog
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_XML)
public Book getBookWithLog(Book book) {
return book;
}
@GET
@Produces(MediaType.APPLICATION_XML)
public Book getBookWithNoLog() {
return new Book(22l, "红楼梦", 11.11);
}
}
不要忘记注册:
register(BooksResource.class);
4)、测试
@Test
public void testNameBinding() {
//访问@AirLog绑定了日志的接口
Client client = ClientBuilder.newClient();
WebTarget webTarget = client.target("http://127.0.0.1:8080/ws");
Book book = new Book();
book.setId(1l);
book.setName("三国演义");
book.setPrice(33.34);
Response response = webTarget.path("books")
.request()
.post(Entity.entity(book, MediaType.APPLICATION_JSON_TYPE));
System.out.println(response.readEntity(String.class));
//访问没有绑定的接口
Client client2 = ClientBuilder.newClient();
WebTarget webTarget2 = client.target("http://127.0.0.1:8080/ws");
Response response2 = webTarget.path("books")
.request()
.get();
System.out.println(response2.readEntity(String.class));
}
客户端输出:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><book><id>1</id><name>三国演义</name><price>33.34</price></book>
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><book><id>22</id><name>红楼梦</name><price>11.11</price></book>
服务端输出,只有标注了@AirLog注解绑定的POST接口有日志:
2023-12-26 23:33:26.945 INFO 18067 --- [nio-8080-exec-1] pers.zhang.filter.AirNameBindingFilter : Air-NameBinding-Filter initialized
2023-12-26 23:33:27.049 INFO 18067 --- [nio-8080-exec-1] pers.zhang.filter.AirNameBindingFilter : Air-NameBinding-ContainerRequestFilter invoked:POST
2023-12-26 23:33:27.049 INFO 18067 --- [nio-8080-exec-1] pers.zhang.filter.AirNameBindingFilter : http://127.0.0.1:8080/ws/books
2023-12-26 23:33:27.106 INFO 18067 --- [nio-8080-exec-1] pers.zhang.filter.AirNameBindingFilter : Air-NameBinding-ContainerResponseFilter invoked:POST
2023-12-26 23:33:27.106 INFO 18067 --- [nio-8080-exec-1] pers.zhang.filter.AirNameBindingFilter : status=200
6.2、动态绑定
名称绑定需要通过自定义的注解名称来绑定Provider和扩展点方法或者类,相比而言,动态绑定无须新增注解,而是使用编码的方式,实现动态特征接口javax.ws.rs.container. DynamicFeature
,定义扩展点方法的名称、请求方法类型等匹配信息。在运行期,一旦Provider匹配当前处理类或方法,面向切面的Provider方法即被触发。
1)、定义绑定Provider
@Provider
public class AirDynamicFeature implements DynamicFeature {
@Override
public void configure(ResourceInfo resourceInfo, FeatureContext context) {
//判断此资源类是否为BooksResource
boolean classMatched = BooksResource.class.isAssignableFrom(resourceInfo.getResourceClass());
//判断是否为要增强的方法
boolean methodNameMatched = resourceInfo.getResourceMethod().getName().contains("getBookWithLog");
//判断请求方法是否为POST
boolean methodTypeMatched = resourceInfo.getResourceMethod().isAnnotationPresent(POST.class);
//匹配成功才注册AirDynamicBindingFilter
if (classMatched && (methodNameMatched || methodTypeMatched)) {
context.register(AirDynamicBindingFilter.class);
}
}
}
public class AirDynamicBindingFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
System.out.println("Air-Dynamic-Bindng-Filter invoked");
}
}
注册DynamicFeature:
register(AirDynamicFeature.class);
2)、定义资源类
@Path("books")
public class BooksResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_XML)
public Book getBookWithLog(Book book) {
return book;
}
@GET
@Produces(MediaType.APPLICATION_XML)
public Book getBookWithNoLog() {
return new Book(22l, "红楼梦", 11.11);
}
}
3)、测试
@Test
public void testNameBinding() {
//访问@AirLog绑定了日志的接口
Client client = ClientBuilder.newClient();
WebTarget webTarget = client.target("http://127.0.0.1:8080/ws");
Book book = new Book();
book.setId(1l);
book.setName("三国演义");
book.setPrice(33.34);
Response response = webTarget.path("books")
.request()
.post(Entity.entity(book, MediaType.APPLICATION_JSON_TYPE));
System.out.println(response.readEntity(String.class));
//访问没有绑定的接口
Client client2 = ClientBuilder.newClient();
WebTarget webTarget2 = client.target("http://127.0.0.1:8080/ws");
Response response2 = webTarget.path("books")
.request()
.get();
System.out.println(response2.readEntity(String.class));
}
服务端输出一次信息:
Air-Dynamic-Bindng-Filter invoked
7、优先级
对于同一个扩展点的多个Provider的执行的先后顺序是靠优先级排序的。优先级的定义使用注解@Priority
,优先级的值是一个整型值,常量定义在javax.ws.rs.Priorities
类中。
public final class Priorities {
private Priorities() {
// prevents construction
}
public static final int AUTHENTICATION = 1000;
public static final int AUTHORIZATION = 2000;
public static final int HEADER_DECORATOR = 3000;
public static final int ENTITY_CODER = 4000;
public static final int USER = 5000;
}
- 对于
ContainerRequest
、PreMatchContainerRequest
、ClientRequest
和读写拦截器
该数值采用升序策略,即数值越小N优先级越高; - 对于
ContainerResponse
和ClientResponse
该数值采用降序策略,即数越大优先级越高;
示例:ContainerRequestFilter采用升序策略,应该是AirDynamicBindingFilter1先被调用。
@Priority(Priorities.USER)
public class AirDynamicBindingFilter1 implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
System.out.println("Air-Dynamic-Bindng-Filter1 invoked");
}
}
@Priority(Priorities.USER + 1)
public class AirDynamicBindingFilter2 implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
System.out.println("Air-Dynamic-Bindng-Filter2 invoked");
}
}
@Provider
public class AirDynamicFeature implements DynamicFeature {
@Override
public void configure(ResourceInfo resourceInfo, FeatureContext context) {
//判断此资源类是否为BooksResource
boolean classMatched = BooksResource.class.isAssignableFrom(resourceInfo.getResourceClass());
//判断是否为要增强的方法
boolean methodNameMatched = resourceInfo.getResourceMethod().getName().contains("getBookWithLog");
//判断请求方法是否为POST
boolean methodTypeMatched = resourceInfo.getResourceMethod().isAnnotationPresent(POST.class);
//匹配成功才注册AirDynamicBindingFilter
if (classMatched && (methodNameMatched || methodTypeMatched)) {
context.register(AirDynamicBindingFilter1.class);
context.register(AirDynamicBindingFilter2.class);
}
}
}
PostMan测试:
服务端输出:
Air-Dynamic-Bindng-Filter1 invoked
Air-Dynamic-Bindng-Filter2 invoked