- 一般來講,正常的流程是pre-->route-->post
- 在pre過濾器階段拋出異常,pre--> error -->post
- 在route過濾器階段拋出異常,pre-->route-->error -->post
- 在post過濾器階段拋出異常,pre-->route-->post--> error
通過上面請求生命周期和核心過濾器的介紹,我們發現在核心過濾器中并沒有實現error階段的過濾器,那么當過濾器出現異常的時候需要怎么處理呢?
自定義一個過濾器ThrowExceptionFilter在執行時期拋出異常
@Component
public class ThrowExceptionFilter extends ZuulFilter{
private static Logger logger = LoggerFactory.getLogger(ThrowExceptionFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
logger.info("this is a pre filter,it will throw a RuntimeException");
doSomething();
return null;
}
private void doSomething(){
throw new RuntimeException("exist some errors....");
}
}
啟動服務,
訪問user服務,
http://192.168.5.5:6069/users/user/home
我們發現api網關服務的控制臺輸出ThrowExceptionFilter
的過濾邏輯的日志信息,但是沒有輸出任何異常信息,同時發起的請求也沒有獲得任何響應結果。
為什么會出現這樣的情況?我們又該怎樣處理過濾器中的一場呢?
try-catch處理?
回想一下,我們在上一節中介紹的所有核心過濾器,有一個post過濾器SendErrorFilter用來處理一場信息的?根據正常的處理流程,該過濾器會處理異常信息,那么這里沒有出現任何異常信息說明很有可能就是這個過濾器沒有執行。所以看看SendErrorFilter
的shouldFilter
函數:
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
// only forward to errorPath if it hasn't been forwarded to already
return ctx.containsKey("error.status_code")
&& !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
}
可以看到,該方法的返回值中有一個重要的判斷依據ctx.containsKey("error.status_code")
,也就是說請求上下文必須有error.status_code
參數,我們實現的ThrowExceptionFilter
中沒有設置這個參數,所以自然不會進入SendErrorFilter
過濾器的處理邏輯。那么如何使用這個參數呢?可以看看route類型的幾個過濾器,由于這些過濾器會對外發起請求,所以肯定有異常需要處理,比如RibbonRoutingFilter
的run方法實現如下:
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
this.helper.addIgnoredHeaders();
try {
RibbonCommandContext commandContext = buildCommandContext(context);
ClientHttpResponse response = forward(commandContext);
setResponse(response);
return response;
}
catch (ZuulException ex) {
context.set(ERROR_STATUS_CODE, ex.nStatusCode);
context.set("error.message", ex.errorCause);
context.set("error.exception", ex);
}
catch (Exception ex) {
context.set("error.status_code",
HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
context.set("error.exception", ex);
}
return null;
}
可以看到,整個發起請求的邏輯都采用了try-catch
塊處理。在catch
異常的處理邏輯中并沒有任何輸出操作,而是向請求中添加了一些error相關的參數,主要有下面的三個參數。
error.status_code:錯誤代碼
error.exception:Exception異常信息
error.message:錯誤信息
error.status_code
就是SendErrorFilter
過濾器用來判斷是否需要執行的重要參數。可以改造一下我們的ThrowExceptionFilter
的run方法,
改造ThrowExceptionFilter的run方法之后:
@Override
public Object run() {
logger.info("this is a pre filter,it will throw a RuntimeException");
RequestContext context = RequestContext.getCurrentContext();
try{
doSomething();
}catch (Exception e){
context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
context.set("error.message",e.getMessage());
context.set("error.exception", e);
}
return null;
}
書中有個錯context.set("error.message",e);
這邊在SendErrorFilter
的時候就會類型轉換報錯。
此時,異常信息已經被SendErrorFilter
過濾器正常處理并返回給客戶端了,同時在網關的控制臺中也輸出了異常信息。從返回的響應信息中,可以看到幾個之前我們在請求上下文中設置的內容,
- status:對應error.status_code參數值
- exception:對應error.exception參數中Exception的類型
- message:對應error.exception參數中Exception的message信息。
再去訪問http://192.168.1.57:6069/user-service/user/index
,頁面顯示錯誤的頁面。
ErrorFilter處理
通過上面的分析與實驗,我們已經知道如何在過濾器中正確的處理異常,讓錯誤信息能夠順利地流轉到SendErrorFilter
過濾器來組織和輸出。但是,我們可以在過濾器中使用try-catch
來處理業務邏輯并向請求上下文中添加異常信息,但是不可控的人為因素,意外的程序因素等,依然會使得一些異常從過濾器中拋出,怎樣處理呢?
我們使用error類型的過濾器,在請求的生命周期的pre
,route
,post
三個階段中有異常拋出的時候都會進入error
階段的處理,所以可以通過創建一個error
類型的過濾器來捕獲這些異常信息,并根據這些異常信息在請求上下文中注入需要返回給客戶端的錯誤描述。這里我們可以直接沿用try-catch
處理異常信息時用的那些error
參數,這樣就可以讓這些信息被SendErrorFilter
捕獲并組織成響應消息返回給客戶端。
@Component
public class ErrorFilter extends ZuulFilter{
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public String filterType() {
return "error";
}
@Override
public int filterOrder() {
return 10;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
Throwable throwable = context.getThrowable();
logger.error("this is a ErrorFilter :{}",throwable.getCause().getMessage());
context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
context.set("error.message",throwable.getCause().getMessage());
return null;
}
}
將上面的ThrowExceptionFilter
過濾器不使用try...catch
來處理,還是直接throw
異常出去,這樣ErrorFilter
過濾器就能接收到拋出的異常,并且能將其流轉到SendErrorFilter
進行處理。(原因在于pre類型的過濾器流轉到error類型的過濾器最后還是要流轉到post類型的過濾器,之后會講到)
@Override
public Object run() {
logger.info("this is a pre filter,it will throw a RuntimeException");
doSomething();
return null;
}
訪問http://192.168.5.5:6069/users/user/index
還是可以將異常和狀態碼打印在頁面上。
不足與優化
我們已經掌握了核心過濾器處理邏輯之下,對自定義過濾器中處理邏輯的兩種基本解決方法:一種是通過在各個階段的過濾器中增加try..catch塊,實現過濾器的內部處理;另外一種利用error類型過濾器的生命周期特性,集中處理pre,route,post階段拋出的異常信息。通常情況下,我們可以將這二種手段同時使用,其中第一種是對開發人員的基本要求,第二種是對第一種處理方式的補充,防止意外的異常拋出。
還是有一些不足,看看外部請求到達api網關服務之后,各個階段的過濾器是如何進行調度的,我們看com.netflix.zuul.http.ZuulServlet的service
方法實現,定義了zuul處理外部請求過程,各個類型的過濾器的執行邏輯。代碼中可以看到3個try...catch
塊,依次代表了pre
,route
,post
三個階段的過濾器調用。在catch的異常處理中我們可以看到它們都會被error過濾器進行處理(之前使用error
過濾器來定義統一的異常處理也正是利用了這個特性);error
類型的過濾器處理完畢后,處理來自post
階段的異常外,都會在被post
過濾器進行處理,
@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
// Marks this request as having passed through the "Zuul engine", as opposed to servlets
// explicitly bound in web.xml, for which requests will not have the same data attached
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
preRoute();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
route();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
postRoute();
} catch (ZuulException e) {
error(e);
return;
}
} catch (Throwable e) {
error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
對于從post
過濾器中拋出的異常的情況,在經過error
過濾器之后,就沒有其他類型的過濾器來接手了,回想之前實現的二種異常處理方法,其中非常核心的一點是,這兩種處理方法都在異常處理時向請求上下文添加了一系列的error.*
參數,而這些參數真正起作用的地方是在post
階段的SendErrorFilter
,在該過濾器中會使用這些參數來組織內容返回給客戶端。而對于post
階段拋出的異常的情況,由error
過濾器處理之后并不會再調用post
階段的請求,自然這些error.*
參數也就不會被SendErrorFilter
消費輸出。我們在自定義post
過濾器的時候,沒有正確處理異常,就依然有可能出現日志中沒有異常但請求響應內容為空的問題。可以將之前的ThrowExceptionFilter的filterType
改為post
來驗證這個問題的存在,
@Component
public class ThrowExceptionFilter extends ZuulFilter{
private static Logger logger = LoggerFactory.getLogger(ThrowExceptionFilter.class);
@Override
public String filterType() {
return "post";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
logger.info("this is a pre filter,it will throw a RuntimeException");
doSomething();
return null;
}
private void doSomething(){
throw new RuntimeException("exist some errors....");
}
}
訪問:http://192.168.5.5:6069/users/user/index
再去訪問就會出現白板不拋異常的情況。
解決這個問題的方法有很多種,最直接的我們可以在實現error過濾器的時候,直接組織結果返回就能實現效果。缺點很明顯,對于錯誤信息組織和返回代碼實現會存在多份,不利于維護,我們希望將post
過濾器拋出的異常交給SendErrorFilter
來處理。
我們在之前實現了一個ErrorFilter
來捕獲pre
,route
,post
過濾器拋出的異常,并組織error.*
參數保存到請求的上下文。由于我們的目標是沿用SendErrorFilter
,這些error.*
參數依然對我們有用,所以可以繼續沿用該過濾器,讓它在post過濾器拋出異常的時候,繼續組織error.*
參數,只是這里我們已經無法將這些error.*
參數傳遞給SendErrorFilter
過濾器來處理了。所以,我們需要在ErrorFilter
過濾器之后再定義一個error
類型的過濾器,讓它來實現SendErrorFilter
的功能,但是這個error
過濾器并不需要處理所有出現異常的情況,它僅僅處理post過濾器拋出的異常,復用它的run
方法,然后重寫它的類型,順序及執行條件,實現對原有邏輯的復用。
public class ErrorExtFilter extends SendErrorFilter{
@Override
public String filterType() {
return "error";
}
@Override
public int filterOrder() {
return 30; //大于ErrorFilter的值
}
//只處理post過濾器拋出異常的過濾器
@Override
public boolean shouldFilter() {
return true;
}
}
如何實現shouldFilter
的邏輯呢?當有異常拋出的時候,記錄下拋出的過濾器,這樣我們就可以在ErrorExtFilter
過濾器的shouldFilter
方法中獲取并以此判斷異常是否來自于post
階段的過濾器了。
為了擴展過濾器的處理邏輯,為請求上下文增加一些自定義屬性,深入了解zuul過濾器的核心處理器:com.netflix.zuul.FilterProcessor
,定義了過濾器調用和處理相關的核心方法:
- getInstance:該方法用來獲取當前處理器的實例
- setProcessor(FilterProcessor processor):該方法用來設置處理器實例,可以使用此方法來設置自定義的處理器。
- processZuulFilter(ZuulFilter filter):該方法定義了用來執行filter的具體邏輯,包括對請求上下文的設置,判斷是否應該執行,執行時一些異常處理等。
- runFilters(String sType):該方法會根據傳入的filterType來調用getFiltersByType(String filterType)獲取排序后的過濾器列表,然后輪詢這些過濾器,并調用processZuulFilter(ZuulFilter filter)來依次執行它們。
- preRoute():調用runFilters("pre")來執行所有pre類型的過濾器。
- route():調用runFilters("route")來執行所有route類型的過濾器。
- postRoute():調用runFilters("post")來執行所有post類型的過濾器。
- error():調用runFilters("error")來執行所有error類型的過濾器。
直接擴展processZuulFilter(ZuulFilter filter)
,當過濾器執行拋出異常的時候,我們來捕獲它,并向請求上下文中記錄一些信息,
public class DidiFilterProcessor extends FilterProcessor{
@Override
public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
try{
return super.processZuulFilter(filter);
}catch (ZuulException e){
RequestContext requestContext = RequestContext.getCurrentContext();
requestContext.set("failed.filter",filter);
throw e;
}
}
}
在上面的代碼實現中,創建了一個FilterProcessor
的子類,并重寫了processZuulFilter(ZuulFilter filter)
,雖然主邏輯依然使用了父類的實現,但是在最外層,我們為其增加了異常捕獲,并在異常處理中為請求上下文添加failed.filter
屬性,以存儲拋出異常的過濾器實例。在實現了這個擴展之后,我們可以完善之前的ErrorExtFilter
的shouldFilter()
方法了,通過從請求上下文中獲取信息作出正確的判斷:
@Component
public class ErrorExtFilter extends SendErrorFilter{
@Override
public String filterType() {
return "error";
}
@Override
public int filterOrder() {
return 30; //大于ErrorFilter的值
}
//只處理post過濾器拋出異常的過濾器
@Override
public boolean shouldFilter() {
//判斷,僅處理來自post過濾器引起的異常
RequestContext context = RequestContext.getCurrentContext();
ZuulFilter failedFilter =(ZuulFilter)context.get("failed.filter");
if(failedFilter != null && failedFilter.filterType().equals("post")){
return true;
}
return false;
}
}
最后,我們還要調用FilterProcessor.setProcessor(new DidiFilterProcessor());
方法來啟動自定義的核心處理器。
@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {
public static void main(String[] args) {
FilterProcessor.setProcessor(new DidiFilterProcessor());
SpringApplication.run(ZuulApplication.class,args);
}
自定義異常信息
實際應用到業務系統中,默認的錯誤信息并不符合系統設計的響應格式,那么我們就需要對返回的異常信息進行定制。對于如何定制這個錯誤信息有很多種方法可以實現。
最直接的是,可以編寫一個自定義的post過濾器來組織錯誤結果,該方法實現起來簡單粗暴,完全可以參考SendErrorFilter
的實現,然后直接組織請求響應而不是forward到/error端點,只是使用該方法時需要注意:為了替代SendErrorFilter
,還需要禁用SendErrorFilter
過濾器(下面提到怎么禁用zuul的filter)。
demo
寫的很隨意的一個過濾器,參考SendErrorFilter和SendResponseFilter過濾器:
@Component
public class SendNewErrorFilter extends ZuulFilter{
private Logger log = LoggerFactory.getLogger(getClass());
protected static final String SEND_ERROR_FILTER_RAN = "sendErrorFilter.ran";
@Override
public String filterType() {
return "post";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
// only forward to errorPath if it hasn't been forwarded to already
return ctx.containsKey("error.status_code")
&& !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
}
@Override
public Object run() {
try {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
HttpServletResponse servletResponse = ctx.getResponse();
servletResponse.setCharacterEncoding("UTF-8");
OutputStream outStream = servletResponse.getOutputStream();
String errormessage = "error,try again later!!";
InputStream is = new ByteArrayInputStream(errormessage.getBytes(servletResponse.getCharacterEncoding()));
writeResponse(is,outStream);
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
private void writeResponse(InputStream zin, OutputStream out) throws Exception {
byte[] bytes = new byte[1024];
int bytesRead = -1;
while ((bytesRead = zin.read(bytes)) != -1) {
out.write(bytes, 0, bytesRead);
}
}
}
然后禁用調默認的SendErrorFilter過濾器
zuul:
SendErrorFilter:
post:
disable: true
SendResponseFilter:
post:
disable: true
再去訪問http://192.168.1.57:6069/user-service/user/index
頁面展示自定義的異常。
方法二
如果不采用重寫過濾器的方式,依然想要使用SendErrorFilter
來處理異常返回的話,我們需要如何去定制返回的結果呢?這個時候,我們的關注點就不能放在zuul的過濾器上了,因為錯誤信息的生成實際上并不是由spring cloud zuul
完成的。我們在介紹SendErrorFilter
的時候提到過,它會根據請求上下文保存的錯誤信息來組織一個forward到/error
端點的請求來獲取錯誤響應,所以我們的擴展目標轉移到/error
端點的實現。
/error
端點的實現來源于Springboot的org.springframework.boot.autoconfigure.web.BasicErrorController
,
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new ResponseEntity<Map<String, Object>>(body, status);
}
protected Map<String, Object> getErrorAttributes(HttpServletRequest request,
boolean includeStackTrace) {
RequestAttributes requestAttributes = new ServletRequestAttributes(request);
return this.errorAttributes.getErrorAttributes(requestAttributes,
includeStackTrace);
}
getErrorAttributes
的實現默認的是DefaultErrorAttributes
的實現。
從源碼中可以看到,實現非常簡單,通過getErrorAttributes
方法根據請求參數組織錯誤信息的返回結果,而這里的getErrorAttributes
方法會將具體組織邏輯委托給org.springframework.boot.autoconfigure.web.ErrorAttributes
接口提供的
getErrorAttributes
來實現。在spring boot
的自動化配置機制中,默認會采用org.springframework.boot.autoconfigure.web.DefaultErrorAttributes
作為該接口的實現。
再定義Error處理的自動化配置中,該接口的默認實現采用@ConditionalOnMissingBean
修飾,說明DefaultErrorAttributes
實例僅在沒有ErrorAttributes
接口的實例時才會被創建出來使用,所以我們只需要自己編寫一個自定義的ErrorAttributes
接口實現類,并創建它的實例替代這個默認實現,達到自定義錯誤信息的效果。
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties(ResourceProperties.class)
public class ErrorMvcAutoConfiguration {
private final ApplicationContext applicationContext;
private final ServerProperties serverProperties;
private final ResourceProperties resourceProperties;
@Autowired(required = false)
private List<ErrorViewResolver> errorViewResolvers;
public ErrorMvcAutoConfiguration(ApplicationContext applicationContext,
ServerProperties serverProperties, ResourceProperties resourceProperties) {
this.applicationContext = applicationContext;
this.serverProperties = serverProperties;
this.resourceProperties = resourceProperties;
}
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
舉個例子,我們不希望將exception
屬性返回給客戶端,那么就可以編寫一個自定義的實現,它可以基于DefaultErrorAttribute
,然后重寫getErrorAttributes
方法,從原來的結果中將exception
移除即可,具體實現如下:
public class DidiErrorAttributes extends DefaultErrorAttributes{
@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
Map<String,Object> result = super.getErrorAttributes(requestAttributes,includeStackTrace);
result.put("error","missing error");
return result;
}
}
發現去掉四個屬性都不行,具體的細節源碼沒去研究,
最后,為了讓自定義的錯誤信息生成邏輯生效,需要在應用主類中加入如下代碼,為其創建實例代替默認的實現:
@Bean
public DefaultErrorAttributes errorAttributes(){
return new DidiErrorAttributes();
}