- 一般来讲,正常的流程是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();
}