一文吃透Spring Cloud gateway自定义错误处理Handler
作者:油而不腻张大叔
正文
我们来学习和了解下GatewayExceptionHandler
有助于我们处理spring gateway和webFlux中的异常自定义处理。它继承自ErrorWebExceptionHandler
, 类关系图如下:
通过类上述关系图,我们可以看到,DefaultErrorWebExceptionHandler
是ErrorWebExceptionHandler
的实现类,如果我们不自定义类异常处理器,系统将自动装配DefaultErrorWebExceptionHandler
。
我们就来庖丁解牛,一步步地来学习下这一组类和接口。
AbstractErrorWebExceptionHandler
AbstractErrorWebExceptionHandler
实现了ErrorWebExceptionHandler
接口,其中handle
方法是其核心方法。我们来看下其实现代码:
public Mono<Void> handle(ServerWebExchange exchange, Throwable throwable) { // 判断response是否已经提交,并且调用isDisconnectedClientError方法判断是否是客户端断开连接。 // 如果已经断开连接,已经无法将消息发送给客户端,直接Mono.error(throwable)抛出异常。 if (!exchange.getResponse().isCommitted() && !this.isDisconnectedClientError(throwable)) { // exchange.getAttributes().putIfAbsent(ERROR_INTERNAL_ATTRIBUTE, error); this.errorAttributes.storeErrorInformation(throwable, exchange); // 创建request ServerRequest request = ServerRequest.create(exchange, this.messageReaders); return this.getRoutingFunction(this.errorAttributes).route(request).switchIfEmpty(Mono.error(throwable)).flatMap((handler) -> { return handler.handle(request); }).doOnNext((response) -> { this.logError(request, response, throwable); }).flatMap((response) -> { return this.write(exchange, response); }); } else { return Mono.error(throwable); } }
通过Handle方法的代码我们看出主要是实现了以下的事项:
- 判断response是否已经提交,并且调用isDisconnectedClientError方法判断是否是客户端断开连接。
- 如果已经断开连接,已经无法将消息发送给客户端,直接Mono.error(throwable)抛出异常。
- 将异常信息存储到exchange中。
- 创建request。
- 获取路由函数。
- 调用路由函数的handle方法。
- 然后执行日志记录。
- 最后将response写入到exchange中。
isDisconnectedClientError方法
private boolean isDisconnectedClientError(Throwable ex) { return DISCONNECTED_CLIENT_EXCEPTIONS.contains(ex.getClass().getSimpleName()) ||this.isDisconnectedClientErrorMessage(NestedExceptionUtils.getMostSpecificCause(ex).getMessage()); }
DISCONNECTED_CLIENT_EXCEPTIONS是一个集合,里面存放了一些异常信息,如果是这些异常信息, 就认为是客户端断开连接了。 或者通过isDisconnectedClientErrorMessage方法判断是否是客户端断开连接的异常信息。
下面我们来看看isDisconnectedClientErrorMessage方法的实现。
isDisconnectedClientErrorMessage方法:
private boolean isDisconnectedClientErrorMessage(String message) { message = message != null ? message.toLowerCase() : ""; return message.contains("broken pipe") || message.contains("connection reset by peer"); }
上述代码的含义是:如果异常信息中包含“broken pipe”或者“connection reset by peer”,就认为是客户端断开连接了。
小结
综合起来,isDisconnectedClientError方法的含义是:如果DISCONNECTED_CLIENT_EXCEPTIONS集合中包含异常信息的类名或者异常信息中包含“broken pipe”或者“connection reset by peer”,就认为是客户端断开连接了。
NestedExceptionUtils
而isDisconnectedClientErrorMessage方法的参数message来自于NestedExceptionUtils.getMostSpecificCause(ex).getMessage()。我们进一步跟踪源码,可以看到NestedExceptionUtils.getMostSpecificCause(ex)方法的源码如下:
public static Throwable getMostSpecificCause(Throwable ex) { Throwable cause; Throwable result = ex; while(null != (cause = result.getCause()) && (result != cause)) { result = cause; } return result; }
上述代码的含义是:如果ex.getCause()不为空,并且ex.getCause()不等于ex,就将ex.getCause()赋值给result,然后继续执行while循环。直到ex.getCause()为空或者ex.getCause()等于ex,就返回result。
getRoutingFunction
我们看到获得路由调用的是getRoutingFunction
方法,而这个方法是一个抽象方法,需要在具体的继承类中实现。
logError
protected void logError(ServerRequest request, ServerResponse response, Throwable throwable) { if (logger.isDebugEnabled()) { logger.debug(request.exchange().getLogPrefix() + this.formatError(throwable, request)); } if (HttpStatus.resolve(response.rawStatusCode()) != null && response.statusCode().equals(HttpStatus.INTERNAL_SERVER_ERROR)) { logger.error(LogMessage.of(() -> { return String.format("%s 500 Server Error for %s", request.exchange().getLogPrefix(), this.formatRequest(request)); }), throwable); } }
上述代码的含义:
- 如果是debug级别的日志,就以debug的方式打印异常信息。
- 如果response的状态码是500,就打印异常信息。
用到的formatError方法就是简单的讲错误格式化,代码不再一一分析。
write
在把内容写入的时候用到了私有方法write,我们看看write方法的代码如下:
private Mono<? extends Void> write(ServerWebExchange exchange, ServerResponse response) { exchange.getResponse().getHeaders().setContentType(response.headers().getContentType()); return response.writeTo(exchange, new AbstractErrorWebExceptionHandler.ResponseContext()); }
上述代码的含义是:将response的contentType设置到exchange的response中,然后调用response的writeTo方法,将response写入到exchange中。 ResponseContext是一个内部类,主要是携带了外部类中的viewResolvers和messageWriters属性。
其他的方法
afterPropertiesSet
public void afterPropertiesSet() throws Exception { if (CollectionUtils.isEmpty(this.messageWriters)) { throw new IllegalArgumentException("Property 'messageWriters' is required"); } }
afterPropertiesSet主要是通过实现InitializingBean接口,实现afterPropertiesSet方法,判断messageWriters是否为空,如果为空就抛出异常。
renderDefaultErrorView
protected Mono<ServerResponse> renderDefaultErrorView(BodyBuilder responseBody, Map<String, Object> error) { StringBuilder builder = new StringBuilder(); Date timestamp = (Date)error.get("timestamp"); Object message = error.get("message"); Object trace = error.get("trace"); Object requestId = error.get("requestId"); builder.append("<html><body><h2>Whitelabel Error Page</h2>").append("<p>This application has no configured error view, so you are seeing this as a fallback.</p>").append("<div id='created'>").append(timestamp).append("</div>").append("<div>[").append(requestId).append("] There was an unexpected error (type=").append(this.htmlEscape(error.get("error"))).append(", status=").append(this.htmlEscape(error.get("status"))).append(").</div>"); if (message != null) { builder.append("<div>").append(this.htmlEscape(message)).append("</div>"); } if (trace != null) { builder.append("<div style='white-space:pre-wrap;'>").append(this.htmlEscape(trace)).append("</div>"); } builder.append("</body></html>"); return responseBody.bodyValue(builder.toString()); }
renderDefaultErrorView方法主要是渲染默认的错误页面,如果没有自定义的错误页面,就会使用这个方法渲染默认的错误页面。
renderErrorView
protected Mono<ServerResponse> renderErrorView(String viewName, BodyBuilder responseBody, Map<String, Object> error) { if (this.isTemplateAvailable(viewName)) { return responseBody.render(viewName, error); } else { Resource resource = this.resolveResource(viewName); return resource != null ? responseBody.body(BodyInserters.fromResource(resource)) : Mono.empty(); } }
上述代码的含义是:
- 如果viewName对应的模板存在,就使用模板渲染错误页面。
- 否则就使用静态资源渲染错误页面。 在使用资源渲染的时候,调用resolveResource方法,代码如下:
private Resource resolveResource(String viewName) { // 获得所有的静态资源的路径 String[] var2 = this.resources.getStaticLocations(); int var3 = var2.length; // 遍历所有的静态资源 for(int var4 = 0; var4 < var3; ++var4) { String location = var2[var4]; try { // 获得静态资源 Resource resource = this.applicationContext.getResource(location); // 获得静态资源的文件 resource = resource.createRelative(viewName + ".html"); if (resource.exists()) { return resource; } } catch (Exception var7) { } } return null; }
DefaultErrorWebExceptionHandler
DefaultErrorWebExceptionHandler是ErrorWebExceptionHandler的默认实现,继承了AbstractErrorWebExceptionHandler。通过对AbstractErrorWebExceptionHandler的分析,我们知道了大致实现原理,下面我们来看看DefaultErrorWebExceptionHandler的具体实现。
getRoutingFunction
在AbstractErrorWebExceptionHandler中,getRoutingFunction方法是一个抽象方法,需要子类实现。DefaultErrorWebExceptionHandler的getRoutingFunction方法的实现如下:
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) { return RouterFunctions.route(this.acceptsTextHtml(), this::renderErrorView).andRoute(RequestPredicates.all(), this::renderErrorResponse); }
我们看到代码中调用了RouterFunctions.route和andRoute方法。RouterFunctions.route的作用是根据请求的accept头信息,判断是否是text/html类型,如果是就调用renderErrorView方法,否则就调用renderErrorResponse方法。
acceptsTextHtml
protected RequestPredicate acceptsTextHtml() { return (serverRequest) -> { try { // 获得请求头中的accept信息 List<MediaType> acceptedMediaTypes = serverRequest.headers().accept(); // 定义一个MediaType.ALL的MediaType对象 MediaType var10001 = MediaType.ALL; // 移除MediaType.ALL acceptedMediaTypes.removeIf(var10001::equalsTypeAndSubtype); // 根据类型和权重对MediaType进行排序 MediaType.sortBySpecificityAndQuality(acceptedMediaTypes); // 获得acceptMediaTypes的stream对象 Stream var10000 = acceptedMediaTypes.stream(); // 获得MediaType.TEXT_HTML的MediaType对象 var10001 = MediaType.TEXT_HTML; var10001.getClass(); // 判断是否有MediaType.TEXT_HTML return var10000.anyMatch(var10001::isCompatibleWith); } catch (InvalidMediaTypeException var2) { return false; } }; }
acceptsTextHtml方法的作用是判断请求头中的accept信息是否是text/html类型。
renderErrorView
protected Mono<ServerResponse> renderErrorView(ServerRequest request) { // 通过getErrorAttributes方法获得错误信息 Map<String, Object> error = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)); // 获得错误状态码 int errorStatus = this.getHttpStatus(error); // 获得错误页面的模板名称 BodyBuilder responseBody = ServerResponse.status(errorStatus).contentType(TEXT_HTML_UTF8); // 获得错误页面的模板名称 return Flux.just(this.getData(errorStatus).toArray(new String[0])).flatMap((viewName) -> { return this.renderErrorView(viewName, responseBody, error); }).switchIfEmpty(this.errorProperties.getWhitelabel().isEnabled() ? this.renderDefaultErrorView(responseBody, error) : Mono.error(this.getError(request))).next(); }
其中
this.getData(errorStatus)
通过错误的code获得错误页面的名称。- this.renderErrorView(viewName, responseBody, error),通过错误页面的名称,错误信息,错误状态码,获得错误页面的响应体。
- this.renderDefaultErrorView(responseBody, error),通过错误信息,错误状态码,获得默认的错误页面的响应体。
renderErrorResponse方法
protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) { // 通过getErrorAttributes方法获得错误信息 Map<String, Object> error = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL)); // 获得错误状态码 // 设置为json格式的响应体 // 返回错误信息 return ServerResponse.status(this.getHttpStatus(error)).contentType(MediaType.APPLICATION_JSON).body(BodyInserters.fromValue(error)); }
通过上述代码看到主要执行了以下流程:
- 通过getErrorAttributes方法获得错误信息
- 获得错误状态码
- 设置为json格式的响应体
- 返回错误信息
getErrorAttributeOptions
protected ErrorAttributeOptions getErrorAttributeOptions(ServerRequest request, MediaType mediaType) { ErrorAttributeOptions options = ErrorAttributeOptions.defaults(); if (this.errorProperties.isIncludeException()) { options = options.including(new Include[]{Include.EXCEPTION}); } if (this.isIncludeStackTrace(request, mediaType)) { options = options.including(new Include[]{Include.STACK_TRACE}); } if (this.isIncludeMessage(request, mediaType)) { options = options.including(new Include[]{Include.MESSAGE}); } if (this.isIncludeBindingErrors(request, mediaType)) { options = options.including(new Include[]{Include.BINDING_ERRORS}); } return options; }
我们看到上述代码执行了下面的流程:
- 获得ErrorAttributeOptions对象
- 判断是否包含异常信息
- 判断是否包含堆栈信息
- 判断是否包含错误信息
- 判断是否包含绑定错误信息 如果包含这些信息,就将对应的信息加入到ErrorAttributeOptions对象中。而这些判断主要是通过ErrorProperties对象中的配置来判断的。
自定义自己的异常处理类
当认为默认的DefaultErrorWebExceptionHandler不满足需求时,我们可以自定义自己的异常处理类。
继承DefaultErrorWebExceptionHandler
只需要继承DefaultErrorWebExceptionHandler类,然后重写getErrorAttributes方法即可。
import org.springframework.context.annotation.Configuration; @Configuration @Order(-1) public class MyErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler { public MyErrorWebExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, ErrorProperties errorProperties, ApplicationContext applicationContext) { super(errorAttributes, resourceProperties, errorProperties, applicationContext); } @Override protected Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) { Map<String, Object> errorAttributes = super.getErrorAttributes(request, options); errorAttributes.put("ext", "自定义异常处理类"); return errorAttributes; } }
继承AbstractErrorWebExceptionHandler
import org.springframework.context.annotation.Configuration; @Configuration @Order(-1) public class MyErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler { public MyErrorWebExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, ErrorProperties errorProperties, ApplicationContext applicationContext) { super(errorAttributes, resourceProperties, errorProperties, applicationContext); } @Override protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) { return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse); } @Override protected Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) { Map<String, Object> errorAttributes = super.getErrorAttributes(request, options); errorAttributes.put("ext", "自定义异常处理类"); return errorAttributes; } }
继承WebExceptionHandler
import org.springframework.context.annotation.Configuration; @Configuration @Order(-1) public class MyErrorWebExceptionHandler implements WebExceptionHandler { @Override public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) { ServerHttpResponse response = exchange.getResponse(); if (response.isCommitted()) { return Mono.error(ex); } else { response.getHeaders().setContentType(MediaType.APPLICATION_JSON); response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); return response.writeWith(Mono.just(response.bufferFactory().wrap("自定义异常处理类".getBytes()))); } } }
总结
总体工作流程
经过对AbstractErrorWebExceptionHandler
和DefaultErrorWebExceptionHandler
中代码的阅读和跟踪。我们不难看出其工作机制。考虑到有返回API方式的json错误信息和错误页面方式的错误信息。所以ExceptionHandler通过request中的accept
的http头进行判断,如果是json格式则以json的方式进行响应,否则则以错误页面的方式进行响应。而错误页面的方式,通过错误的code获得错误页面的名称,然后通过错误页面的名称,错误信息,错误状态码,获得错误页面的响应体。
得到的经验
- 异步编程和同步编程之间差异很大,特别是在对request和response进行操作的时候。
- 源代码充分考虑各种情况和细节,在编程中值得我们去做参考。
以上就是一文吃透Spring Cloud gateway自定义错误处理Handler的详细内容,更多关于Spring Cloud gateway Handler的资料请关注脚本之家其它相关文章!