zuul异常处理

注意,关于怎样在zuul中处理异常,网上有很多方法,本文只讲述其中的一种。

首先,我们要知道以下几点:

  1. zuul中有一个默认的处理异常的filter,名为 SendErrorFilter,这个过滤器实际所做的工作只是将异常处理转发到了 ‘/error’ 这个路径上
  2. 承接上一点,在springboot中,有一个默认的处理异常的controller,名为 BasicErrorController,它映射到了 ‘/error’ 这个路径,这个controller对异常的处理是这样的:对于非rest方式,返回一个错误页面;对于rest方式,返回一个json。(做过springboot的应该都知道是哪个错误页面或什么样的json)
  3. 结合前面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": "系统维护时间,禁止访问"
}

最后,关于代码,有以下几点需要讲解:

  1. 自定义的异常为什么要继承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;
            }
        }
  1. 在自定义的控制器中,我们使用如下代码获取到了到错误码和异常信息:
 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());
  1. 为什么要在自定义的控制器中将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());
		}
	}
  1. 关于上述第一点,还有一个地方可以更直接地印证:
request.setAttribute("javax.servlet.error.status_code", exception.getStatusCode());

上面代码中的exception.getStatusCode()的默认实现如下:

    default int getStatusCode() {
    	return HttpStatus.INTERNAL_SERVER_ERROR.value();
	}
  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值