美文网首页
Spring Cloud Gateway过滤器精确控制异常返回(

Spring Cloud Gateway过滤器精确控制异常返回(

作者: 程序员欣宸 | 来源:发表于2021-12-01 08:12 被阅读0次

    欢迎访问我的GitHub

    这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

    本篇概览

    在这里插入图片描述
    • 正如上图所示,异常发生时系统固定返回8个字段,这就有些不够灵活了,在一些对格式和内容有严格要求的场景下,咱们需要能够完全控制返回码和返回body的内容,如下所示,只返回三个字段,每个字段都是完全为业务服务的:
    {
        # 这是有具体业务含义的返回码
        "code": "010020003",
    
        # 这是能精确描述错误原因的文本信息
        "message": "请确保请求参数中的user-id字段是有效的",
        
        # 这是常规的业务数据,发生异常时该字段为空
        "data": null
    }
    
    • 今天咱们的目标就是通过编码定制异常发生时的返回信息,具体内容就是上述JSON数据:只有code、message、data三个字段

    源码下载

    名称 链接 备注
    项目主页 https://github.com/zq2599/blog_demos 该项目在GitHub上的主页
    git仓库地址(https) https://github.com/zq2599/blog_demos.git 该项目源码的仓库地址,https协议
    git仓库地址(ssh) git@github.com:zq2599/blog_demos.git 该项目源码的仓库地址,ssh协议
    • 这个git项目中有多个文件夹,本篇的源码在<font color="blue">spring-cloud-tutorials</font>文件夹下,如下图红框所示:
    在这里插入图片描述
    • <font color="blue">spring-cloud-tutorials</font>文件夹下有多个子工程,本篇的代码是<font color="red">gateway-change-body</font>,如下图红框所示:
    在这里插入图片描述

    为何不用常规手段

    • 提到全局异常处理,经验丰富的您应该想到了常用的ControllerAdvice和ExceptionHandler注解修饰的全局异常处理类,但是Spring Cloud Gateway是基于WebFlux的,咱们之前处理异常时用到的HttpServletRequest在Spring Cloud Gateway中并不适用,因此,不能用ControllerAdvice和ExceptionHandler的手段来处理全局异常

    基本思路

    • 在动手前做好充足的理论分析,写出的代码才能正常工作

    • 打开DefaultErrorWebExceptionHandler.java,找到renderErrorResponse方法,来看看Spring Cloud Gateway原本是如何构造异常返回内容的:

    在这里插入图片描述
    • 此刻聪明的您应该想到怎么做了:做个新的类继承DefaultErrorWebExceptionHandler,覆盖其renderErrorResponse方法,新的renderErrorResponse方法中,按照实际业务需要来设置返回内容,没错,这就是咱们的思路,不过还要细化一下,最终具体的步骤如下:
    1. 新增一个异常类<font color="blue">CustomizeInfoException.java</font>,该类有三个字段:http返回码、业务返回码、业务描述信息

    2. 在返回异常的代码位置,使用CustomizeInfoException类来抛出异常,按照实际业务场景设置CustomizeInfoException实例的各个字段

    3. 新增MyErrorWebExceptionHandler.java,继承自DefaultErrorWebExceptionHandler,重写了renderErrorResponse方法,这里面检查异常实例是否是CustomizeInfoException类型,如果是,就从其中取出http返回码、业务返回码、业务描述信息等字段,构造返回body的内容,异常实例若不是CustomizeInfoException类型,就保持之前的处理逻辑不变;

    4. 新增configuration类,用于将MyErrorWebExceptionHandler实例注册到spring环境

    • 分析完毕,开始编码吧,为了简单起见,本篇不再新增maven子工程,而是基于前文创建的子工程<font color="red">gateway-change-body</font>,在这里面继续写代码;

    编码

    • 新增异常类<font color="blue">CustomizeInfoException.java</font>:
    package com.bolingcavalry.changebody.exception;
    
    import lombok.Data;
    import org.springframework.http.HttpStatus;
    
    @Data
    public class CustomizeInfoException extends Exception {
        /**
         * http返回码
         */
        private HttpStatus httpStatus;
    
        /**
         * body中的code字段(业务返回码)
         */
        private String code;
    
        /**
         * body中的message字段(业务返回信息)
         */
        private String message;
    }
    
    • 修改RequestBodyRewrite.java的apply方法,这里面是在处理请求body,如果检查到没有<font color="blue">user-id</font>字段,就不将请求转发到服务提供方<font color="blue">provider-hello</font>,而是返回错误,这里的错误就用CustomizeInfoException类来处理:
    @Override
        public Publisher<String> apply(ServerWebExchange exchange, String body) {
            try {
                Map<String, Object> map = objectMapper.readValue(body, Map.class);
    
                // 如果请求参数中不含user-id,就返回异常
                if (!map.containsKey("user-id")) {
                    CustomizeInfoException customizeInfoException = new CustomizeInfoException();
                    // 这里返回406,您可以按照业务需要自行调整
                    customizeInfoException.setHttpStatus(HttpStatus.NOT_ACCEPTABLE);
    
                    // 这里按照业务需要自行设置code
                    customizeInfoException.setCode("010020003");
    
                    // 这里按照业务需要自行设置返回的message
                    customizeInfoException.setMessage("请确保请求参数中的user-id字段是有效的");
    
                    return Mono.error(customizeInfoException);
                }
    
                // 取得id
                int userId = (Integer)map.get("user-id");
    
                // 得到nanme后写入map
                map.put("user-name", mockUserName(userId));
    
                return Mono.just(objectMapper.writeValueAsString(map));
            } catch (Exception ex) {
                log.error("1. json process fail", ex);
                return Mono.error(new Exception("1. json process fail", ex));
            }
        }
    
    • 异常处理类MyErrorWebExceptionHandler.java,这里有一处需要<font color="red">重点关注的是:</font>下面的代码仅是参考而已,您无需拘泥于CustomizeInfoException有关的逻辑,完全能按照业务需求自由设置返回的状态码和body:
    package com.bolingcavalry.changebody.handler;
    
    import com.bolingcavalry.changebody.exception.CustomizeInfoException;
    import org.springframework.boot.autoconfigure.web.ErrorProperties;
    import org.springframework.boot.autoconfigure.web.WebProperties;
    import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
    import org.springframework.boot.web.reactive.error.ErrorAttributes;
    import org.springframework.context.ApplicationContext;
    import org.springframework.http.MediaType;
    import org.springframework.web.reactive.function.BodyInserters;
    import org.springframework.web.reactive.function.server.ServerRequest;
    import org.springframework.web.reactive.function.server.ServerResponse;
    import reactor.core.publisher.Mono;
    import java.util.HashMap;
    import java.util.Map;
    
    public class MyErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {
    
        public MyErrorWebExceptionHandler(ErrorAttributes errorAttributes, WebProperties.Resources resources, ErrorProperties errorProperties, ApplicationContext applicationContext) {
            super(errorAttributes, resources, errorProperties, applicationContext);
        }
    
        @Override
        protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
            // 返回码
            int status;
            // 最终是用responseBodyMap来生成响应body的
            Map<String, Object> responseBodyMap = new HashMap<>();
    
            // 这里和父类的做法一样,取得DefaultErrorAttributes整理出来的所有异常信息
            Map<String, Object> error = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
    
            // 原始的异常信息可以用getError方法取得
            Throwable throwable = getError(request);
    
            // 如果异常类是咱们定制的,就定制
            if (throwable instanceof CustomizeInfoException) {
                CustomizeInfoException myGatewayException = (CustomizeInfoException) throwable;
                // http返回码、body的code字段、body的message字段,这三个信息都从CustomizeInfoException实例中获取
                status = myGatewayException.getHttpStatus().value();
                responseBodyMap.put("code", myGatewayException.getCode());
                responseBodyMap.put("message", myGatewayException.getMessage());
                responseBodyMap.put("data", null);
            } else {
                // 如果不是咱们定制的异常,就维持和父类一样的逻辑
                // 返回码
                status = getHttpStatus(error);
                // body内容
                responseBodyMap.putAll(error);
            }
    
            return ServerResponse
                    // http返回码
                    .status(status)
                    // 类型和以前一样
                    .contentType(MediaType.APPLICATION_JSON)
                    // 响应body的内容
                    .body(BodyInserters.fromValue(responseBodyMap));
        }
    }
    
    • 最后是配置类MyErrorWebFluxAutoConfiguration.java:
    package com.bolingcavalry.changebody.config;
    
    import com.bolingcavalry.changebody.handler.MyErrorWebExceptionHandler;
    import org.springframework.beans.factory.ObjectProvider;
    import org.springframework.boot.autoconfigure.AutoConfigureBefore;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
    import org.springframework.boot.autoconfigure.web.ServerProperties;
    import org.springframework.boot.autoconfigure.web.WebProperties;
    import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.boot.web.reactive.error.ErrorAttributes;
    import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.annotation.Order;
    import org.springframework.http.codec.ServerCodecConfigurer;
    import org.springframework.web.reactive.config.WebFluxConfigurer;
    import org.springframework.web.reactive.result.view.ViewResolver;
    import java.util.stream.Collectors;
    
    @Configuration(proxyBeanMethods = false)
    @AutoConfigureBefore(WebFluxAutoConfiguration.class)
    public class MyErrorWebFluxAutoConfiguration {
    
        private final ServerProperties serverProperties;
    
        public MyErrorWebFluxAutoConfiguration(ServerProperties serverProperties) {
            this.serverProperties = serverProperties;
        }
    
        @Bean
        @Order(-1)
        public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes,
                                                                 org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties,
                                                                 WebProperties webProperties, ObjectProvider<ViewResolver> viewResolvers,
                                                                 ServerCodecConfigurer serverCodecConfigurer, ApplicationContext applicationContext) {
    
            MyErrorWebExceptionHandler exceptionHandler = new MyErrorWebExceptionHandler(errorAttributes,
                    resourceProperties.hasBeenCustomized() ? resourceProperties : webProperties.getResources(),
                    this.serverProperties.getError(), applicationContext);
            exceptionHandler.setViewResolvers(viewResolvers.orderedStream().collect(Collectors.toList()));
            exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
            exceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
            return exceptionHandler;
        }
    }
    
    • 编码完成,该把程序运行起来验证效果了;

    验证

    • 启动应用gateway-change-body

    • 用postman发起POST请求,地址是<font color="blue">http://localhost:8081/hello/change</font>,如下图,红框2中的http返回码是咱们代码里设置的,红框3显示返回的内容就是咱们定制的那三个字段:

    在这里插入图片描述
    • 至此,控制Spring Cloud Gateway应用异常返回的实战已经全部完成,从源码分析结合实战演练,希望欣宸的文章能陪伴您深入了解Spring Cloud Gateway,打造出更加强大的网关应用;

    你不孤单,欣宸原创一路相伴

    1. Java系列
    2. Spring系列
    3. Docker系列
    4. kubernetes系列
    5. 数据库+中间件系列
    6. DevOps系列

    欢迎关注公众号:程序员欣宸

    微信搜索「程序员欣宸」,我是欣宸,期待与您一同畅游Java世界...
    https://github.com/zq2599/blog_demos

    相关文章

      网友评论

          本文标题:Spring Cloud Gateway过滤器精确控制异常返回(

          本文链接:https://www.haomeiwen.com/subject/qozjxrtx.html