前言
在单体SpringBoot项目中我们需要捕获全局异常只需要在项目中配置 @RestControllerAdvice
和 @ExceptionHandler
就可以针对不同类型异常进行统一处理,统一包装后返回给前端调用方。
@Slf4j @RestControllerAdvice public class RestExceptionHandler { /** * 默认全局异常处理。 * @return ResultData */ @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ResultData<String> exception(Exception e) { log.error("全局异常信息 ex={}", e.getMessage(), e); return ResultData.fail(ReturnCode.RC500.getCode(),e.getMessage()); } }
但是在微服务架构下,例如网关调用业务系统失败(比如网关层jwt token解析异常、服务下线)这时候应用层的 @RestControllerAdvice
就会不生效,因为此时流量根本没到应用层。
下面我们分别模拟两种场景,让大家感受一下:
- jwt解析异常
jwt解析异常
故意写错token让其无法解析,后端返回的数据为:
{ "timestamp": "2020-12-22T02:32:03.143+0000", "path": "/account-service/account/test/jianzh5", "status": 500, "error": "Internal Server Error", "message": "Cannot convert access token to JSON", "requestId": "7043b1f8-1" }
- 服务下线
服务下线异常
停止后端服务,后端返回的数据为:
{ "timestamp": "2020-12-22T02:36:13.281+0000", "path": "/account-service/account/getByCode/jianzh5", "status": 503, "error": "Service Unavailable", "message": "Unable to find instance for account-service", "requestId": "7043b1f8-6" }
在前后端分离的项目中,一般都要约定项目整体返回格式,前端需要根据返回数据确定页面逻辑。在我们项目例子中我们约定好的响应格式如下:
@Data @ApiModel(value = "统一返回结果封装",description = "接口返回统一结果") public class ResultData<T> { /** 结果状态 ,具体状态码参见ResultData.java*/ @ApiModelProperty(value = "状态码") private int status; @ApiModelProperty(value = "响应信息") private String message; @ApiModelProperty(value = "后端返回结果") private T data; @ApiModelProperty(value = "后端响应状态") private boolean success; @ApiModelProperty(value = "响应时间戳") private long timestamp ; public ResultData (){ this.timestamp = System.currentTimeMillis(); } ... }
很显然在这些情况下返回的异常数据并不符合我们的预期格式,我们需要改造网关返回数据。
原因剖析
在SpringCloud gateway中默认使用 DefaultErrorWebExceptionHandler
来处理异常。这个可以通过配置类 ErrorWebFluxAutoConfiguration
得之。
在 DefaultErrorWebExceptionHandler
类中的默认异常处理逻辑如下:
public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler { ... protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) { return RouterFunctions.route(this.acceptsTextHtml(), this::renderErrorView).andRoute(RequestPredicates.all(), this::renderErrorResponse); } ... }
根据请求头确认返回什么资源格式。
返回的数据内容在 DefaultErrorAttributes
类中构建而成。
public class DefaultErrorAttributes implements ErrorAttributes { ... public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) { Map<String, Object> errorAttributes = new LinkedHashMap(); errorAttributes.put("timestamp", new Date()); errorAttributes.put("path", request.path()); Throwable error = this.getError(request); MergedAnnotation<ResponseStatus> responseStatusAnnotation = MergedAnnotations.from(error.getClass(), SearchStrategy.TYPE_HIERARCHY).get(ResponseStatus.class); HttpStatus errorStatus = this.determineHttpStatus(error, responseStatusAnnotation); errorAttributes.put("status", errorStatus.value()); errorAttributes.put("error", errorStatus.getReasonPhrase()); errorAttributes.put("message", this.determineMessage(error, responseStatusAnnotation)); errorAttributes.put("requestId", request.exchange().getRequest().getId()); this.handleException(errorAttributes, this.determineException(error), includeStackTrace); return errorAttributes; } ... }
阅读到这里就可以看到为什么上面会返回那样的数据格式,接下来我们需要改写返回格式。
解决方案
这里我们我们可以自定义一个 CustomErrorWebExceptionHandler
类用来继承 DefaultErrorWebExceptionHandler
,然后修改生成前端响应数据的逻辑。再然后定义一个配置类,写法可以参考 ErrorWebFluxAutoConfiguration
,简单将异常类替换成 CustomErrorWebExceptionHandler
类即可。
这种方法大家请自行研究,基本都是复制代码,改写不复杂,这种方法我们就不演示了,这里给大家介绍另外一种写法:
我们定义一个全局异常类 GlobalErrorWebExceptionHandler
让其直接实现顶级接口 ErrorWebExceptionHandler
重写 handler()
方法,在 handler()
方法中返回我们自定义的响应类。但是需要注意重写的实现类优先级一定要小于内置 ResponseStatusExceptionHandler
经过它处理的获取对应错误类的响应码。
代码如下:
/** * 网关全局异常处理 * @author javadaily */ @Slf4j @Order(-1) @Configuration @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class GlobalErrorWebExceptionHandler implements ErrorWebExceptionHandler { private final ObjectMapper objectMapper; @Override public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) { ServerHttpResponse response = exchange.getResponse(); if (response.isCommitted()) { return Mono.error(ex); } // 设置返回JSON response.getHeaders().setContentType(MediaType.APPLICATION_JSON); if (ex instanceof ResponseStatusException) { response.setStatusCode(((ResponseStatusException) ex).getStatus()); } return response.writeWith(Mono.fromSupplier(() -> { DataBufferFactory bufferFactory = response.bufferFactory(); try { //返回响应结果 return bufferFactory.wrap(objectMapper.writeValueAsBytes(ResultData.fail(500,ex.getMessage()))); } catch (JsonProcessingException e) { log.error("Error writing response", ex); return bufferFactory.wrap(new byte[0]); } })); } }
测试结果
测试结果
符合我们的预期结果,实现网关层的异常拦截!
以上,希望对你有所帮助。