請直接看結(jié)論。
一、 代碼結(jié)構(gòu)
SendResponseFilter是Zuul的最后一個Filter,負責(zé)最終響應(yīng)結(jié)果的渲染輸出。
其run方法異常簡介,如下:
@Override
public Object run() {
try {
addResponseHeaders();
writeResponse();
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
只做兩件事情:一、添加響應(yīng)頭;二、渲染響應(yīng)結(jié)果到響應(yīng)體。
addResponseHeaders()方法較簡單,大家可自行閱讀。
二、 writeResponse
下面對writeResponse()進行分析確認,以便了解zuul內(nèi)部機制,便于繞開各種小坑。
如下:
private void writeResponse()throws Exception {
RequestContext context = RequestContext.getCurrentContext();
// there is no body to send
// getResponseBody是從上下文取返回文本,這也是推薦做法。
// getResponseDataStream直接讀輸出流
// 這兩個是SendResponseFilter獲取輸出內(nèi)容的兩種渠道
? if (context.getResponseBody() ==null
? ? ? ? && context.getResponseDataStream() ==null) {
return;
}
// 獲取輸出對象
// context.getResponse() 這個方法普通filter也可以調(diào)用,個人認為不夠安全,容易增大各filter協(xié)調(diào)的難度,引入各種bug
? HttpServletResponse servletResponse = context.getResponse();
if (servletResponse.getCharacterEncoding() ==null) {// only set if not set
? ? ? servletResponse.setCharacterEncoding("UTF-8");
}
OutputStream outStream = servletResponse.getOutputStream();
InputStream is =null;
try {
// 首先看上下文中有沒有返回文本(getResponseBody),如果有,直接輸出
? ? ? if (RequestContext.getCurrentContext().getResponseBody() !=null) {
String body = RequestContext.getCurrentContext().getResponseBody();
writeResponse(
new ByteArrayInputStream(
body.getBytes(servletResponse.getCharacterEncoding())),
outStream);
// 輸出上下文中response為key的文本,完成,收工!
? ? ? ? return;
}
boolean isGzipRequested =false;
final String requestEncoding = context.getRequest()
.getHeader(ZuulHeaders.ACCEPT_ENCODING);
if (requestEncoding !=null
? ? ? ? ? ? && HTTPRequestUtils.getInstance().isGzipped(requestEncoding)) {
isGzipRequested =true;
}
// getResponseBody中沒有值,從getResponseDataStream獲取
? ? ? is = context.getResponseDataStream();
InputStream inputStream = is;
if (is !=null) {
if (context.sendZuulResponse()) {
// if origin response is gzipped, and client has not requested gzip,
// decompress stream
// before sending to client
// else, stream gzip directly to client
// 如果啟用壓縮,返回流再包裝一層
? ? ? ? ? ? if (context.getResponseGZipped() && !isGzipRequested) {
// If origin tell it's GZipped but the content is ZERO bytes,
// don't try to uncompress
? ? ? ? ? ? ? final Long len = context.getOriginContentLength();
if (len ==null || len >0) {
try {
inputStream =new GZIPInputStream(is);
}
catch (java.util.zip.ZipException ex) {
log.debug(
"gzip expected but not "
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? +"received assuming unencoded response "
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? + RequestContext.getCurrentContext()
.getRequest().getRequestURL()
.toString());
inputStream = is;
}
}
else {
// Already done : inputStream = is;
? ? ? ? ? ? ? }
}
else if (context.getResponseGZipped() && isGzipRequested) {
servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING,"gzip");
}
writeResponse(inputStream, outStream);
}
}
}
finally {
try {
if (is !=null) {
is.close();
}
outStream.flush();
// The container will close the stream for us
? ? ? }
catch (IOException ex) {
}
}
}
三、結(jié)論
開發(fā)自定義Filter時,如果涉及對輸出內(nèi)容的控制或改造,需要了解Zuul是最終如何處理內(nèi)容的。
Zuul從上下文中通過getResponseBody或getResponseDataStream讀取返回內(nèi)容,優(yōu)先采用getResponseBody的內(nèi)容。
因此,一旦我們采用 RequestContext.getCurrentContext().setResponseBody("haha"); 的方式設(shè)置返回內(nèi)容,responseDataStream中即使有內(nèi)容也會被忽略。
開發(fā)自定義Filter時,如果需要獲取返回內(nèi)容,加工,再放回,策略如下:
1、 如果其他filter已經(jīng)將內(nèi)容放到 responseBody 中,取出、處理、再放回即可。
String s = RequestContext.getCurrentContext().getResponseBody();
s +="mycustom";
RequestContext.getCurrentContext().setResponseBody(s);
2、 如果 RequestContext.getCurrentContext().getResponseBody() 獲取不到,則返回內(nèi)容在responseDataStream中。
流中的內(nèi)容只可以讀一次,可以通過wrapper的方式,但操作復(fù)雜。
居中的方案是讀出來,處理,放入responseBody中。
這時候,可能后面的filter,期待從responseDataStream中讀數(shù)據(jù),很遺憾,它讀不到了, 各fitler之間的協(xié)調(diào)確實麻煩。
3、 既然了解了Zuul Filter的這個特點,大家可以約定,涉及對response內(nèi)容修改的情況下:
(1)每個人要加Filter的時候,一定要加在最后(filterOrder最大),這樣可以避免影響其他的Filter,如果確實邏輯上有限制,需要確保后續(xù)的filter的正常運行。
(2)每個filter在注釋中說明白,自己從哪里取的數(shù)據(jù),怎么放回去的。
(3)性能允許的情況下,第一個filter把數(shù)據(jù)從流里讀出來,放入responseBody,大家都針對responseBody操作。
(4)需要獲取返回內(nèi)容是,可以通過如下工具方法:
public static String getResponseBody(){
RequestContext ctx = RequestContext.getCurrentContext();
String ret = ctx.getResponseBody();
if(StringUtils.isNotBlank(ret)){
return ret;
}
InputStream is = ctx.getResponseDataStream();
if(is==null){
return null;
}
try {
ByteArrayOutputStream result =new ByteArrayOutputStream();
byte[] buffer =new byte[1024];
int length;
while ((length = is.read(buffer)) != -1) {
result.write(buffer,0, length);
}
ret = result.toString();
ctx.setResponseBody(ret);
return ret;
}catch (IOException ex) {
// ignore
? ? }finally {
try {
if (is !=null) {
is.close();
}
}
catch (IOException ex) {
}
}
return null;
}