咱们平时在用SpringMVC
的时候,只要是通过DispatcherServlet
处理的请求,能够经过@ControllerAdvice
和@ExceptionHandler
自定义不一样类型异常的处理逻辑,具体能够参考ResponseEntityExceptionHandler
和DefaultHandlerExceptionResolver
,底层原理很简单,就是发生异常的时候搜索容器中已经存在的异常处理器而且匹配对应的异常类型,匹配成功以后使用该指定的异常处理器返回结果进行Response
的渲染,若是找不到默认的异常处理器则用默认的进行兜底(我的认为,Spring在不少功能设计的时候都有这种“有则使用自定义,无则使用默认提供”这种思想十分优雅)。java
SpringMVC
中提供的自定义异常体系在Spring-WebFlux
中并不适用,其实缘由很简单,二者底层的运行容器并不相同。WebExceptionHandler
是Spring-WebFlux
的异常处理器顶层接口,所以追溯到子类能够追踪到DefaultErrorWebExceptionHandler
是Spring Cloud Gateway
的全局异常处理器,配置类是ErrorWebFluxAutoConfiguration
。spring
先画一个假想可是贴近实际架构图,定位一下网关的做用:json
网关在整个架构中的做用是:后端
假设网关服务老是正常的前提下:bash
对于第1点来讲,假设后端应用不能平滑无损上线,会有必定的概率出现网关路由请求到一些后端的“僵尸节点(请求路由过去的时候,应用更好在重启或者恰好中止)”,这个时候会路由会失败抛出异常,通常状况是Connection Refuse。架构
对于第2点来讲,假设后端应用没有正确处理异常,那么应该会把异常信息通过网关转发回到服务端应用,这种状况理论上不会出现异常。app
其实还有第3点隐藏的问题,网关若是不仅仅承担路由的功能,还包含了鉴权、限流等功能,若是这些功能开发的时候对异常捕获没有作完善的处理甚至是逻辑自己存在BUG,有可能致使异常没有被正常捕获处理,走了默认的异常处理器DefaultErrorWebExceptionHandler
,默认的异常处理器的处理逻辑可能并不符合咱们预期的结果。curl
咱们能够先看默认的异常处理器的配置类ErrorWebFluxAutoConfiguration
:ide
@Configuration @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) @ConditionalOnClass(WebFluxConfigurer.class) @AutoConfigureBefore(WebFluxAutoConfiguration.class) @EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class }) public class ErrorWebFluxAutoConfiguration { private final ServerProperties serverProperties; private final ApplicationContext applicationContext; private final ResourceProperties resourceProperties; private final List<ViewResolver> viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; public ErrorWebFluxAutoConfiguration(ServerProperties serverProperties, ResourceProperties resourceProperties, ObjectProvider<ViewResolver> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer, ApplicationContext applicationContext) { this.serverProperties = serverProperties; this.applicationContext = applicationContext; this.resourceProperties = resourceProperties; this.viewResolvers = viewResolversProvider.orderedStream() .collect(Collectors.toList()); this.serverCodecConfigurer = serverCodecConfigurer; } @Bean @ConditionalOnMissingBean(value = ErrorWebExceptionHandler.class, search = SearchStrategy.CURRENT) @Order(-1) public ErrorWebExceptionHandler errorWebExceptionHandler( ErrorAttributes errorAttributes) { DefaultErrorWebExceptionHandler exceptionHandler = new DefaultErrorWebExceptionHandler( errorAttributes, this.resourceProperties, this.serverProperties.getError(), this.applicationContext); exceptionHandler.setViewResolvers(this.viewResolvers); exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters()); exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders()); return exceptionHandler; } @Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes( this.serverProperties.getError().isIncludeException()); } }
注意到两个Bean实例ErrorWebExceptionHandler
和DefaultErrorAttributes
都使用了@ConditionalOnMissingBean
注解,也就是咱们能够经过自定义实现去覆盖它们。先自定义一个CustomErrorWebFluxAutoConfiguration
(除了ErrorWebExceptionHandler
的自定义实现,其余直接拷贝ErrorWebFluxAutoConfiguration
):测试
@Configuration @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) @ConditionalOnClass(WebFluxConfigurer.class) @AutoConfigureBefore(WebFluxAutoConfiguration.class) @EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class}) public class CustomErrorWebFluxAutoConfiguration { private final ServerProperties serverProperties; private final ApplicationContext applicationContext; private final ResourceProperties resourceProperties; private final List<ViewResolver> viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; public CustomErrorWebFluxAutoConfiguration(ServerProperties serverProperties, ResourceProperties resourceProperties, ObjectProvider<ViewResolver> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer, ApplicationContext applicationContext) { this.serverProperties = serverProperties; this.applicationContext = applicationContext; this.resourceProperties = resourceProperties; this.viewResolvers = viewResolversProvider.orderedStream() .collect(Collectors.toList()); this.serverCodecConfigurer = serverCodecConfigurer; } @Bean @ConditionalOnMissingBean(value = ErrorWebExceptionHandler.class, search = SearchStrategy.CURRENT) @Order(-1) public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) { // TODO 这里完成自定义ErrorWebExceptionHandler实现逻辑 return null; } @Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException()); } }
ErrorWebExceptionHandler
的实现,能够直接参考DefaultErrorWebExceptionHandler
,甚至直接继承DefaultErrorWebExceptionHandler
,覆盖对应的方法便可。这里直接把异常信息封装成下面格式的Response
返回,最后须要渲染成JSON格式:
{ "code": 200, "message": "描述信息", "path" : "请求路径", "method": "请求方法" }
咱们须要分析一下DefaultErrorWebExceptionHandler
中的一些源码:
// 封装异常属性 protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) { return this.errorAttributes.getErrorAttributes(request, includeStackTrace); } // 渲染异常Response protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) { boolean includeStackTrace = isIncludeStackTrace(request, MediaType.ALL); Map<String, Object> error = getErrorAttributes(request, includeStackTrace); return ServerResponse.status(getHttpStatus(error)) .contentType(MediaType.APPLICATION_JSON_UTF8) .body(BodyInserters.fromObject(error)); } // 返回路由方法基于ServerResponse的对象 @Override protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) { return route(acceptsTextHtml(), this::renderErrorView).andRoute(all(), this::renderErrorResponse); } // HTTP响应状态码的封装,原来是基于异常属性的status属性进行解析的 protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) { int statusCode = (int) errorAttributes.get("status"); return HttpStatus.valueOf(statusCode); }
肯定三点:
DefaultErrorWebExceptionHandler#getErrorAttributes()
,而且结果是一个Map<String, Object>
实例转换成的字节序列。RouterFunction
实现只支持HTML格式返回,咱们须要修改成JSON格式返回(或者说支持全部格式返回)。DefaultErrorWebExceptionHandler#getHttpStatus()
是响应状态码的封装,原来的逻辑是基于异常属性getErrorAttributes()
的status属性进行解析的。自定义的JsonErrorWebExceptionHandler
以下:
public class JsonErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler { public JsonErrorWebExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, ErrorProperties errorProperties, ApplicationContext applicationContext) { super(errorAttributes, resourceProperties, errorProperties, applicationContext); } @Override protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) { // 这里其实能够根据异常类型进行定制化逻辑 Throwable error = super.getError(request); Map<String, Object> errorAttributes = new HashMap<>(8); errorAttributes.put("message", error.getMessage()); errorAttributes.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value()); errorAttributes.put("method", request.methodName()); errorAttributes.put("path", request.path()); return errorAttributes; } @Override protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) { return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse); } @Override protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) { // 这里其实能够根据errorAttributes里面的属性定制HTTP响应码 return HttpStatus.INTERNAL_SERVER_ERROR; } }
配置类CustomErrorWebFluxAutoConfiguration
添加JsonErrorWebExceptionHandler
:
@Bean @ConditionalOnMissingBean(value = ErrorWebExceptionHandler.class, search = SearchStrategy.CURRENT) @Order(-1) public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) { JsonErrorWebExceptionHandler exceptionHandler = new JsonErrorWebExceptionHandler( errorAttributes, resourceProperties, this.serverProperties.getError(), applicationContext); exceptionHandler.setViewResolvers(this.viewResolvers); exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters()); exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders()); return exceptionHandler; }
很简单,这里把异常的HTTP响应状态码统一为HttpStatus.INTERNAL_SERVER_ERROR(500)
,改造的东西并很少,只要了解原来异常处理的上下文逻辑便可。
测试场景一:只启动网关,下游服务不启动的状况下直接调用下游服务:
curl http://localhost:9090/order/host // 响应结果 {"path":"/order/host","code":500,"message":"Connection refused: no further information: localhost/127.0.0.1:9091","method":"GET"}
测试场景二:下游服务正常启动和调用,网关自身抛出异常。
在网关应用自定义一个全局过滤器而且故意抛出异常:
@Component public class ErrorGlobalFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { int i = 1/0; return chain.filter(exchange); } }
curl http://localhost:9090/order/host // 响应结果 {"path":"/order/host","code":500,"message":"/ by zero","method":"GET"}
响应结果和定制的逻辑一致,而且后台的日志也打印了对应的异常堆栈。
笔者一直认为,作异常分类和按照分类处理是工程里面十分重要的一环。笔者在所在公司负责的系统中,坚持实现异常分类捕获,主要是须要区分能够重试补偿以及没法重试须要及时预警的异常,这样子才能针对可恢复异常定制自愈逻辑,对不能恢复的异常及时预警和人为介入。因此,Spring Cloud Gateway
这个技术栈也必须调研其自定义异常的处理逻辑。
(本文完 c-1-d e-a-20190511)