【原创文章,转载请注明原文章地址,谢谢!】
在REST应用中,也会出现针对一组请求需要在请求之前或者之后做统一处理的情况,比如登录检查,版本校对,额外的版权信息等,通过过滤器能够统一的处理。
服务器端过滤器(Server Filter)
Jersey中的过滤器分为两块,针对服务器端的过滤器和针对客户端的过滤器,先介绍服务器端的过滤器。
我们知道Servlet中的过滤器Filter,是一种双向的过滤器,即一个过滤器可以对请求进行一次过滤,然后调用执行链,让请求向下运行,然后再返回响应的时候,再次通过过滤器,在这个时候就可以对响应进行处理。而在JAX-RS中,过滤器是单向的,换句话说,要针对请求进行过滤,要选择针对请求的过滤器,要针对响应进行过滤,就要选择针对响应的过滤器:
javax.ws.rs.container.ContainerRequestFilter:针对请求的过滤器;
javax.ws.rs.container.ContainerResponseFilter:针对响应的过滤器;
基本使用
先来做一个最简单的过滤器测试:
首先实现一个ContainerRequestFilter:
public class MyRequestTestFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
System.out.println("===my request test filter===");
}
}
我们的代码很简单,在filter方法中打印一行数据;
然后再实现一个ContainerResponseFilter:
public class MyResponseTestFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
throws IOException {
System.out.println("===my response filter test===");
}
}
同样,在filter方法中打印一行数据。注意,现在先不用去关心filter方法的ContainerRequestContext,ContainerResponseContext等参数,后面介绍。
完成两个过滤器之后,进行代码测试,先创建一个资源:
@Path("filter1")
@GET
public String resource1() {
return "success";
}
注意,要让过滤器生效,需要在启动Jersey之前,注册我们的过滤器:
public RestApplication() {
this.packages("cn.wolfcode.jersey");
this.register(MultiPartFeature.class);
this.register(MyRequestTestFilter.class).register(MyResponseTestFilter.class);
}
完成一个测试代码:
@Test
public void test() {
String responseText = ClientBuilder.newClient()
.target("http://localhost:8082/webapi").path("filter/filter1")
.request(MediaType.TEXT_PLAIN).get(String.class);
System.out.println(responseText);
}
运行测试,在服务端能够看到输出:
===my request test filter===
===my response filter test===
更多细节
首先来看下两个过滤器的执行方式,在上面的测试代码中,当我们正常完成一个资源的请求,其执行流程为:
requestFilter--->resource-->responseFilter
但是如果我们执行以下测试:
@Test
public void test() {
String responseText = ClientBuilder.newClient()
.target("http://localhost:8082/webapi").path("filter/filter2")
.request(MediaType.TEXT_PLAIN).get(String.class);
System.out.println(responseText);
}
我们请求了一个不存在的资源地址,返回404,但是服务端输出:
===my response filter test===
意思就是,requestFilter一定要资源请求到了之后,才会执行,而responseFilter是只要有响应返回即可执行,我们可以对404等异常响应处理。
其次,在ContainerRequestFilter中,filter方法提供了一个ContainerRequestContext参数,我们来看看这个类的功能:
ContainerRequestContext
ContainerRequestContext:包装了请求相关的内容,比如请求URI,请求方法,请求实体,请求头等等信息,这是一个可变的类,可以在请求过滤器中被修改;
我们在本节中不做过多的例子,我们来分析一个Jersey内提供的ContainerRequestFilter——HttpMethodOverrideFilter。我们前面提到过,如果客户端是通过网页表单请求,但是我们知道表单只有POST和GET两种请求方式,怎么模拟PUT,DELETE等请求方式呢?在SpringMVC中,可以通过表单提交一个_method域,在该域中设置要模拟的请求类型,比如DELETE,在服务端配置一个HttpMethodOverrideFilter过滤器即可。
同理,在Jersey中,也存在这种请求问题。Jersey就是使用HttpMethodOverrideFilter来完成请求方法的转化。我们来看看代码(为了方便查看,代码进行了简化,完整代码请查看源代码即可):
public final class HttpMethodOverrideFilter implements ContainerRequestFilter {
@Override
public void filter(final ContainerRequestContext request) {
if (!request.getMethod().equalsIgnoreCase("POST")) {
return;
}
final String header = getParamValue(Source.HEADER, request.getHeaders(), "X-HTTP-Method-Override");
final String query = getParamValue(Source.QUERY, request.getUriInfo().getQueryParameters(), "_method");
final String override;
if (header == null) {
override = query;
} else {
override = header;
if (query != null && !query.equals(header)) {
// inconsistent query and header param values
throw new BadRequestException();
}
}
if (override != null) {
request.setMethod(override);
if (override.equals("GET")) {
if (request.getMediaType() != null
&& MediaType.APPLICATION_FORM_URLENCODED_TYPE.getType().equals(request.getMediaType().getType())) {
final UriBuilder ub = request.getUriInfo().getRequestUriBuilder();
final Form f = ((ContainerRequest) request).readEntity(Form.class);
for (final Map.Entry> param : f.asMap().entrySet()) {
ub.queryParam(param.getKey(), param.getValue().toArray());
}
request.setRequestUri(request.getUriInfo().getBaseUri(), ub.build());
}
}
}
}
}
可以简单的来看看执行流程:
1,请求比如是POST方法(调用getMethod获取本次请求方法);
2,两种方式可以完成请求方法的指定,第一种可以通过设置请求头中的X-HTTP-Method-Override属性,第二种可以通过添加_method查询参数,(这里分别调用getHeaders方法和getUriInfo方法获取头信息和请求URI信息);
3,设置请求方法,通过调用request.setMethod方法完成;
4,如果请求方式是POST,但是想转成GET方式,就需要把提交的表单内容变成URI的查询参数,具体参考代码即可。
ContainerResponseContext
对于ContainerResponseFilter来说,filter方法里面提供了两个参数:
ContainerRequestContext和ContainerResponseContext,第一个ContainerRequestContext我们前面已经说过,但是注意,在ResponseFilter中,requestContext已经是不能修改的!!ContainerResponseContext包装了响应相关的内容,这个对象在responseFilter中是可以修改的,比如下面的例子:
public class PoweredByResponseFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
throws IOException {
responseContext.getHeaders().add("X-Powered-By", "wolfcode.cn");
}
}
在这段代码中,我们通过responseContext为每一个响应都添加了一个额外的X-Powered-By响应头信息。更多的ContainerResponseContext方法,请查看API文档。
PreMatch和PostMatch
针对requestFilter,我们前面演示的所有的requestFilter都属于PostMatch,这也是默认的匹配时机,即过滤器只有当一个资源方法被正确的找到并准备执行的时候,过滤器执行过滤。换句话说,PostMath过滤器不能影响资源方法的匹配过程。所以我们上面看到的HttpMethodOverrideFilter在默认情况下(PostMatch)是不可能生效的,因为方法的匹配是在资源匹配之前完成的。
在Jersey中提供了@PreMatching注解,只需要在对应的requestFilter类上添加该标签,请求过滤器会在执行资源匹配之前提前运行。
所以,我们之前介绍的HttpMethodOverrideFilter,完整的类声明如下:
@PreMatching
@Priority(Priorities.HEADER_DECORATOR + 50)
public final class HttpMethodOverrideFilter implements ContainerRequestFilter {
可以看到,确实是标记为@PreMatching注解,那么就可以在该requestFilter中使用request.setMethod修改请求方法了。如果我们是在PostMatch请求过滤器中调用setMethod方法,那么过滤器会抛出IllegalArgumentException异常。
关于优先级@Priority标签,下文介绍。
客户端过滤器(Client Filter)
和服务端过滤器类似,Jersey也提供了两种客户端过滤器:
javax.ws.rs.client.ClientRequestFilter:客户端请求过滤器;
javax.ws.rs.client.ClientResponseFilter:客户端响应过滤器;
一个客户端过滤器示例代码:
public class CheckRequestFilter implements ClientRequestFilter {
@Override
public void filter(ClientRequestContext requestContext)
throws IOException {
if (requestContext.getHeaders(
).get("Client-Name") == null) {
requestContext.abortWith(
Response.status(Response.Status.BAD_REQUEST)
.entity("Client-Name header must be defined.")
.build());
}
}
}
该代码起一个演示作用,可以看到,首先CheckRequestFilter实现了ClientRequestFilter,即客户端请求拦截器,作用于客户端向服务端发送请求的过程,在filter方法中,判断请求头中是否包含Client-Name这个属性,如果没有包含,则直接调用requestContext的abortWith方法,传入一个状态为Response.Status.BAD_REQUEST(400)的响应,换句话说,该过滤器直接阻止了客户端向服务端的请求发送。
测试代码:
@Test
public void testClientFilter() {
String responseText = ClientBuilder.newClient()
.register(CheckRequestFilter.class)
.target("http://localhost:8082/webapi").path("filter/filter1")
.request(MediaType.TEXT_PLAIN)
.header("Client-Name", "wolfcode.cn").get(String.class);
System.out.println(responseText);
}
当然,在什么地方注册这个客户端过滤器无所谓,在ClientConfig,ClientBuilder或者WebTarget中都可以。
如果按照正常的流程,在header中增加Client-Name,能够正常执行,返回success;如果把测试代码修改为:
@Test
public void testClientFilter() {
String responseText = ClientBuilder.newClient()
.register(CheckRequestFilter.class)
.target("http://localhost:8082/webapi").path("filter/filter1")
.request(MediaType.TEXT_PLAIN)
.get(String.class);
System.out.println(responseText);
}
那么测试失败,直接抛出400的异常提示。
关于ClientRequestContext,ClientResponseContext这两个参数,可以参考服务端过滤器的使用,参考API即可。
其他
关于Filter的Name Binding,动态绑定和优先级,因为和拦截器(Interceptor)是相同的,所以这三个内容我们在下一节Interceptor中介绍。