本文共 5429 字,大约阅读时间需要 18 分钟。
设为星标,每天进步一点点!
在单体SpringBoot项目中我们需要捕获全局异常只需要在项目中配置 @RestControllerAdvice
和 @ExceptionHandler
就可以针对不同类型异常进行统一处理,统一包装后返回给前端调用方。
@Slf4j@RestControllerAdvicepublic class RestExceptionHandler { /** * 默认全局异常处理。 * @return ResultData */ @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ResultDataexception(Exception e) { log.error("全局异常信息 ex={}", e.getMessage(), e); return ResultData.fail(ReturnCode.RC500.getCode(),e.getMessage()); }}
但是在微服务架构下,例如网关调用业务系统失败(比如网关层jwt token解析异常、服务下线)这时候应用层的 @RestControllerAdvice
就会不生效,因为此时流量根本没到应用层。
下面我们分别模拟两种场景,让大家感受一下:
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{ /** 结果状态 ,具体状态码参见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 RouterFunctiongetRoutingFunction(ErrorAttributes errorAttributes) { return RouterFunctions.route(this.acceptsTextHtml(), this::renderErrorView).andRoute(RequestPredicates.all(), this::renderErrorResponse); } ...}
根据请求头确认返回什么资源格式。
返回的数据内容在 DefaultErrorAttributes
类中构建而成。
public class DefaultErrorAttributes implements ErrorAttributes { ... public MapgetErrorAttributes(ServerRequest request, boolean includeStackTrace) { Map errorAttributes = new LinkedHashMap(); errorAttributes.put("timestamp", new Date()); errorAttributes.put("path", request.path()); Throwable error = this.getError(request); MergedAnnotation 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 Monohandle(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]); } })); }}
符合我们的预期结果,实现网关层的异常拦截!
以上,希望对你有所帮助。
这里为大家准备了一份小小的礼物,关注公众号,输入如下代码,即可获得百度网盘地址,无套路领取!
001:《程序员必读书籍》 002:《从无到有搭建中小型互联网公司后台服务架构与运维架构》 003:《互联网企业高并发解决方案》 004:《互联网架构教学视频》 006:《SpringBoot实现点餐系统》 007:《SpringSecurity实战视频》 008:《Hadoop实战教学视频》 009:《腾讯2019Techo开发者大会PPT》010: 微信交流群
转载地址:http://tucoz.baihongyu.com/