大家好,我是一安~
导读:本篇主要是对SpringCloud gateway
代码优化,如何实现全局异常捕获。
简介
在单体SpringBoot
项目中我们需要捕获全局异常只需要在项目中配置@RestControllerAdvice
和 @ExceptionHandler
就可以针对不同类型异常进行统一处理,统一包装后返回给前端调用方。但是在微服务架构下,例如网关调用业务系统失败(比如网关层token
解析异常、服务下线)这时候应用层的@RestControllerAdvice
就会不生效,因为此时根本没到应用层。
问题剖析
我们首先先看Spring Cloud Gateway
原生的实现:
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnWebApplication(
type = Type.REACTIVE
)
@ConditionalOnClass({WebFluxConfigurer.class})
@AutoConfigureBefore({WebFluxAutoConfiguration.class})
@EnableConfigurationProperties({ServerProperties.class, WebProperties.class})
public class ErrorWebFluxAutoConfiguration {
private final ServerProperties serverProperties;
public ErrorWebFluxAutoConfiguration(ServerProperties serverProperties) {
this.serverProperties = serverProperties;
}
@Bean
@ConditionalOnMissingBean(
value = {ErrorWebExceptionHandler.class},
search = SearchStrategy.CURRENT
)
@Order(-1)
public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes, WebProperties webProperties, ObjectProvider<ViewResolver> viewResolvers, ServerCodecConfigurer serverCodecConfigurer, ApplicationContext applicationContext) {
DefaultErrorWebExceptionHandler exceptionHandler = new DefaultErrorWebExceptionHandler(errorAttributes, webProperties.getResources(), this.serverProperties.getError(), applicationContext);
exceptionHandler.setViewResolvers((List)viewResolvers.orderedStream().collect(Collectors.toList()));
exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
exceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
return exceptionHandler;
}
@Bean
@ConditionalOnMissingBean(
value = {ErrorAttributes.class},
search = SearchStrategy.CURRENT
)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
}
在这个自动装配类中,只有两个简单的
Bean
,一个是DefaultErrorAttributes
:用来存储和操作异常错误信息的;还有一个DefaultErrorWebExceptionHandler
:Spring Boot
原生的处理。可以看到两个都使用了
@ConditionalOnMissingBean
注解,也就是我们可以通过自定义实现去覆盖它们。
DefaultErrorWebExceptionHandler
部分源码:
// 封装异常属性
protected Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
return this.errorAttributes.getErrorAttributes(request, options);
}
// 渲染异常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 int getHttpStatus(Map<String, Object> errorAttributes) {
return (Integer)errorAttributes.get("status");
}
最后封装到响应体的对象来源于 DefaultErrorWebExceptionHandler#getErrorAttributes()
,并且结果是一个Map<String, Object>
实例转换成的字节序列。原来的 RouterFunction
实现只支持HTML
格式返回,我们需要修改为JSON
格式返回(或者说支持所有格式返回)。DefaultErrorWebExceptionHandler#getHttpStatus()
是响应状态码的封装
方案
自定义CustomErrorWebFluxAutoConfiguration
:
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnWebApplication(
type = ConditionalOnWebApplication.Type.REACTIVE
)
@ConditionalOnClass({WebFluxConfigurer.class})
@AutoConfigureBefore({WebFluxAutoConfiguration.class})
@EnableConfigurationProperties({ServerProperties.class, WebProperties.class})
public class CustomErrorWebFluxAutoConfiguration {
private final ServerProperties serverProperties;
public CustomErrorWebFluxAutoConfiguration(ServerProperties serverProperties) {
this.serverProperties = serverProperties;
}
@Bean
@ConditionalOnMissingBean(
value = {ErrorWebExceptionHandler.class},
search = SearchStrategy.CURRENT
)
@Order(-1)
public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes, WebProperties webProperties, ObjectProvider<ViewResolver> viewResolvers, ServerCodecConfigurer serverCodecConfigurer, ApplicationContext applicationContext) {
CustomErrorWebExceptionHandler exceptionHandler = new CustomErrorWebExceptionHandler(errorAttributes, webProperties.getResources(), this.serverProperties.getError(), applicationContext);
exceptionHandler.setViewResolvers((List)viewResolvers.orderedStream().collect(Collectors.toList()));
exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
exceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
return exceptionHandler;
}
@Bean
@ConditionalOnMissingBean(
value = {ErrorAttributes.class},
search = SearchStrategy.CURRENT
)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
}
除了
ErrorWebExceptionHandler
的自定义实现,其他直接拷贝ErrorWebFluxAutoConfiguration
。
自定义CustomErrorWebExceptionHandler
:
public class CustomErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {
public CustomErrorWebExceptionHandler(ErrorAttributes errorAttributes, WebProperties.Resources resources, ErrorProperties errorProperties, ApplicationContext applicationContext) {
super(errorAttributes, resources, errorProperties, applicationContext);
}
@Override
protected Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
// 这里其实可以根据异常类型进行定制化逻辑
Throwable error = super.getError(request);
MergedAnnotation<ResponseStatus> responseStatusAnnotation = MergedAnnotations.from(error.getClass(), MergedAnnotations.SearchStrategy.TYPE_HIERARCHY).get(ResponseStatus.class);
HttpStatus errorStatus = this.determineHttpStatus(error, responseStatusAnnotation);
Map<String, Object> errorAttributes = new HashMap<>(8);
errorAttributes.put("message", error.getMessage());
errorAttributes.put("code", errorStatus.value());
errorAttributes.put("data", null);
return errorAttributes;
}
private HttpStatus determineHttpStatus(Throwable error, MergedAnnotation<ResponseStatus> responseStatusAnnotation) {
return error instanceof ResponseStatusException ? ((ResponseStatusException)error).getStatus() : (HttpStatus)responseStatusAnnotation.getValue("code", HttpStatus.class).orElse(HttpStatus.INTERNAL_SERVER_ERROR);
}
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(this.acceptsTextHtml(), this::renderErrorView).andRoute(RequestPredicates.all(), this::renderErrorResponse);
}
@Override
protected int getHttpStatus(Map<String, Object> errorAttributes) {
return (Integer)errorAttributes.get("code");
}
}
补充说明
网关ReactiveAuthenticationManager
方式自定义授权管理器:
放行所有的 OPTION
请求。判断某个请求 url
用户是否有权限访问。所有不存在的请求 url
直接无权限访问。
/**
* 方式2:ReactiveAuthenticationManager 配置方式要换成 WebFlux的方式
*/
@Bean
public SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception{
//token管理器
ReactiveAuthenticationManager tokenAuthenticationManager = new ReactiveJwtAuthenticationManager(tokenStore);
//认证过滤器
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(tokenAuthenticationManager);
authenticationWebFilter.setServerAuthenticationConverter(new ServerBearerTokenAuthenticationConverter());
http.httpBasic().disable()
.csrf().disable()
.authorizeExchange()
.pathMatchers(HttpMethod.OPTIONS).permitAll()
.anyExchange().access(accessManager)
//认证成功后没有权限操作
.and().exceptionHandling().accessDeniedHandler(new CustomServerAccessDeniedHandler())
//还没有认证时发生认证异常,比如token过期,token不合法
.authenticationEntryPoint(new CustomServerAuthenticationEntryPoint())
.and()
//oauth2认证过滤器
.addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
return http.build();
}
/**
* 无权限访问异常
*/
@Slf4j
public class CustomServerAccessDeniedHandler implements ServerAccessDeniedHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {
ServerHttpRequest request = exchange.getRequest();
return exchange.getPrincipal()
.doOnNext(principal -> log.info("用户:[{}]没有访问:[{}]的权限.", principal.getName(), request.getURI()))
.flatMap(principal -> {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.FORBIDDEN);
String body = "{"code":403,"message":"您无权限访问"}";
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer))
.doOnError(error -> DataBufferUtils.release(buffer));
});
}
}
/**
* 认证失败异常处理
*/
public class CustomServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {
return Mono.defer(() -> Mono.just(exchange.getResponse()))
.flatMap(response -> {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
String body = "{"code":401,"message":"token不合法或过期"}";
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer))
.doOnError(error -> DataBufferUtils.release(buffer));
});
}
}
如果这篇文章对你有所帮助,或者有所启发的话,帮忙 分享、收藏、点赞、在看,你的支持就是我坚持下去的最大动力!
原文始发于微信公众号(一安未来):SpringCloud Alibaba微服务实战之网关的全局异常处理
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/144984.html