1、问题介绍
系统采用Spring cloud zuul做请求转发,需求需要记录转发目标返回的请求结果。由于流的不可逆性,在读取返回结果后zuul无法正常返回数据给前端。
2、SendResponseFilter
源码解读
ZuulFilter
核心的代码,在返回到前端之前对返回结果进行处理的方法如下。
@Override
public Object run() {
try {
addResponseHeaders();
writeResponse();
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
从方法名可以看出来,addResponseHeaders
是对返回头处理,writeResponse
则是对返回结果进行封装。进入writeResponse
看代码。
// there is no body to send
if (context.getResponseBody() == null && context.getResponseDataStream() == null) {
return;
}
首先对上下文中的返回报文进行判断,如果没有报文,则直接返回不处理。
if (servletResponse.getCharacterEncoding() == null) {
// only set if not set
servletResponse.setCharacterEncoding("UTF-8");
}
对编码类型进行处理。
if (context.getResponseBody() != null) {
String body = context.getResponseBody();
is = new ByteArrayInputStream(
body.getBytes(servletResponse.getCharacterEncoding()));
}
else {
is = context.getResponseDataStream();
if (is!=null && context.getResponseGZipped()) {
// if origin response is gzipped, and client has not requested gzip,
// decompress stream before sending to client
// else, stream gzip directly to client
if (isGzipRequested(context)) {
servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, "gzip");
}
else {
is = handleGzipStream(is);
}
}
}
这一段是对输入流的封装。从上下文中获取返回字符串,如果字符串不为空,则封装一个ByteArrayInputStream
,如果上下文中存在返回结果流,并且是GZip压缩流,则添加返回头gzip。如果不是GZip压缩流,则将其封装成RecordingInputStream
,并再次封装成GZip压缩流。前置条件都结束之后,开始执行writeResponse
写入返回数据。
private void writeResponse(InputStream zin, OutputStream out) throws Exception {
byte[] bytes = buffers.get();
int bytesRead = -1;
while ((bytesRead = zin.read(bytes)) != -1) {
out.write(bytes, 0, bytesRead);
}
}
这一段是将输入流的数据写入response中。最终在finally
中执行GZip输入流的close
方法。
重点来了。我们查看GZIPInputStream
的构造函数和close
方法,发现InflaterInputStream
继续调用了其父类FilterInputStream
的构造方法,
最终将RecordingInputStream
赋值给了FilterInputStream
里的protected volatile InputStream in
。而close
中调用的方法则正是FilterInputStream
中的成员变量InputStream
的close
方法,即RecordingInputStream
的close
方法。查看该方法,发现最终调用的也是原始输入流的close
方法。那么无论上下文中的InputStream是什么类型,最终close的时候都是调用该实例的close方法,所以可以在SendResponseFilter
之前写一个自定义的filter将上下文中的inputStream替换掉。自定义一个ZuulPostResponseFilter
,设置级别为SEND_RESPONSE_FILTER_ORDER-1
,仿照org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter.RecordingInputStream
写一个RecordingInputStream
类,重写close
方法。由于输出流存在压缩的情况,所以写一个是否压缩字段gzipped
。如下:
RecordingInputStream(InputStream delegate, boolean gzipped) {
super();
this.delegate = Objects.requireNonNull(delegate);
this.gzipped = gzipped;
}
@Override
public void close() throws IOException {
logObject value;
if (this.gzipped) {
InputStream is = new GZIPInputStream(new ByteArrayInputStream(buffer.toByteArray()));
value = JSON.parseObject(StreamUtils.copyToString(is, Charset.defaultCharset()));
} else {
value = JSON.parseObject(buffer.toString());
}
System.out.println(JSON.toJSONString(value));
this.delegate.close();
}
在ZuulPostResponseFilter
中的run
方法中包装流。
InputStream is = ctx.getResponseDataStream();
if (is != null) {
is = new RecordingInputStream(is, ctx.getResponseGZipped());
}
ctx.setResponseDataStream(is);
经过这样包装之后,在SendResponseFilter
将返回的response body写入输出力之后调用关闭流的方法,会调用自定义RecordingInputStream
中重写的close
方法,从而达到日志记录的功能。