【实战,完全定制返回body Spring Cloud Gateway过滤器精确控制异常返回】欢迎访问我的GitHub这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos
本篇概览
- Spring Cloud Gateway应用中 , 处理请求时若发生异常未被捕获 , 请求方收到的响应是系统默认的内容 , 无法满足实际业务需求
- 因此 , 从前一篇文章《Spring Cloud Gateway过滤器精确控制异常返回(分析篇)》开始 , 咱们深入分析了Spring Cloud Gateway的相关源码 , 了解到全局异常的处理细节 , 然后 , 通过前文《Spring Cloud Gateway过滤器精确控制异常返回(实战 , 控制http返回码和message字段)》的实战 , 咱们已经能随意设置http返回码 , 以及body中的message字段 , 也就是控制下图两个红框中的内容:

文章插图
- 正如上图所示 , 异常发生时系统固定返回8个字段 , 这就有些不够灵活了 , 在一些对格式和内容有严格要求的场景下 , 咱们需要能够完全控制返回码和返回body的内容 , 如下所示 , 只返回三个字段 , 每个字段都是完全为业务服务的:
{# 这是有具体业务含义的返回码"code": "010020003",# 这是能精确描述错误原因的文本信息"message": "请确保请求参数中的user-id字段是有效的",# 这是常规的业务数据 , 发生异常时该字段为空"data": null}- 今天咱们的目标就是通过编码定制异常发生时的返回信息 , 具体内容就是上述JSON数据:只有code、message、data三个字段
- 本篇实战中的完整源码可在GitHub下载到 , 地址和链接信息如下表所示(https://github.com/zq2599/blog_demos):
- 这个git项目中有多个文件夹 , 本篇的源码在spring-cloud-tutorials文件夹下 , 如下图红框所示:

文章插图
- spring-cloud-tutorials文件夹下有多个子工程 , 本篇的代码是gateway-change-body , 如下图红框所示:

文章插图
为何不用常规手段
- 提到全局异常处理 , 经验丰富的您应该想到了常用的ControllerAdvice和ExceptionHandler注解修饰的全局异常处理类 , 但是Spring Cloud Gateway是基于WebFlux的 , 咱们之前处理异常时用到的HttpServletRequest在Spring Cloud Gateway中并不适用 , 因此 , 不能用ControllerAdvice和ExceptionHandler的手段来处理全局异常
- 在动手前做好充足的理论分析 , 写出的代码才能正常工作
- 打开DefaultErrorWebExceptionHandler.java , 找到renderErrorResponse方法 , 来看看Spring Cloud Gateway原本是如何构造异常返回内容的:

文章插图
- 此刻聪明的您应该想到怎么做了:做个新的类继承DefaultErrorWebExceptionHandler , 覆盖其renderErrorResponse方法 , 新的renderErrorResponse方法中 , 按照实际业务需要来设置返回内容 , 没错 , 这就是咱们的思路 , 不过还要细化一下 , 最终具体的步骤如下:
- 新增一个异常类CustomizeInfoException.java , 该类有三个字段:http返回码、业务返回码、业务描述信息
- 在返回异常的代码位置 , 使用CustomizeInfoException类来抛出异常 , 按照实际业务场景设置CustomizeInfoException实例的各个字段
- 新增MyErrorWebExceptionHandler.java , 继承自DefaultErrorWebExceptionHandler , 重写了renderErrorResponse方法 , 这里面检查异常实例是否是CustomizeInfoException类型 , 如果是 , 就从其中取出http返回码、业务返回码、业务描述信息等字段 , 构造返回body的内容 , 异常实例若不是CustomizeInfoException类型 , 就保持之前的处理逻辑不变;
- 新增configuration类 , 用于将MyErrorWebExceptionHandler实例注册到spring环境
- 分析完毕 , 开始编码吧 , 为了简单起见 , 本篇不再新增maven子工程 , 而是基于前文创建的子工程gateway-change-body , 在这里面继续写代码;
- 新增异常类CustomizeInfoException.java:
package com.bolingcavalry.changebody.exception;import lombok.Data;import org.springframework.http.HttpStatus;@Datapublic class CustomizeInfoException extends Exception {/*** http返回码*/private HttpStatus httpStatus;/*** body中的code字段(业务返回码)*/private String code;/*** body中的message字段(业务返回信息)*/private String message;}- 修改RequestBodyRewrite.java的apply方法 , 这里面是在处理请求body , 如果检查到没有user-id字段 , 就不将请求转发到服务提供方provider-hello , 而是返回错误 , 这里的错误就用CustomizeInfoException类来处理:
@Overridepublic 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);// 这里按照业务需要自行设置codecustomizeInfoException.setCode("010020003");// 这里按照业务需要自行设置返回的messagecustomizeInfoException.setMessage("请确保请求参数中的user-id字段是有效的");return Mono.error(customizeInfoException);}// 取得idint userId = (Integer)map.get("user-id");// 得到nanme后写入mapmap.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 , 这里有一处需要重点关注的是:下面的代码仅是参考而已 , 您无需拘泥于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);}@Overrideprotected 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请求 , 地址是http://localhost:8081/hello/change , 如下图 , 红框2中的http返回码是咱们代码里设置的 , 红框3显示返回的内容就是咱们定制的那三个字段:

文章插图
- 至此 , 控制Spring Cloud Gateway应用异常返回的实战已经全部完成 , 从源码分析结合实战演练 , 希望欣宸的文章能陪伴您深入了解Spring Cloud Gateway , 打造出更加强大的网关应用;
- Java系列
- Spring系列
- Docker系列
- kubernetes系列
- 数据库+中间件系列
- DevOps系列
https://github.com/zq2599/blog_demos
- 春季老年人吃什么养肝?土豆、米饭换着吃
- 三八妇女节节日祝福分享 三八妇女节节日语录
- 老人谨慎!选好你的“第三只脚”
- 校方进行了深刻的反思 青岛一大学生坠亡校方整改校规
- 脸皮厚的人长寿!有这特征的老人最长寿
- 长寿秘诀:记住这10大妙招 100%增寿
- 春季老年人心血管病高发 3条保命要诀
- 眼睛花不花要看四十八 老年人怎样延缓老花眼
- 香槟然能防治老年痴呆症? 一天三杯它人到90不痴呆
- 老人手抖的原因 为什么老人手会抖
