注意,关于怎样在zuul中处理异常,网上有很多方法,本文只讲述其中的一种。
首先,我们要知道以下几点:
- zuul中有一个默认的处理异常的filter,名为
SendErrorFilter
,这个过滤器实际所做的工作只是将异常处理转发到了 ‘/error’ 这个路径上 - 承接上一点,在springboot中,有一个默认的处理异常的controller,名为
BasicErrorController
,它映射到了 ‘/error’ 这个路径,这个controller对异常的处理是这样的:对于非rest方式,返回一个错误页面;对于rest方式,返回一个json。(做过springboot的应该都知道是哪个错误页面或什么样的json) - 结合前面2点,可以看出,zuul默认的异常处理是:zuul自己不处理,交给springboot处理。
前提讲完之后,我们讲一下本文的背景:根据业务的实际需要,我们在zuul中定义了一个filter,如果某个请求不满足该filter的过滤条件,则返回给客户端一个自定义的json格式的错误码。
我们的思路是,当filter中设定的条件不满足的时候,抛出自定义的异常。根据前面的介绍,该异常会被zuul内置的处理异常的filter(SendErrorFilter
)捕获,然后交给springboot处理。然而,springboot默认的返回结果并不是我们想要的,我们想要的是返回一个包含自定义错误码和错误消息的json。那么,我们怎么实现呢?本文的思路是覆盖springboot默认的处理异常的controller(BasicErrorController
)。
接下来看代码
- 首先,看一下自定义的filter,这个过滤器很简单:0点到20点之间不可以访问系统
@Component
public class AccessTimeFilter extends ZuulFilter {
/**
* 当前时间
*/
private static final LocalTime NOW = LocalTime.now();
/**
* 零点
*/
private static final LocalTime ZERO_CLOCK = LocalTime.of(0, 0);
/**
* 二十点
*/
private static final LocalTime TWENTY_CLOCK = LocalTime.of(20, 0);
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 5;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
if (NOW.isAfter(ZERO_CLOCK) && NOW.isBefore(TWENTY_CLOCK)) {
// 如果用户在0-20点之间访问了系统,则抛出异常
throw new GatewayException(ApiResponseCode.CODE_INVALID_ACCESS_TIME);
}
return null;
}
}
- 然后,看一下自定义的异常
GatewayException
,这个异常包装了我们的错误码和错误消息
/**
* 自定义网关异常
*
* 至于为什么要继承 {@link ZuulException}
*
* 可以参考 {@link com.netflix.zuul.FilterProcessor#processZuulFilter(ZuulFilter)} 方法中的异常处理
*/
public class GatewayException extends ZuulException {
public GatewayException(ApiResponseCode apiResponseCode) {
super(apiResponseCode.getMessage(), apiResponseCode.getCode(),
apiResponseCode.getMessage());
}
public GatewayException(int code, String message) {
super(message, code, message);
}
}
- 最后,我们覆盖springboot默认的处理异常的controller,注意,怎样覆盖springbootmore的异常处理controller不是重点,网上有很多方法,这里我们要关注的是异常处理逻辑。
@RestController
public class GatewayErrorController implements ErrorController {
/**
* zuul的异常处理
*
* @param request HTTP请求
* @return API统一响应
*/
@RequestMapping
public ApiResponse<Void> error(HttpServletRequest request, HttpServletResponse response) {
Integer code = (Integer) request.getAttribute("javax.servlet.error.status_code");
Exception exception = (Exception) request.getAttribute("javax.servlet.error.exception");
String message = "服务器内部错误";
if (exception instanceof ZuulException) {
message = exception.getMessage();
}
response.setStatus(HttpStatus.OK.value());
return new ApiResponse<>(code, message);
}
@Override
public String getErrorPath() {
return "/error";
}
}
关键代码到这里就没了,我们看一下效果:
{
"code": 912,
"message": "系统维护时间,禁止访问"
}
最后,关于代码,有以下几点需要讲解:
- 自定义的异常为什么要继承
ZuulException
。最早的时候,我写的自定义的异常并没有继承ZuulException
,这时候,发现返回的错误码总是500,经过排查找到了原因,在FilterProcessor
中有下面几行代码。从中可以看出,如果捕获到的异常是ZuulException
的实例,那么直接抛出;否则,将捕获到的异常包装成为ZuulException
再抛出。问题就出在当捕获到的异常不是ZuulException
的实例的时候,zuul会将错误码设置为500(见下面代码)。所以,我们就选择让自定义的异常继承ZuulException
。
} catch (Throwable e) {
if (bDebug) {
Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage());
}
usageNotifier.notify(filter, ExecutionStatus.FAILED);
if (e instanceof ZuulException) {
throw (ZuulException) e;
} else {
ZuulException ex = new ZuulException(e, "Filter threw Exception", 500, filter.filterType() + ":" + filterName);
ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
throw ex;
}
}
- 在自定义的控制器中,我们使用如下代码获取到了到错误码和异常信息:
Integer code = (Integer) request.getAttribute("javax.servlet.error.status_code");
Exception exception = (Exception) request.getAttribute("javax.servlet.error.exception");
那么为什么可以这么做呢?看一下SendErrorFilter
中是怎么处理的就知道了:
request.setAttribute("javax.servlet.error.status_code", exception.getStatusCode());
log.warn("Error during filtering", exception.getThrowable());
request.setAttribute("javax.servlet.error.exception", exception.getThrowable());
- 为什么要在自定义的控制器中将response的状态码设置为200。这个其实和zuul是有关的,在zuul的
RequestContext
中有这样一段代码:
/**
* Use this instead of response.setStatusCode()
*
* @param nStatusCode
*/
public void setResponseStatusCode(int nStatusCode) {
getResponse().setStatus(nStatusCode);
set("responseStatusCode", nStatusCode);
}
从中可以看出,它将HTTP响应码的值设成nStatusCode
的值,当出现异常的时候,这个nStatusCode
的值就是异常的状态码(见下面SendErrorFilter
中的代码)。这显然不是我们想要的(浏览器怎么会认识你自定义的响应码)。所以,简单起见,我们直接在控制器中将响应码设为200。
if (dispatcher != null) {
ctx.set(SEND_ERROR_FILTER_RAN, true);
if (!ctx.getResponse().isCommitted()) {
// 将HTTP响应的状态码设置为异常状态码
ctx.setResponseStatusCode(exception.getStatusCode());
dispatcher.forward(request, ctx.getResponse());
}
}
- 关于上述第一点,还有一个地方可以更直接地印证:
request.setAttribute("javax.servlet.error.status_code", exception.getStatusCode());
上面代码中的exception.getStatusCode()
的默认实现如下:
default int getStatusCode() {
return HttpStatus.INTERNAL_SERVER_ERROR.value();
}