自定义 Zuul 异常。

25

我在Zuul中有一个场景,当路由的URL对应的服务可能会宕机时,响应体会抛出500 HTTP状态和ZuulException的JSON响应体。

{
  "timestamp": 1459973637928,
  "status": 500,
  "error": "Internal Server Error",
  "exception": "com.netflix.zuul.exception.ZuulException",
  "message": "Forwarding error"
}

我想要做的就是定制或移除JSON响应,有时也想改变HTTP状态码。

我尝试使用@ControllerAdvice创建异常处理程序,但该异常未被处理程序捕获。

更新:

因此,我扩展了Zuul过滤器,我可以在运行方法中看到它已经执行了错误,那么我如何更改响应呢?以下是我得到的部分内容。我在某处读到了SendErrorFilter,但我该如何实现它以及它是什么?

public class CustomFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return "post";
    }

    @Override
    public int filterOrder() {

        return 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        final RequestContext ctx = RequestContext.getCurrentContext();
        final HttpServletResponse response = ctx.getResponse();
        if (HttpStatus.INTERNAL_SERVER_ERROR.value() == ctx.getResponse().getStatus()) {
            try {
                response.sendError(404, "Error Error"); //trying to change the response will need to throw a JSON body.
            } catch (final IOException e) {
                e.printStackTrace();
            } ;
        }

        return null;
    }

将此添加到具有@EnableZuulProxy注释的类中

@Bean
public CustomFilter customFilter() {
    return new CustomFilter();
}

你已经尝试过什么了吗? - Aritz
我尝试通过在类上注释@ControllerAdvice来添加异常处理程序,但似乎没有起作用。我认为我需要在Zuul过滤器中做一些事情,但不确定需要发生什么。 - Grinish Nepal
1
好的,那么最好编辑一下你的问题,以便展示你的尝试。因为你可能会注意到有些人认为你没有自己尝试过。如果你改进了问题,我会给你一个赞,因为我认为这是一个有趣的主题。 - Aritz
已更新问题。 - Grinish Nepal
2
编写自定义的“ErrorController”实现也可以帮助有人解决转发错误问题:https://jmnarloch.wordpress.com/2015/09/16/spring-cloud-zuul-error-handling/ - Vladimir Salin
7个回答

27
我们终于让它工作起来了[由我的同事编写]:-
public class CustomErrorFilter extends ZuulFilter {

    private static final Logger LOG = LoggerFactory.getLogger(CustomErrorFilter.class);
    @Override
    public String filterType() {
        return "post";
    }

    @Override
    public int filterOrder() {
        return -1; // Needs to run before SendErrorFilter which has filterOrder == 0
    }

    @Override
    public boolean shouldFilter() {
        // only forward to errorPath if it hasn't been forwarded to already
        return RequestContext.getCurrentContext().containsKey("error.status_code");
    }

    @Override
    public Object run() {
        try {
            RequestContext ctx = RequestContext.getCurrentContext();
            Object e = ctx.get("error.exception");

            if (e != null && e instanceof ZuulException) {
                ZuulException zuulException = (ZuulException)e;
                LOG.error("Zuul failure detected: " + zuulException.getMessage(), zuulException);

                // Remove error code to prevent further error handling in follow up filters
                ctx.remove("error.status_code");

                // Populate context with new response values
                ctx.setResponseBody(“Overriding Zuul Exception Body”);
                ctx.getResponse().setContentType("application/json");
                ctx.setResponseStatusCode(500); //Can set any error code as excepted
            }
        }
        catch (Exception ex) {
            LOG.error("Exception filtering in custom error filter", ex);
            ReflectionUtils.rethrowRuntimeException(ex);
        }
        return null;
    }
}

你能否建议一下,当Zuul层发生任何异常时,如何重定向到标准错误页面,因为我不想硬编码ResponseBody。谢谢@grinish-nepal。 - Joey Trang
所以您没有添加错误过滤器,只有发布过滤器。 - Bruce Lee
它是一个后置过滤器,但它在 sendErrorFilter 之前运行,您可以在注释中看到这一点。 - Grinish Nepal
1
最新版本的Zuul没有error.exceptionerror.status_code。相反,请使用throwable。顺便说一下,如果您想替换错误响应,则请使用erro过滤器类型而不是post - sancho

11

此答案所述,Zuul RequestContext中不包含error.exception
目前最新的Zuul错误过滤器:

@Component
public class ErrorFilter extends ZuulFilter {
    private static final Logger LOG = LoggerFactory.getLogger(ErrorFilter.class);

    private static final String FILTER_TYPE = "error";
    private static final String THROWABLE_KEY = "throwable";
    private static final int FILTER_ORDER = -1;

    @Override
    public String filterType() {
        return FILTER_TYPE;
    }

    @Override
    public int filterOrder() {
        return FILTER_ORDER;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        final RequestContext context = RequestContext.getCurrentContext();
        final Object throwable = context.get(THROWABLE_KEY);

        if (throwable instanceof ZuulException) {
            final ZuulException zuulException = (ZuulException) throwable;
            LOG.error("Zuul failure detected: " + zuulException.getMessage());

            // remove error code to prevent further error handling in follow up filters
            context.remove(THROWABLE_KEY);

            // populate context with new response values
            context.setResponseBody("Overriding Zuul Exception Body");
            context.getResponse().setContentType("application/json");
            // can set any error code as excepted
            context.setResponseStatusCode(503);
        }
        return null;
    }
}

为什么这不会覆盖我的响应体?它只是一个空的主体? - D.Tomov
@D.Tomov 如果您想要覆盖响应,请将过滤器类型更改为“error”。 - sancho

10

我曾经遇到同样的问题,并且能够用更简单的方法解决它

只需将以下代码放入你的过滤器的 run() 方法中

    if (<your condition>) {
        ZuulException zuulException = new ZuulException("User message", statusCode, "Error Details message");
        throw new ZuulRuntimeException(zuulException);
    }

并且SendErrorFilter将向用户传递带有所需statusCode的消息。

虽然这种异常嵌套看起来不太美观,但在这里它确实有效。


使用ZuulRuntimeException相比仅使用RuntimeException有哪些好处? - IcedDante
通过这种方式,您可以添加自定义消息和状态代码。使用RuntimeException将生成500的状态代码。不确定消息内容。 - Vasile Rotaru

4
转发通常由过滤器完成,在这种情况下,请求甚至不会到达控制器。这就解释了为什么您的@ControllerAdvice不起作用。
如果您在控制器中进行转发,则@ControllerAdvice应该起作用。 检查Spring是否创建了使用@ControllerAdvice注释的类的实例。为此,请在该类中放置断点并查看是否被命中。
在应该发生转发的控制器方法中也添加一个断点。您可能会意外调用另一个控制器方法来检查吗?
这些步骤应该帮助您解决问题。
在使用@ControllerAdvice注释的类中添加一个使用@ExceptionHandler(Exception.class)注释的ExceptionHandler方法,这应该会捕获每个异常。
编辑:
您可以尝试添加自己的过滤器,将Zuulfilter返回的错误响应转换。在那里,您可以根据需要更改响应。
如何自定义错误响应在此处说明:exception handling for filter in spring 正确放置过滤器可能有点棘手。关于正确位置不太确定,但您应该知道您的过滤器的顺序和处理异常的位置。
如果您将其放在Zuulfilter之前,则必须在调用doFilter()之后编写错误处理。
如果您将其放在Zuulfilter之后,则必须在调用doFilter()之前编写错误处理。
在doFilter()之前和之后添加断点可能有助于找到正确的位置。

实际上我这里没有转发任何内容。在我的Spring Boot应用程序中,我只有使用spring-cloud的@EnableZullProxy,以便将我的路由添加到配置中。因此,其中没有控制器。我希望controllerAdvice可以起作用,但由于转发是由过滤器完成的,所以我需要获取该部分并对其进行自定义,但我无法弄清楚如何做到这一点。 - Grinish Nepal

3
以下是使用@ControllerAdvice的步骤:
  1. 首先添加一个错误类型的过滤器,并让它在zuul中的SendErrorFilter之前运行。
  2. 确保从RequestContext中删除与异常相关联的键,以防止SendErrorFilter执行。
  3. 使用RequestDispatcher将请求转发到下面解释的ErrorController。
  4. 添加一个@RestController类并使其扩展AbstractErrorController,在执行新的错误过滤器时重新抛出异常(在key、exception中添加它,在您的控制器中从RequestContext获取它)。
现在,@ControllerAdvice类将捕获异常。

这个实际上是起作用了,只不过我将ErrorController实现到ControllerAdvice类中,并添加了RestController注解。这可能不太酷,但它能正常工作。 - Bruce Lee

2
    The simplest solution is to follow first 4 steps.


     1. Create your own CustomErrorController extends
        AbstractErrorController which will not allow the
        BasicErrorController to be called.
     2. Customize according to your need refer below method from
        BasicErrorController.

    <pre><code> 
        @RequestMapping
        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<>(body, status);
        }
    </pre></code> 

     4. You can control whether you want exception / stack trace to be printed or not can do as mentioned below:
    <pre><code>
    server.error.includeException=false
    server.error.includeStacktrace=ON_TRACE_PARAM
    </pre></code>
 ====================================================

    5. If you want all together different error response re-throw your custom exception from your CustomErrorController and implement the Advice class as mentioned below:

    <pre><code>

@Controller
@Slf4j
public class CustomErrorController extends BasicErrorController {

    public CustomErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties,
            List<ErrorViewResolver> errorViewResolvers) {

        super(errorAttributes, serverProperties.getError(), errorViewResolvers);
        log.info("Created");
    }

    @Override
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = getStatus(request);
        throw new CustomErrorException(String.valueOf(status.value()), status.getReasonPhrase(), body);
    }
}


    @ControllerAdvice
    public class GenericExceptionHandler {
    // Exception handler annotation invokes a method when a specific exception
        // occurs. Here we have invoked Exception.class since we
        // don't have a specific exception scenario.
        @ExceptionHandler(CustomException.class)
        @ResponseBody
        public ErrorListWsDTO customExceptionHandle(
                final HttpServletRequest request,
                final HttpServletResponse response,
                final CustomException exception) {
                LOG.info("Exception Handler invoked");
                ErrorListWsDTO errorData = null;
                errorData = prepareResponse(response, exception);
                response.setStatus(Integer.parseInt(exception.getCode()));
                return errorData;
        }

        /**
         * Prepare error response for BAD Request
         *
         * @param response
         * @param exception
         * @return
         */
        private ErrorListWsDTO prepareResponse(final HttpServletResponse response,
                final AbstractException exception) {
                final ErrorListWsDTO errorListData = new ErrorListWsDTO();
                final List<ErrorWsDTO> errorList = new ArrayList<>();
                response.setStatus(HttpStatus.BAD_REQUEST.value());
                final ErrorWsDTO errorData = prepareErrorData("500",
                        "FAILURE", exception.getCause().getMessage());
                errorList.add(errorData);
                errorListData.setErrors(errorList);
                return errorListData;
        }

        /**
         * This method is used to prepare error data
         *
         * @param code
         *            error code
         * @param status
         *            status can be success or failure
         * @param exceptionMsg
         *            message description
         * @return ErrorDTO
         */
        private ErrorWsDTO prepareErrorData(final String code, final String status,
                final String exceptionMsg) {

                final ErrorWsDTO errorDTO = new ErrorWsDTO();
                errorDTO.setReason(code);
                errorDTO.setType(status);
                errorDTO.setMessage(exceptionMsg);
                return errorDTO;
        }

    }
    </pre></code>

1

这是对我有效的方法。RestExceptionResponse是在@ControllerAdvice中使用的类,因此我们在出现内部ZuulExceptions的情况下有一个相同的异常响应。

@Component
@Log4j
public class CustomZuulErrorFilter extends ZuulFilter {

    private static final String SEND_ERROR_FILTER_RAN = "sendErrorFilter.ran";

    @Override
    public String filterType() {
        return ERROR_TYPE;
    }

    @Override
    public int filterOrder() {
        return SEND_ERROR_FILTER_ORDER - 1; // Needs to run before SendErrorFilter which has filterOrder == 0
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        Throwable ex = ctx.getThrowable();
        return ex instanceof ZuulException && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
    }

    @Override
    public Object run() {
        try {
            RequestContext ctx = RequestContext.getCurrentContext();
            ZuulException ex = (ZuulException) ctx.getThrowable();

            // log this as error
            log.error(StackTracer.toString(ex));

            String requestUri = ctx.containsKey(REQUEST_URI_KEY) ? ctx.get(REQUEST_URI_KEY).toString() : "/";
            RestExceptionResponse exceptionResponse = new RestExceptionResponse(HttpStatus.INTERNAL_SERVER_ERROR, ex, requestUri);

            // Populate context with new response values
            ctx.setResponseStatusCode(500);
            this.writeResponseBody(ctx.getResponse(), exceptionResponse);

            ctx.set(SEND_ERROR_FILTER_RAN, true);
        }
        catch (Exception ex) {
            log.error(StackTracer.toString(ex));
            ReflectionUtils.rethrowRuntimeException(ex);
        }
        return null;
    }


    private void writeResponseBody(HttpServletResponse response, Object body) throws IOException {
        response.setContentType("application/json");
        try (PrintWriter writer = response.getWriter()) {
            writer.println(new JSonSerializer().toJson(body));
        }
    }
}

输出结果如下:

{
    "timestamp": "2020-08-10 16:18:16.820",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/service",
    "exception": {
        "message": "Filter threw Exception",
        "exceptionClass": "com.netflix.zuul.exception.ZuulException",
        "superClasses": [
            "com.netflix.zuul.exception.ZuulException",
            "java.lang.Exception",
            "java.lang.Throwable",
            "java.lang.Object"
        ],
        "stackTrace": null,
        "cause": {
            "message": "com.netflix.zuul.exception.ZuulException: Forwarding error",
            "exceptionClass": "org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException",
            "superClasses": [
                "org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException",
                "java.lang.RuntimeException",
                "java.lang.Exception",
                "java.lang.Throwable",
                "java.lang.Object"
            ],
            "stackTrace": null,
            "cause": {
                "message": "Forwarding error",
                "exceptionClass": "com.netflix.zuul.exception.ZuulException",
                "superClasses": [
                    "com.netflix.zuul.exception.ZuulException",
                    "java.lang.Exception",
                    "java.lang.Throwable",
                    "java.lang.Object"
                ],
                "stackTrace": null,
                "cause": {
                    "message": "Load balancer does not have available server for client: template-scalable-service",
                    "exceptionClass": "com.netflix.client.ClientException",
                    "superClasses": [
                        "com.netflix.client.ClientException",
                        "java.lang.Exception",
                        "java.lang.Throwable",
                        "java.lang.Object"
                    ],
                    "stackTrace": null,
                    "cause": null
                }
            }
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接