10.请求拦截和响应拦截
文章目录
- 前言
- 前景回顾
- 拦截器应用
- 请求拦截器
- 响应拦截器
- 测试
- 响应拦截器原理
- 总结
前言
优秀的设计总是少不了丰富的扩展点, 比如spring可以自动装配, aop扩展, web模块也有拦截器, 甚至对servlet的过滤器都有封装; 再比如netty、doubbo等等都支持在数据流入流出都允许用户自定义扩展点实现定制化处理, 咱们的feign框架也同样如此, 在可以定制化组件的同时, 也允许我们对发起请求之前和接受请求之后根据扩展点实现个性化的处理。
前景回顾
- 在
SynchronousMethodHandler#invoke
方法中, 会先用参数填充模板得到有完整请求数据的载体RequestTemplate
, 然后执行请求拦截器, 拦截器执行完成之后, 再讲目标请求地址设置给RequestTemplate
, 最后构建客户端参数Request
- 在请求完成之后会在
ResponseHandler#handleResponse
方法中执行相应责任链, 该责任链的每一个节点在feign框架中都可以看作是一个响应拦截器
拦截器应用
请求拦截器
public interface RequestInterceptor {
/**
* 拦截所有请求
*/
void apply(RequestTemplate template);
}
我们可以在请求前给参数打印日志或者添加请求头啥的
下面定义两个请求拦截器RequestInterceptor
/**
* 请求头添加认证拦截器
*/
public class AuthHeardRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
template.header("Authorization", "abc123");
}
}
/**
* 日志请求拦截器
*/
@Slf4j
public class LogRequestIntercept implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
Collection<String> requestVariables = template.getRequestVariables();
log.info("请求行参数:{}", requestVariables);
String s = new String(template.body(), StandardCharsets.UTF_8);
log.info("请求体参数:{}", s);
}
}
响应拦截器
打印返回时的数据
public class LogResponseIntercept implements ResponseInterceptor {
@Override
public Object intercept(InvocationContext invocationContext, Chain chain) throws Exception {
Response response = invocationContext.response();
System.out.println("响应状态Status: " + response.status());
System.out.println("响应头Headers: " + response.headers());
// 流只能第一次
// byte[] bodyData = Util.toByteArray(response.body().asInputStream());
// String responseStr = StandardCharsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(bodyData)).toString();
// String s = new String(bodyData, StandardCharsets.UTF_8);
// System.out.println("响应内容为: " + responseStr);
Object result = chain.next(invocationContext);
//
ObjectMapper objectMapper = new ObjectMapper();
String jsonString = objectMapper.writeValueAsString(result);
System.out.println("响应内容为: " + jsonString);
return result;
}
}
测试
写个测试controller
@PostMapping("/feign/header")
public Person header(@RequestBody Person person, @RequestHeader Map<String, Object> header) {
System.out.println("uncleqiao 收到body:" + person);
System.out.println("uncleqiao 收到header:" + header);
person.setName("小乔同学");
person.setAge(20);
return person;
}
feign接口
public interface DemoClient4 {
@RequestLine("POST /feign/header")
@Headers("Content-Type: application/json")
Person header(@Param String name, @Param Integer age);
}
测试类
@Test
void interceptFunc() {
JavaTimeModule javaTimeModule = new JavaTimeModule();
DemoClient4 client = Feign.builder()
.logLevel(feign.Logger.Level.FULL)
.encoder(new JacksonEncoder(List.of(javaTimeModule)))
.decoder(new JacksonDecoder(List.of(javaTimeModule)))
.requestInterceptor(new LogRequestIntercept())
.requestInterceptor(new AuthHeardRequestInterceptor())
.responseInterceptor(new LogResponseIntercept())
.logger(new Slf4jLogger())
.target(DemoClient4.class, "http://localhost:8080");
Person result = client.header("zs", 18);
System.out.println("收到结果:" + result);
}
结果
// controller打印
uncleqiao 收到body:Person(name=zs, age=18, gender=null, birthday=null)
uncleqiao 收到header:{authorization=abc123, content-type=application/json, accept=*/*, user-agent=Java/17.0.7, host=localhost:8080, connection=keep-alive, content-length=33}
// 请求拦截器打印
请求行参数:[]
请求体参数:{
"name" : "zs",
"age" : 18
}
// 响应拦截器打印
响应状态Status: 200
响应头Headers: {connection=[keep-alive], content-type=[application/json], date=[Sun, 01 Dec 2024 04:21:18 GMT], keep-alive=[timeout=60], transfer-encoding=[chunked]}
注意
这里定义的响应拦截器LogResponseIntercept
中, 打印的返回结果是在责任链执行完的后面执行的, 也就是下面这段
Object result = chain.next(invocationContext);
ObjectMapper objectMapper = new ObjectMapper();
String jsonString = objectMapper.writeValueAsString(result);
System.out.println("响应内容为: " + jsonString);
由于SocketInputStream io流只能读取一次, 如果我们在执行前使用如下方式读取了, 那么责任链后面的节点包括InvocationContext
都无法再继续读取, 会抛出流已关闭异常
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
String responseStr = StandardCharsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(bodyData)).toString();
// String s = new String(bodyData, StandardCharsets.UTF_8);
System.out.println("响应内容为: " + responseStr);
但是细心的同学可能发现当设置日志级别为logLevel(feign.Logger.Level.FULL)
的时候, 也是可以在执行整个责任链之前读取流中的数据打印日志的
原因
在ResponseHandler#handleResponse
方法中, 在执行拦截链之前会有个logAndRebufferResponseIfNeeded
方法来打印日志,
其中有段代码是这样的
// 请求体内容 response.body():Response$InputStreamBody
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
bodyLength = bodyData.length;
if (logLevel.ordinal() >= Level.FULL.ordinal() && bodyLength > 0) {
// 打印响应内容
log(configKey, "%s", decodeOrDefault(bodyData, UTF_8, "Binary data"));
}
log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
// 重建响应体, 因为SocketInputStream流只能读取一次, 所以必须重新设置
// 这里设置的是body是ByteArrayBody类型, 它可以重复读
return response.toBuilder().body(bodyData).build();
-
response.body().asInputStream() 底层用的就是
SocketInputStream
, 所以默认只能读取一次 -
Util.toByteArray方法将流中的字节都复制出来, 并关闭了原来的
SocketInputStream
public static byte[] toByteArray(InputStream in) throws IOException { checkNotNull(in, "in"); try { ByteArrayOutputStream out = new ByteArrayOutputStream(); copy(in, out); return out.toByteArray(); } finally { ensureClosed(in); } }
-
将复制出来的字节重新设置到了body中, 这个body是ByteArrayBody对象, 里面的数据可以多次读取
public Builder body(byte[] data) { this.body = ByteArrayBody.orNull(data); return this; } private static Body orNull(byte[] data) { if (data == null) { return null; } return new ByteArrayBody(data); }
这就是为什么当设置feign的日志级别是
feign.Logger.Level.FULL
的时候, 我们可以在拦截器中先读取数据, 再执行拦截器的原理。不过有打印返回数据的需求, 还是建议在执行响应拦截器之后再打印, 因为日志级别一般也不是开到FULL这么高。
响应拦截器原理
关于响应拦截器的责任链, 在第6篇中有详细介绍过, 这里有必要再拿出来说说
protected ResponseInterceptor.Chain responseInterceptorChain() {
ResponseInterceptor.Chain endOfChain =
ResponseInterceptor.Chain.DEFAULT;
ResponseInterceptor.Chain executionChain = this.responseInterceptors.stream()
.reduce(ResponseInterceptor::andThen)
.map(interceptor -> interceptor.apply(endOfChain))
.orElse(endOfChain);
return (ResponseInterceptor.Chain) Capability.enrich(executionChain,
ResponseInterceptor.Chain.class, capabilities);
}
1.reduce
Optional<T> reduce(BinaryOperator<T> accumulator)
public interface BinaryOperator<T> extends BiFunction<T,T,T>{...}
public interface BiFunction<T, U, R> {
R apply(T t, U u);
}
-
reduce方法
接收一个BinaryOperator参数, 函数式接口的方法签名为R apply(T t, U u);
, 由于BinaryOperator定义了父类的泛型是<T,T,T>; 所以方法签名可以看作是T apply(T t, T u)
, 也就是接受两个相同类型的变量, 然后返回的也是同一个类型。 -
reduce会遍历每一个集合对象, 属于累加型, 类似于
a = 0; for (i = 0; i++; i<list.size) a = sum(a,i);
这种, 也就是先定义一个累加值, 然后累加的值会和每一个集合元素用BinaryOperator 的apply
方法处理 -
ResponseInterceptor::andThen
是一个类的非静态方法引用式lambda表达式, 它会默认传入一个当前实例作为调用对象, 也就是responseInterceptor.andThen(..)
, 聚合的拦截器就会和每一个拦截器用andThen
方法进行处理 -
andThan方法如下, 它会用匿名lambda的形式创建一个
ResponseInterceptor
, 然后拦截器内部nextContext -> nextInterceptor.intercept
又是用一个匿名lambda形式创建了一个Chain, 这个Chain中包含了当前拦截器的调用nextInterceptor.intercept
, 同时它使用的第二个参数是上一个拦截器传过来的chain
, 而不是当前创建的nextContext
default ResponseInterceptor andThen(ResponseInterceptor nextInterceptor) {
return (ic, chain) -> intercept(ic,
nextContext -> nextInterceptor.intercept(nextContext, chain));
}
-
也就是说andThen方法创建了一个拦截器, 同时也创建了一个
Chain
对象作为该拦截器的第二个参数, 然后当前拦截器nextInterceptor.intercept
放在这个Chain
具体实现对象的调用方法里, 也就是next; 换句话说调用Chain.next 就是调用nextInterceptor#intercept
-
所以
reduce
每一次遍历都会创建一个新的拦截器, 并且创建一个Chain
在拦截器内部调用给它的调用函数intercept
, 同时调用函数中执行了当前拦截器, 所以就形成了一个调用链, 前面一个拦截器的intercept
中调用了当前拦截器nextInterceptor
, 只不过每次调用的Chain
参数都是新创建的 -
map方法就比较简单了, 它调用聚合之后的拦截器的
apply
方法, 该方法创建了一个Chain
, 实际调用的时候参数传入的是InvocationContext
对象, 在内部调用当前聚合拦截器的intercept
方法, 第一个参数是InvocationContext
对象, 第二个就是ResponseInterceptor.Chain.DEFAULT
default Chain apply(Chain chain) { // 调用当前拦截器的intercept方法; 这里就是聚合的拦截器 return request -> this.intercept(request, chain); }
它平铺之后如下
public ResponseInterceptor.Chain buildRespChain() {
ResponseInterceptor.Chain endOfChain = ResponseInterceptor.Chain.DEFAULT;
// 合并所有拦截器成一个 ResponseInterceptor; 这个定义和循环的动作对应reduce
ResponseInterceptor combinedInterceptor = null;
for (ResponseInterceptor interceptor : this.responseInterceptors) {
if (combinedInterceptor == null) {
combinedInterceptor = interceptor;
} else {
ResponseInterceptor previousCombinedInterceptor = combinedInterceptor;
// 这个聚合赋值的动作也对应reduce
// 这个创建拦截器的动作对应andThen的(ic, chain) -> intercept(...)动作
combinedInterceptor = new ResponseInterceptor() {
@Override
public Object intercept(InvocationContext ic, Chain chain) throws Exception {
// 这个new Chain对应andThen的nextContext -> nextInterceptor.intercept(nextContext, chain)动作
return previousCombinedInterceptor.intercept(ic, new Chain() {
@Override
public Object next(InvocationContext context) throws Exception {
return interceptor.intercept(context, chain);
}
});
}
};
}
}
// 如果没有拦截器,直接返回 endOfChain
if (combinedInterceptor == null) {
return endOfChain;
}
ResponseInterceptor temp = combinedInterceptor;
// 使用 apply 构造最终责任链
return new ResponseInterceptor.Chain() {
@Override
public Object next(InvocationContext request) throws Exception {
return temp.intercept(request, endOfChain);
}
};
}
总结
- 请求拦截器需要实现
RequestInterceptor
接口, 它在真正使用客户端执行调用前执行, 可以用它来处理请求头, 打印日志啥的 - 响应拦截器在客户端请求成功后处理, 默认不支持在拦截链之前完之前打印响应对象, 因为
SocketInputStream
只能读取一次, 但是开启feign的日志级别为HEADERS
和FULL
可以打破这个限制 - 响应拦截器使用stream流组装, 显得晦涩难懂, 需要大家多揣摩