SpringCloud Alibaba微服务实战之网关的全局异常处理

SpringCloud Alibaba微服务实战之网关的全局异常处理

大家好,我是一安~

导读:本篇主要是对SpringCloud gateway代码优化,如何实现全局异常捕获。

简介

在单体SpringBoot项目中我们需要捕获全局异常只需要在项目中配置@RestControllerAdvice@ExceptionHandler就可以针对不同类型异常进行统一处理,统一包装后返回给前端调用方。但是在微服务架构下,例如网关调用业务系统失败(比如网关层token解析异常、服务下线)这时候应用层的@RestControllerAdvice就会不生效,因为此时根本没到应用层。

模拟token异常:SpringCloud Alibaba微服务实战之网关的全局异常处理

模拟服务下线:SpringCloud Alibaba微服务实战之网关的全局异常处理

问题剖析

我们首先先看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:用来存储和操作异常错误信息的;还有一个DefaultErrorWebExceptionHandlerSpring 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");
    }
}

效果:SpringCloud Alibaba微服务实战之网关的全局异常处理至此我们已成功优化了网关服务实现全局异常捕获。

补充说明

网关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微服务实战之网关的全局异常处理

Logback日志框架超详细教程


8种专坑同事的 SQL 写法,性能降低100倍,不来看看?


Nginx 可视化!配置监控一条龙!

SpringCloud Alibaba微服务实战之网关的全局异常处理

原文始发于微信公众号(一安未来):SpringCloud Alibaba微服务实战之网关的全局异常处理

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/144984.html

(0)
青莲明月的头像青莲明月

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!