SpringCloud微服务实战二十四 - SpringCloud Gateway的全局异常处理
前言
在单体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解析异常
故意写错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);