设为星标,每天进步一点点!
前言
在单体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]);
}
}));
}
}
测试结果
测试结果
符合我们的预期结果,实现网关层的异常拦截!
以上,希望对你有所帮助。
干货分享
这里为大家准备了一份小小的礼物,关注公众号,输入如下代码,即可获得百度网盘地址,无套路领取!
001:《程序员必读书籍》
002:《从无到有搭建中小型互联网公司后台服务架构与运维架构》
003:《互联网企业高并发解决方案》
004:《互联网架构教学视频》
006:《SpringBoot实现点餐系统》
007:《SpringSecurity实战视频》
008:《Hadoop实战教学视频》
009:《腾讯2019Techo开发者大会PPT》
010: 微信交流群
本文分享自微信公众号 - JAVA日知录(javadaily)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。