1、問(wèn)題介紹
系統(tǒng)采用Spring cloud zuul做請(qǐng)求轉(zhuǎn)發(fā),需求需要記錄轉(zhuǎn)發(fā)目標(biāo)返回的請(qǐng)求結(jié)果。由于流的不可逆性,在讀取返回結(jié)果后zuul無(wú)法正常返回?cái)?shù)據(jù)給前端。
2、SendResponseFilter源碼解讀
ZuulFilter核心的代碼,在返回到前端之前對(duì)返回結(jié)果進(jìn)行處理的方法如下。
@Override
public Object run() {
try {
addResponseHeaders();
writeResponse();
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
從方法名可以看出來(lái),addResponseHeaders是對(duì)返回頭處理,writeResponse則是對(duì)返回結(jié)果進(jìn)行封裝。進(jìn)入writeResponse看代碼。
// there is no body to send
if (context.getResponseBody() == null && context.getResponseDataStream() == null) {
return;
}
首先對(duì)上下文中的返回報(bào)文進(jìn)行判斷,如果沒(méi)有報(bào)文,則直接返回不處理。
if (servletResponse.getCharacterEncoding() == null) {
// only set if not set
servletResponse.setCharacterEncoding("UTF-8");
}
對(duì)編碼類型進(jìn)行處理。
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);
}
}
}
這一段是對(duì)輸入流的封裝。從上下文中獲取返回字符串,如果字符串不為空,則封裝一個(gè)ByteArrayInputStream,如果上下文中存在返回結(jié)果流,并且是GZip壓縮流,則添加返回頭gzip。如果不是GZip壓縮流,則將其封裝成RecordingInputStream,并再次封裝成GZip壓縮流。前置條件都結(jié)束之后,開始執(zhí)行writeResponse寫入返回?cái)?shù)據(jù)。
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);
}
}
這一段是將輸入流的數(shù)據(jù)寫入response中。最終在finally中執(zhí)行GZip輸入流的close方法。
重點(diǎn)來(lái)了。我們查看GZIPInputStream的構(gòu)造函數(shù)和close方法,發(fā)現(xiàn)InflaterInputStream繼續(xù)調(diào)用了其父類FilterInputStream的構(gòu)造方法,
最終將RecordingInputStream賦值給了FilterInputStream里的protected volatile InputStream in。而close中調(diào)用的方法則正是FilterInputStream中的成員變量InputStream的close方法,即RecordingInputStream的close方法。查看該方法,發(fā)現(xiàn)最終調(diào)用的也是原始輸入流的close方法。那么無(wú)論上下文中的InputStream是什么類型,最終close的時(shí)候都是調(diào)用該實(shí)例的close方法,所以可以在SendResponseFilter之前寫一個(gè)自定義的filter將上下文中的inputStream替換掉。自定義一個(gè)ZuulPostResponseFilter,設(shè)置級(jí)別為SEND_RESPONSE_FILTER_ORDER-1,仿照org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter.RecordingInputStream寫一個(gè)RecordingInputStream類,重寫close方法。由于輸出流存在壓縮的情況,所以寫一個(gè)是否壓縮字段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);
經(jīng)過(guò)這樣包裝之后,在SendResponseFilter將返回的response body寫入輸出力之后調(diào)用關(guān)閉流的方法,會(huì)調(diào)用自定義RecordingInputStream中重寫的close方法,從而達(dá)到日志記錄的功能。