异常处理在开源SpringBoot/SpringCloud微服务框架的最佳实践
前期内容导读:
- Java开源RSA/AES/SHA1/PGP/SM2/SM3/SM4加密算法介绍
- Java开源AES/SM4/3DES对称加密算法介绍及其实现
- Java开源AES/SM4/3DES对称加密算法的验证说明
- Java开源RSA/SM2非对称加密算法对比介绍
- Java开源RSA非对称加密算法实现
- Java开源SM2非对称加密算法实现
- Java开源接口微服务代码框架
- Json在开源SpringBoot/SpringCloud微服务框架中的最佳实践
- 加解密在开源SpringBoot/SpringCloud微服务框架的最佳实践
- 链路追踪在开源SpringBoot/SpringCloud微服务框架的最简实践
- OAuth2在开源SpringBoot/SpringCloud微服务框架的最佳实践
- 熔断降级与限流在开源SpringBoot/SpringCloud微服务框架的最佳实践
- 在Java开源接口微服务代码框架 (代码已全部开源
)基础上,站在过来人的角度,总结了分布式微服务中非常容易被忽略的异常处理逻辑,有助于更好地理解和开发微服务; - 本文不是简单讲解异常的分类,而是告诉你异常相关的场景、处理思路和具体实现,力求简洁优雅;
1. Java为什么要有异常
- 按照个人见解,可以从两个维度去解释:
- 从Java面向对象的设计理念来讲,设计的类主要是面向对象的,对象有特定的行为和属性,但是对象的行为和属性解释不了现实世界的真实活动。如:设计了一个汽车对象,有方向盘、轮子、发动机等属性,有前进、左转、右转、倒车等行为。当汽车对象发生前进动作时,可能因为车轮爆胎而左转了。这个左转行为跟前进动作就完全不匹配了。汽车对象正常的前进行为属于正常的常规行为,但是前进时突然发生的意外左转就属于异常行为。编码时,类似的例子比比皆是。
- 从Java虚拟机的设计理念来讲,程序执行的过程,就是做计数标记,然后不停地压栈和出栈的过程,异常就是程序段(方法)返回到执行前的行为,注意:栈内改变的数据并未回滚。如:执行一个复杂的加密方法时,突然发生了数组越界,则加密方法已经执行的部分就会全部出栈,这时程序就得不到加密的结果了。简单来说,虚拟机无法正常进行下去的行为都属于异常。这样的异常非常多,比如:内存溢出、内存不足、空指针、数组越界等。
- 在网上也看到一些关于异常产生的观点,和本人理解的维度有点不同,也建议大家看看:
- Java 为什么要引入异常处理机制?
- java为什么引入异常处理机制
2 Java异常分类
- 按照JDK的异常体系去分,根异常是Throwable(就好比所有类的父类Object),Throwable可以分为错误(Error)和异常(Exception),我们通常需要捕获Exception异常;
- 除了JDK异常外,还有大量三方件和用户自定义的Exception,这些Exception以运行时异常为主。自定义异常肯定是无法穷举的,只能大致分类。按照个人理解,无论是JDK异常还是自定义异常,均可分为模块化异常和服务化异常。模块化异常只是代码内部的异常,客户端无感知;服务化异常则是把最终的异常结果响应到客户端,影响客户体验。
上述多个维度的异常划分并不矛盾,要想做好微服务研发,就必须处理好服务化异常;而服务化异常内部有大量的模块化异常;模块化异常则是由JDK异常和自定义异常组成的。
3. JDK异常处理
- JDK的异常体系如图所示:
图片来源:java 异常分类和处理机制- 通常情况下,Error是我们无法掌控的,但是在特殊场景下,需要捕获Throwable(为了兼顾捕获Exception和Error,一般不直接捕获Error),主要是捕获异常的堆栈信息,之后还要继续往外抛异常。比如:虚拟机的内存溢出异常,不会因为你不往外抛就不崩溃。
- 我们需要非常关注Exception,包括非运行时异常和运行时异常(RuntimeException),而且是二者都要处理好。
- 自定义异常种类繁多,可以在后面的模块化和服务化异常中去附带说明;
4. 模块化异常处理
-
个人理解的模块化异常是指异常没有最终暴露给客户端,且是通过自己的业务逻辑代码响应给客户端的。
-
按照Java开源接口微服务代码框架 场景规划,模块化异常列表如下:
异常类型 所属分类 示例 备注 业务校验异常 运行时异常 1.用户名和密码不匹配异常;
2. …也可以用自定义异常 切面异常 自定义异常 1.渠道限流(Rest调用第三方时调用量超限)异常;
2. …也可以用自定义异常 逻辑不当异常 运行时异常 1.数组越界异常;
2. 加解密失败异常;
3. …也可以用自定义异常 依赖服务异常 非运行时异常 1.请求超时异常,如SocketTimeoutException;
2.服务拒绝异常;
3. …也可以用自定义异常 中间件异常 非运行时异常 1.数据库连不上导致查不到数据的异常;
2.磁盘无法写入异常;
3. …也可以用自定义异常 … … … … - 表格的模块化异常包含了非运行时异常和非运行时异常,所以一般为了兼顾,采取捕获Exception;
- 模块化异常基本上都可以用自定义的异常去替代;
-
模块化异常的核心问题是在做业务逻辑处理的时候,内部发生了异常,但最终会被外层的代码转换成正常的结果返回到客户端。如:在BaseRestService 发起远程调用时,会捕获渠道限流切面、网络超时、数据库异常等所有的Exception异常,并转换成正常的结果响应对象ResultCode,只是把错误码填充到结果对象了而已。
protected ResultCode<O> invokeResult(T model) { ResultCode<O> resultCode = ResultCode.error(ErrCodeEnum.SERVER_ERROR.getCode()); try { //此处被切面做了限流判定逻辑 resultCode = this.getRemoteService().invoke(model); } catch (CommonException e) { resultCode = ResultCode.error(e.getErrCode().getCode()); } catch (Exception e) { log.error("unknown error in channel.", e); } finally { resultCode.setReqId(model.getReqId()); resultCode.setCost(System.currentTimeMillis() - model.getStart()); } return resultCode; }
通过切面实现的渠道限流详细设计参见文档:熔断降级与限流在开源SpringBoot/SpringCloud微服务框架的最佳实践
-
在模块化异常中,还有一类非常特殊的异常,就是线程池异常。由于线程是异步执行的,通常无法通知到发起方。这时候就要记录好相应的异常堆栈信息,CommonThreadFactory 代码如下:
public class CommonThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { //获取主线程的链路信息 MDCAdapter mdc = MDC.getMDCAdapter(); Map<String, String> map = mdc.getCopyOfContextMap(); Thread t = new Thread(r, this.poolPrefix + "-thread-" + THREAD_ID.getAndIncrement()) { @Override public void run() { try { //把链路追踪设置到线程池中的线程 if (null != map) { MDC.getMDCAdapter().setContextMap(map); } super.run(); } finally { //使用完毕后,清理缓存,避免内存溢出 MDC.clear(); } } }; return t; } static { Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { LOGGER.error("Current rejected thread is {},error:{}", t.getName(), e); } }); } }
5. 服务化异常处理
-
服务化异常承载了一部分异常场景的业务逻辑,是业务响应的一部分。
-
按照Java开源接口微服务代码框架 场景规划,服务化异常列表如下:
异常类型 所在服务 示例 实现技术 参数校验异常 业务服务
1.用户名不存在异常;
2. …spring-boot-starter-validation改造 过滤器校验异常 网关服务
认证服务1.签名验证失败异常;
2. 用户认证失败异常;
3. …1.GlobalFilter扩展;
2.spring-authorization-server Filter扩展;熔断降级限流异常 网关服务
业务服务1.熔断降级异常;
2. 限流异常;自定义Sentinel异常扩展 客户限流异常 业务服务 1.客户A QPS超限异常;
2. …Redis实现 全局异常 网关服务
认证服务
业务服务
1.内部错误异常;
2. …webflux全局异常扩展;
spring-authorization-server Filter扩展+web全局异常扩展;
web全局异常异常扩展- 参数校验异常是每个微服务必备的,当前虽然从场景分析,仅需业务服务做校验,但是长远考虑,还是所有服务都得做。因此需要把校验框架加在公共代码模块,保证所有微服务随时可以添加上;
- 此处的客户限流和上一章节的渠道限流含义不同:客户限流表示限制客户端的调用;渠道限流表示限制我们对第三方服务的调用。在熔断降级与限流在开源SpringBoot/SpringCloud微服务框架的最佳实践 文档中有详细的设计与实现。
-
根据实现的技术不同,分为web服务化异常、spring-authorization-server服务化异常和webflux服务化异常。
5.1 web服务化异常处理
-
web服务化异常单独汇总如下:
所在服务 异常类型 示例 实现技术 直接响应 业务服务 参数校验异常 1.用户名不存在异常;
2. …spring-boot-starter-validation改造 ✖ 业务服务 熔断降级异常 1.熔断降级异常 自定义Sentinel异常扩展
✖业务服务 非功能限流异常 1. 限流异常 自定义Sentinel异常扩展
✖业务服务 客户限流异常 1.客户A QPS超限异常;
2. …Redis实现 ✔ 业务服务 全局异常 1.内部错误异常;
2. …web全局异常异常扩展 ✔ - 你可能觉得参数异常可以单独返回错误信息到客户端,比如:参数校验,完全可以在校验失败的注解中直接返回错误的message。但是如果你做过非常严格的海外业务(比如:国外重量级企业的项目),就会知道编译后的代码是不允许出现中文的;如果你做过多语言项目(如:跨境电商等),就会知道响应的错误Message的国际化整改非常痛苦;
- 熔断降级和非功能限流,如果只是针对Rest接口而言,都是可以在自定义的异常中返回的,但是考虑到使用@SentinelResource注解对其中的部分资源做熔断降级和限流时,则只能通过全局异常来做统一把控;
- 以上所有不能直接响应的服务化异常,都是通过全局异常来响应到客户端的;
5.1.1 web参数校验服务化异常最佳实践
5.1.1.1 web参数校验服务化异常分析
-
参数校验可以非常好的结合
spring-boot-starter-validation
框架,可以支持各种复杂的校验场景; -
参数校验不是我们的最终目的,参数校验完,我们还需要返回响应的异常信息到客户端;
-
做接口服务时,通常要求一个错误码一个对应的描述,比如:
{"code":"001","msg":"参数错误"}
。但是在接口的参数校验时,又希望一个错误码对应多个描述,比如:{"code":"001","msg":"用户名不能为空"}
、{"code":"001","msg":"用户名长度不符合要求"}
;基于上面的分析,设计思路如下:
- 使用
spring-boot-starter-validation
,并通过Spring框架的@ControllerAdvice
和@ExceptionHandler
注解组合,获取到未捕获到或者主动抛到web容器中的异常,并单独定义一个参数异常处理方法,对参数校验进行处理; - 给错误码设计多个字段:
code
表示错误码;msg
表示错误码对应的异常信息,如:参数错误
;detail
表示错误详情,对应msg的更详细信息,如:用户名不能为空
; -
spring-boot-starter-validation
校验的注解中,不能把msg
直接返回,而是要通过错误码对象的code
+detail
拼接的国际化key去定义,然后再从国际化错误码中去获取相应的显示内容;
- 使用
5.1.1.2 web参数校验服务化异常实现过程
- 引入校验框架的maven pom依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> <version>2.7.4</version> </dependency> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.2.5.Final</version> </dependency>
- 为了接收校验框架抛出到容器的参数异常,编写全局异常的处理器GlobalExceptionHandler:
@Slf4j @ControllerAdvice public class GlobalExceptionHandler extends BaseExceptionHandler { @ExceptionHandler({MethodArgumentNotValidException.class}) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseBody public ResultCode<?> handleValidErr(HttpServletRequest req, MethodArgumentNotValidException e) { List<ObjectError> errors = e.getBindingResult().getAllErrors(); log.error("current[{}] request happened err:{}", req.getRequestURI(), errors); return validHandler.handleValidErr(errors); } /** * 自动注入校验处理器 */ @Autowired private ValidHandler validHandler; }
- 其中专门通过
ValidHandler
参数校验器对异常结果进行处理,其实现会在后面介绍; - 本文所列的代码全部开源,参见文档开头介绍。如需获取完整源码仅需下载到本地即可,下同。
- 其中专门通过
- 为了把校验框架抛出的异常解析成错误码对象,定义了默认的参数校验器DefaultValidHandlerImpl:
@Slf4j public class DefaultValidHandlerImpl implements ValidHandler { @Override public ResultCode<?> handleValidErr(List<ObjectError> errors) { if (CollectionUtils.isEmpty(errors)) { log.error("failed to get valid error."); ErrCode errCode = ErrCodeMgr.getServerErr(); return ResultCode.error(errCode.getCode()); } else { for (ObjectError error : errors) { String code = error.getDefaultMessage(); log.error("get valid error:{}", code); if (!StringUtils.isEmpty(code) && code.contains(Const.LINK)) { return ResultCode.build(ErrCodeMgr.getValid(code)); } } return ResultCode.build(ErrCodeMgr.getServerErr()); } } }
- 定义一个国际化的错误码文件errcode_zh_CN.properties:
100001.MSG=通过 100002.MSG=签名验证失败 100003.MSG=流量超限 100004.MSG=认证失败 100005.MSG=参数错误 100098.MSG=内部错误 100099.MSG=未通过
- 国际化文件都是会有多种语言的,限于篇幅,只列了中文部分。下同。
- 定义一个国际化的参数校验错误码文件errcode_valid_zh_CN.properties:
100005_bq.tips.name.invalid-length=用户名长度非法 100005_bq.tips.name.not-empty=用户名长度不能为空
- 编写读取上述错误码信息的错误码管理器ErrCodeMgr:
public final class ErrCodeMgr { /** * 获取标准的错误码 * * @param code 错误码code * @param locale 语言 * @return 错误码对象 */ public static ErrCode get(String code, Locale locale) { if (StringUtils.isEmpty(code)) { LOGGER.error("no standard code[{}/{}].", code, locale); return getServerErr(); } String msgKey = code + CODE_SUFFIX; String msg = I18N.get(locale, msgKey); if (StringUtils.isEmpty(msg)) { LOGGER.error("not exist standard code[{}/{}].", code, locale); return getServerErr(); } return ErrCode.build(code, msg); } /** * 获取标准的错误码(带detail,适用于参数校验场景) * * @param code 错误码code * @param locale 语言 * @return 错误码对象 */ public static ErrCode getValid(String code, Locale locale) { if (StringUtils.isEmpty(code)) { LOGGER.error("no standard parameter code[{}/{}].", code, locale); return getServerErr(); } String[] codes = StringUtils.split(code, Const.LINK); String realCode = codes[0]; ErrCode errCode = get(realCode); if (null == errCode) { LOGGER.error("no standard code[{}/{}] in parameter config.", code, locale); return getServerErr(); } //只有当参数校验对应的标准错误码存在时,才添加detail if (code.contains(errCode.getCode())) { errCode.setDetail(I18N.getValid(code)); } return errCode; } }
- 错误码的映射关系和读取,设计和实现上稍微有点复杂,以后会写一篇文章单独介绍。
- 为了支持复杂场景,需要编写校验的组ValidGroup.java:
public interface ValidGroup extends Default { interface Add extends ValidGroup { } interface Get extends ValidGroup { } interface GetBatch extends ValidGroup { } interface Update extends ValidGroup { } interface Delete extends ValidGroup { } }
- 至此,基于
spring-boot-starter-validation
改造的校验框架就全部完成了。
5.1.1.3 web参数校验服务化异常验证
- Controller入参的校验注解有
@Validated
和@Valid
,一个是Spring校验框架提供,一个是Servlet标准注解,其实现是由hibernate-validator
完成,而hibernate-validator
也是Spring校验框架的核心模块; - 基于上述分析,编写测试RestController的入参对象UserInner,代码如下:
@Data public class UserInner { /** * 用户名 */ @NotNull(message = "100005_bq.tips.name.not-empty") @Length(min = 5, max = 9, message = "100005_bq.tips.name.invalid-length") @Length(min = 8, max = 15, message = "100005_bq.tips.name.invalid-length", groups = ValidGroup.Get.class) @JsonMaskAnn private String name; /** * 真实姓名 */ @Length(min = 5, max = 15, message = "100005_bq.tips.name.invalid-length", groups = ValidGroup.Get.class) @Length(min = 5, max = 20, message = "100005_bq.tips.name.invalid-length", groups = ValidGroup.Add.class) @JsonMaskAnn private String realName; }
- 编写测试Rest服务DemoUserController,分别对带分组和不带分组的情况进行验证:
@Slf4j @RestController public class DemoUserController { @PostMapping("/demo/user/valid-get") public ResultCode<UserOuter> validate1(@RequestBody @Validated UserInner user) { UserOuter outer = new UserOuter(); BeanUtils.copyProperties(user, outer); ResultCode<UserOuter> resultCode = ResultCode.ok(outer); return resultCode; } @PostMapping("/demo/user/valid-get2") public ResultCode<UserOuter> validate2(@RequestBody @Valid UserInner user) { UserOuter outer = new UserOuter(); BeanUtils.copyProperties(user, outer); ResultCode<UserOuter> resultCode = ResultCode.ok(outer); return resultCode; } @PostMapping("/demo/user/valid-group-get") public ResultCode<UserOuter> validateGroupGet(@RequestBody @Validated(ValidGroup.Get.class) UserInner user) { UserOuter outer = new UserOuter(); BeanUtils.copyProperties(user, outer); ResultCode<UserOuter> resultCode = ResultCode.ok(outer); return resultCode; } }
- 验证过程如下:
- 先验证不带校验组的情况,分别通过curl命名调用
@Validated
和@Valid
注解的请求,命名如下:curl --location 'http://localhost:9993/demo/user/valid-get' \ --header 'Content-Type: application/json' \ --data '{ "name": "hao" }' curl --location 'http://localhost:9993/demo/user/valid-get2' \ --header 'Content-Type: application/json' \ --data '{ "name": "hao" }'
- 其返回结果完全相同,json如下:
{ "code": "100005", "msg": "参数错误", "detail": "用户名长度非法", "cost": 0 }
- 进一步印证
@Validated
和@Valid
注解是等效的,但是基于Servlet标准(或者JavaEE标准)的@Valid
注解不支持分组校验,所以建议使用@Validated
,既能达到@Valid
的校验效果,还能扩展支持分组; - 从验证结果Json来看,错误code,错误message,错误detail全部都能显示了,这样就可以满足不同的参数异常展示效果了;
- 进一步印证
- 再来验证下不带分组和带分组的组合情况,分别构造如下请求报文:
curl --location 'http://localhost:9993/demo/user/valid-group-get' \ --header 'Content-Type: application/json' \ --data '{ "name": "hao1234567", "real_name":"name123" }' curl --location 'http://localhost:9993/demo/user/valid-group-get' \ --header 'Content-Type: application/json' \ --data '{ "name": "hao123456", "real_name":"name123456789123" }'
- 二者的响应结果均为:
{ "code": "100005", "msg": "参数错误", "detail": "用户名长度非法", "cost": 0 }
- 从请求1及结果分析可知:当带不带分组和带分组的校验规则同时存在时,二者同时生效,而且必须都满足才能校验通过,否则就校验不通过;
- 从请求2及结果分析可知:只有匹配的分组校验规则是生效的,其它分组的校验规则是无效的;
- 先验证不带校验组的情况,分别通过curl命名调用
- 前面也讲了校验失败时,返回错误code,错误message,错误detail会显得比较繁琐,实现的代码框架实际也可以支持扩展定制成只需要错误code,错误message,只不过错误message内存放的是错误detail。为了达成这个效果,仅需新增自定义的校验处理器ValidHandlerImpl即可,其它代码无须做任何改动。
@Slf4j @Component public class ValidHandlerImpl implements ValidHandler { @Override public ResultCode<?> handleValidErr(List<ObjectError> errors) { if (CollectionUtils.isEmpty(errors)) { log.error("failed to get valid error."); ErrCode errCode = ErrCodeMgr.getServerErr(); return ResultCode.error(errCode.getCode()); } else { for (ObjectError error : errors) { String message = error.getDefaultMessage(); log.error("get valid error:{}", message); if (!StringUtils.isEmpty(message) && message.contains(Const.LINK)) { ErrCode errCode = ErrCodeMgr.getValid(message); log.error("current valid error:{}", JsonUtil.toJson(errCode)); errCode.setMsg(errCode.getDetail()); errCode.setDetail(null); return ResultCode.build(errCode); } } return ResultCode.build(ErrCodeMgr.getServerErr()); } } }
注意:ValidHandlerImpl这个类也已提交并生效了,如果要达成前面示例的返回效果,需要注释掉这个类的代码。这个类的验证效果略。
5.1.2 web熔断降级和限流服务化异常最佳实践
5.1.2.1 web熔断降级和限流服务化异常实现
- 在熔断降级与限流在开源SpringBoot/SpringCloud微服务框架的最佳实践
中已经介绍了基于Sentinel的熔断降级和限流。Sentinel主要分为基于Rest请求和基于资源标签的熔断降级和限流2类。二者均需引入pom依赖:<!--sentinel熔断降级--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> <version>2021.0.5.0</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-sentinel-datasource</artifactId> <version>2021.0.5.0</version> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> <version>1.8.6</version> </dependency>
- Sentinel基于Rest请求的熔断降级和限流已经能够满足大部分场景了,建议优先考虑。Sentinel基于Rest请求的统一异常配置服务SentinelWebConfigurer定义如下:
@Slf4j @Configuration public class SentinelWebConfigurer { /** * 定义异常时的处理器(使用自定义的错误码) * * @return 熔断降级异常时的处理器 */ @Bean public BlockExceptionHandler blockSentinelHandler() { return (request, response, e) -> { log.error("limit block happened.", e); ResultCode<?> resultCode = ResultCode.error(ErrCodeEnum.LIMIT_ERROR.getCode()); ResponseUtil.writeErrorBody(response, JsonUtil.toJson(resultCode, snakeCase)); }; } /** * 是否驼峰式json(默认支持) */ @Value("${bq.json.snake-case:true}") private boolean snakeCase; }
5.1.2.2 web接口熔断降级和限流服务化异常验证
-
因为sentinel默认是懒加载,需要先请求下接口。先执行命令获取JwtToken:
curl --location --request POST 'http://localhost:9992/oauth/enc/token?scope=read&grant_type=client_credentials' \ --header 'bq-integrity: ef5b4373e5c24c2c0ccd6700a3e9b70f2b3a80268f9d4b1cc5bf8740e5dd81ca' \ --header 'bq-enc: app001' \ --header 'Authorization: 04d726fd8806b1c0763fead3a90592e592d8abb8290a6b638208c19977ae2d52e3553e507ad72f7e62b33a70fdaca4d8416ec091a59fe7eb8246745b5b6c88c15db580847186e895b29b47a569d2fc4e70e1e63e44aa529b396db710663d9a0c0c0f3a171885f74e0d8eec10469cb757d02b57a850547dc03bfa23'
-
获取到jwtToken后,替换到如下命令中的JwtToken后,执行命令发起对
/demo/jwk
接口的调用:curl --location 'http://localhost:9992/demo/jwk' \ --header 'Authorization: Bearer yyy...' \ --header 'Content-Type: application/json' \ --data '{ "code":"test123" }'
-
打开Sentinel控制面板就可以看到如下请求:
-
继续点击图注的流控按钮,添加限流规则:
-
多次执行上述命令,请求
/demo/jwk
接口,会看到如下返回结果:{ "code": "100003", "msg": "流量超限", "cost": 0 }
-
观测运行日志,发现也从我们自定义的handler中打印出了相关异常:
[bq-biz][Tid:2e534ec2f199a69b,Sid:c64a17f11048a522][ERROR][c.b.b.c.SentinelWebConfigurer_lambda$blockSentinelHandler$0] - limit block happened. com.alibaba.csp.sentinel.slots.block.flow.FlowException: null
总结:Sentinel对单个接口的限流已经演示完毕。但是Sentinel熔断降级和限流并不仅限于接口,还可以对里面的部分资源进行熔断降级和限流。比如接口中,有查询数据库/请求其他服务时,可以在其Service方法上添加
@SentinelResource
注解。 -
接口的熔断降级实现过程和上述的限流逻辑相比,除了配置规则不一样,其它完全一致,验证过程略。
5.1.2.3 web资源注解熔断降级和限流服务化异常验证
- Sentinel中可以使用
@SentinelResource
注解对资源做限流(参见文档 )。简单总结下:@SentinelResource
使用场景比较苛刻,要求方法的参数名和接口的入参一致,没法做全局统一的异常管控,也就是每个限流的@SentinelResource
资源都得写个异常处理逻辑,非常不优雅。代码如下:@Slf4j @RestController public class QrCodeController extends BaseBizController<QrCodeResult, QrCode, QrCodeInner> { @SentinelResource(value = "demo_jwk", blockHandler = "blockHandler") @PostMapping("/demo/jwk") @Override public ResultCode<QrCodeResult> execute(@RequestBody QrCodeInner inner) { log.info("current inner:{}", JsonUtil.toJson(inner)); return restService.execute(inner.toModel()); } protected ResultCode<QrCodeResult> blockHandler(QrCodeInner inner, BlockException e) { log.error("current inner:{},block exception.", JsonUtil.toJson(inner), e); return ResultCode.error(ErrCodeEnum.LIMIT_ERROR.getCode()); } /** * 注入自定义的Rest服务 */ @Resource(name = DemoConst.DEMO_REST_SERVICE) private RestService<QrCodeResult, QrCode> restService; }
- 于是想到干脆直接在Spring的全局异常处理器
GlobalExceptionHandler
中,新增熔断降级与限流的中断异常BlockException处理逻辑:@Slf4j @ControllerAdvice public class GlobalExceptionHandler extends BaseExceptionHandler { @ExceptionHandler({CommonException.class, NoHandlerFoundException.class, Exception.class}) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseBody public ResultCode<?> handleErr(HttpServletRequest req, Exception e) { Throwable ex = e; if (e instanceof UndeclaredThrowableException) { UndeclaredThrowableException realEx = (UndeclaredThrowableException)e; ex = realEx.getUndeclaredThrowable(); } if (ex instanceof BlockException) { log.error("sentinel block happened.", ex); return ResultCode.error(ErrCodeEnum.LIMIT_ERROR.getCode()); } return handle(req.getRequestURI(), e); } }
- 配置资源相关的限流见下图:
- 多次执行上述命令,请求
/demo/jwk
接口,会看到如下返回结果:{ "code": "100003", "msg": "流量超限", "cost": 0 }
- 观测运行日志,发现也从我们自定义的handler中打印出了相关异常:
[ERROR][c.b.b.w.d.QrCodeController_blockHandler] - current inner:{"code":"test123"},block exception. com.alibaba.csp.sentinel.slots.block.flow.FlowException: null
- 资源的熔断降级实现过程和上述的限流逻辑相比,除了配置规则不一样,其它完全一致,验证过程略。
5.1.3 web客户限流服务化异常最佳实践
- 在熔断降级与限流在开源SpringBoot/SpringCloud微服务框架的最佳实践
中已经介绍了基于Redis的客户限流。现在再简单阐述下与异常相关的实践过程。 - 在SpringMvc中定义一个HandlerInterceptor切入点,代码如下:
@Component(BootConst.CLIENT_LIMIT_SVC) public class ClientLimitHandler implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { JwtToken token = JwtUtil.getJwtToken(request.getHeader(HttpHeaders.AUTHORIZATION)); if (null == token || StringUtils.isEmpty(token.toClientId())) { //没有token,不做限流控制 return true; } Map<String, String> urls = MapUtils.invertMap(assemblyConfService.getClientUrl()); String urlId = urls.get(UrlUtil.shortUrl(request.getRequestURI())); if (StringUtils.isEmpty(urlId)) { //没有配置限流 return true; } AccessLimit model = new AccessLimit(); model.setUrlId(urlId); model.setAccessId(token.toClientId()); model.setConfig(LimitConfig.clientConf()); boolean needLimit = limitHandler.limit(model); if (needLimit) { ResultCode<?> resultCode = ResultCode.error(ErrCodeEnum.LIMIT_ERROR.getCode()); ResponseUtil.writeErrorBody(response, JsonUtil.toJson(resultCode, snakeCase)); return false; } return true; } }
- 当超过redis的限流阈值时,直接通过切入点的response对象响应到客户侧(限流异常被limitHandler限流器处理true和false了)。验证结果参见熔断降级与限流在开源SpringBoot/SpringCloud微服务框架的最佳实践
4.3.1.2 业务微服务的客户限流验证
章节。
5.1.4 web全局异常服务化最佳实践
- 前面讲了接口微服务框架涉及返回到客户端的各种场景,并且分别做了编码设计和验证。但是,还是有可能会漏掉一些异常场景。所有有必要在SpringWeb(也可以叫SpringMVC)的全局异常GlobalExceptionHandler中捕获Exception,代码如下:
@Slf4j @ControllerAdvice public class GlobalExceptionHandler extends BaseExceptionHandler { @ExceptionHandler({CommonException.class, NoHandlerFoundException.class, Exception.class}) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseBody public ResultCode<?> handleErr(HttpServletRequest req, Exception e) { Throwable ex = e; if (e instanceof UndeclaredThrowableException) { UndeclaredThrowableException realEx = (UndeclaredThrowableException)e; ex = realEx.getUndeclaredThrowable(); } if (ex instanceof BlockException) { log.error("sentinel block happened.", ex); return ResultCode.error(ErrCodeEnum.LIMIT_ERROR.getCode()); } return handle(req.getRequestURI(), e); } }
作为基础框架代码,此处还可以优化成:只在项目中存在Sentinel时,才捕获并处理Sentinel异常。本人计划在下个版本就完成优化。
- 我们还要在业务服务的logback-spring.xml中配置全局异常打印到console/default和error日志文件中去。配置如下:
<?xml version="1.0" encoding="UTF-8"?> <!--日志级别以及优先级排序: FATAL > ERROR > WARN > INFO > DEBUG--> <configuration debug="false"> <!--控制台日志--> <appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> </appender> <!--default日志 --> <appender name="defaultAppender" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/default.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_PATH}/%d{yy-MM-dd}/default-%d{yy-MM-dd}.log</FileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> </appender> <!--error日志 --> <appender name="errorAppender" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/error.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_PATH}/%d{yy-MM-dd}/error-%d{yy-MM-dd}.log</FileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <!--全局异常日志--> <logger name="com.biuqu.boot.handler.GlobalExceptionHandler" additivity="false"> <appender-ref ref="errorAppender"/> <appender-ref ref="consoleAppender"/> <appender-ref ref="defaultAppender"/> </logger> <!--建立一个默认的root的logger --> <root level="${LOG_LEVEL}"> <appender-ref ref="consoleAppender"/> <appender-ref ref="defaultAppender"/> </root> </configuration>
5.2 spring-authorization-server服务化异常
-
spring-authorization-server服务化异常单独汇总如下:
所在服务 异常类型 示例 实现技术 直接响应 认证服务 过滤器校验异常 1. 用户认证失败异常; spring-authorization-server Filter扩展 ✔ 认证服务 全局异常 1.内部错误异常;
2. …spring-authorization-server Filter扩展+web全局异常扩展 ✔ - SpringMVC全局异常所在的扩展点本质上也是挂在某个外层的过滤器上,所以二者均可以直接响应到客户端;
-
在本文
5.1.1 web参数校验服务化异常最佳实践
章节的基础上,可知spring-authorization-server
也是建立在SpringMvc的基础上,所以共用了GlobalExceptionHandler
全局异常,即:未捕获的异常都会被GlobalExceptionHandler
处理。 -
参考OAuth2在开源SpringBoot/SpringCloud微服务框架的最佳实践 文章的介绍,
spring-authorization-server
本身就是由很多的过滤器来实现的。很多异常会被spring-authorization-server
的过滤器给处理掉,导致返回到客户端的异常不统一,需要通过过滤器的配置点来扩展。 -
定义认证失败时的处理器JwtAuthFailureHandlerImpl:
@Slf4j @Component public class JwtAuthFailureHandlerImpl extends BaseJwtExceptionHandler<AuthenticationException> implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) { handle(resp, e); } @Override protected void log(AuthenticationException exception) { OAuth2Error error = ((OAuth2AuthenticationException)exception).getError(); log.error("jwt auth failed:{}", JsonUtil.toJson(error)); super.log(exception); } }
-
定义认证失败且抛到Web容器的异常处理器:
@Component public class JwtExceptionHandlerImpl extends BaseJwtExceptionHandler<AccessDeniedException> implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) { handle(response, exception); } } @Slf4j public abstract class BaseJwtExceptionHandler<T extends RuntimeException> { /** * 认证失败的异常处理 * * @param response 响应对象 * @param exception 异常对象 */ public void handle(HttpServletResponse response, T exception) { try { log(exception); response.setStatus(HttpStatus.OK.value()); MediaType mediaType = new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8); response.setHeader(HttpHeaders.CONTENT_TYPE, mediaType.toString()); ResultCode<?> fail = ResultCode.error(ErrCodeEnum.AUTH_ERROR.getCode()); String json = JsonUtil.toJson(fail, snakeCase); response.getWriter().write(json); response.getWriter().flush(); } catch (Exception e) { log.error("auth failed with unknown exception.", exception); } } /** * 认证失败的日志处理 * * @param exception 失败的异常 */ protected void log(T exception) { log.error("auth failed.", exception); } /** * 是否驼峰式json(默认支持) */ @Value("${bq.json.snake-case:true}") private boolean snakeCase; }
-
再通过
spring-authorization-server
框架自定义的过滤器配置服务ServerConfigurer分别注入认证失败和全局异常:@Slf4j @EnableWebSecurity @Configuration(proxyBeanMethods = false) public class ServerConfigurer { @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain serverChain(HttpSecurity http, AuthenticationManager authManager, JWKSource<SecurityContext> jwkSource, JwtGenerator jwtGen) throws Exception { //1.前后端分离,禁用会话管理和csrf(跨站攻击) http.sessionManagement().disable(); http.csrf().disable(); //2.添加匿名访问的url(应该包括jwk) Set<String> anonymous = Sets.newHashSet(); if (!CollectionUtils.isEmpty(ignoreUrls)) { anonymous.addAll(ignoreUrls); } String[] anonUrls = anonymous.toArray(new String[] {}); http.authorizeRequests(registry -> registry.antMatchers(anonUrls).permitAll().anyRequest().authenticated()); //3.设置服务端配置(指定jwt生成器等) OAuth2AuthorizationServerConfigurer<HttpSecurity> serverConf = new OAuth2AuthorizationServerConfigurer<>(); http.apply(serverConf); serverConf.tokenGenerator(jwtGen); //设置认证信息匹配失败的异常 serverConf.clientAuthentication(clientConf -> clientConf.errorResponseHandler(this.failureHandler)); //设置token生成失败的异常 serverConf.tokenEndpoint(tokenConf -> tokenConf.errorResponseHandler(this.failureHandler)); //设置全局处理异常 http.exceptionHandling(exceptionHandler -> exceptionHandler.accessDeniedHandler(this.exceptionHandler)); //4.设置业务请求的jwt解析配置 http.oauth2ResourceServer(resourceConf -> { resourceConf.bearerTokenResolver(new DefaultBearerTokenResolver()); OAuth2ResourceServerConfigurer<HttpSecurity>.JwtConfigurer resourceJwtConf = resourceConf.jwt(); resourceJwtConf.decoder(OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource)); //设置资源解析失败的异常(主要是资源带的token解析/认证失败) resourceConf.accessDeniedHandler(this.exceptionHandler); }); return filterMgr.custom(http, authManager); } /** * 定制的过滤器管理器 */ @Autowired private SecurityFilterMgr filterMgr; /** * 鉴权拒绝(出现了非失败的异常)时的异常处理(参见{@link JwtExceptionHandlerImpl}实现) */ @Autowired private AccessDeniedHandler exceptionHandler; /** * 鉴权不通过的异常处理(参见{@link JwtAuthFailureHandlerImpl}实现) */ @Autowired private AuthenticationFailureHandler failureHandler; }
-
基于
spring-authorization-server
框架的微服务只要调用生成Token的接口/oauth/enc/token
或/oauth/token
接口验证即可。验证仅需要通过模拟错误的密码、过期或者非法的刷新JwtToken来调用,过程略。
5.3 webflux服务化异常
-
webflux服务化异常单独汇总如下:
所在服务 异常类型 示例 实现技术 直接响应 网关服务 过滤器校验异常 1.签名验证失败异常 GlobalFilter扩展 ✔ 网关服务 熔断降级限流异常 1.熔断降级异常 自定义Sentinel异常扩展 ✔ 网关服务 全局异常 1.内部错误异常;
2. …webflux全局异常扩展; ✔ -
本微服务框架主要用到SpringCloud-Gateway框架,而SpringCloud-Gateway是基于webflux的。网关服务仅需要关注公共的加解密、报文签名校验和JwtToken校验,全是在过滤器里面完成的。
-
在网关中引入Sentinel pom依赖:
<!--sentinel熔断降级--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId> <version>2021.0.5.0</version> </dependency>
5.3.1 webflux过滤器校验异常最佳实践
-
网关过滤器校验主要是做加解密、报文完整性和JwtToken校验。以完整性过滤器校验为例,进行说明:
@Slf4j @Component public class IntegrityCheckGatewayFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //1.解析出该请求的摘要配置和加密配置 ServerHttpRequest request = exchange.getRequest(); String url = request.getURI().getPath(); boolean signed = checkConf.needSign(url); PathMatcher pathMatcher = new AntPathMatcher(); boolean ignore = this.whitelist.stream().anyMatch(pattern -> pathMatcher.match(pattern, url)); //2.没有摘要或者在白名单里面的请求则直接放过请求 if (!signed || ignore) { return chain.filter(exchange); } //3.做完整性校验(使用加密器门面的默认摘要算法) String body = exchange.getAttribute(GatewayConst.BODY_CACHE_KEY); boolean result = checkIntegrity(request, body); if (!result) { log.error("[{}]check integrity failed.", url); return ServerUtil.writeErr(exchange, ErrCodeEnum.SIGNATURE_ERROR.getCode(), snakeCase); } log.info("[{}]check integrity successfully.", url); return chain.filter(exchange); } /** * 使用本地秘钥的默认加密器做摘要验证 * <p> * 拼接header认证头和body: `${Authorization}|${body}`,字段不存在或者为空时,使用空串代替 * * @param request 请求对象 * @param body 缓存的body * @return true表示检验通过 */ private boolean checkIntegrity(ServerHttpRequest request, String body) { String sign = request.getHeaders().getFirst(GatewayConst.HEADER_INTEGRITY); if (StringUtils.isEmpty(sign)) { return false; } String auth = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); if (StringUtils.isEmpty(auth)) { auth = StringUtils.EMPTY; } StringBuilder builder = new StringBuilder(); builder.append(auth); String encId = request.getHeaders().getFirst(GatewayConst.HEADER_ENC_ID); if (StringUtils.isEmpty(encId)) { encId = StringUtils.EMPTY; } builder.append(Const.JOIN).append(encId); if (StringUtils.isEmpty(body)) { body = StringUtils.EMPTY; } builder.append(Const.JOIN).append(body); String integrity = this.securityFacade.hash(builder.toString()); log.info("current signature:{},src:{}", integrity, sign); return sign.equals(integrity); } @Override public int getOrder() { return -40; } /** * 不用做鉴权的白名单 */ @Resource(name = GatewayConst.WHITELIST) private Set<String> whitelist; /** * 注入安全服务服务 */ @Autowired private SecurityFacade securityFacade; /** * 注入校验规则 */ @Autowired private IntegrityCheckConfig checkConf; /** * 是否驼峰式json(默认支持) */ @Value("${bq.json.snake-case:true}") private boolean snakeCase; }
-
在校验失败时,会通过封装的ServerUtil工具类直接响应到客户端:
@Slf4j public final class ServerUtil { /** * 回写异常结果 * * @param exchange server对象(包含request和response) * @param json 带错误码的resultCode json * @return 标准的异常结果对象 */ public static Mono<Void> writeErr(ServerWebExchange exchange, String json) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); MediaType utf8Type = new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8); response.getHeaders().add(HttpHeaders.CONTENT_TYPE, utf8Type.toString()); DataBuffer dataBuffer = response.bufferFactory().wrap(json.getBytes(StandardCharsets.UTF_8)); return response.writeWith(Flux.just(dataBuffer)); } /** * 回写异常结果 * * @param exchange server对象(包含request和response) * @param code 错误码 * @param snake 驼峰转换 * @return 标准的异常结果对象 */ public static Mono<Void> writeErr(ServerWebExchange exchange, String code, boolean snake) { ResultCode<?> resultCode = ResultCode.error(code); long start = Long.parseLong(exchange.getAttribute(GatewayConst.START_CACHE_KEY).toString()); resultCode.setCost(System.currentTimeMillis() - start); String json = JsonUtil.toJson(resultCode, snake); return writeErr(exchange, json); } }
-
Jwt OAuth2 Basic认证加密过滤器、报文加解密过滤器、JwtToken校验过滤器的详细编码逻辑参见文档加解密在开源SpringBoot/SpringCloud微服务框架的最佳实践 。文章来源:https://www.toymoban.com/news/detail-529008.html
-
过滤器的验证过程亦参见加解密在开源SpringBoot/SpringCloud微服务框架的最佳实践 ,此处略。文章来源地址https://www.toymoban.com/news/detail-529008.html
5.3.2 webflux全局异常最佳实践
- 基于Webflux的网关配置全局异常的方式与SpringWeb完全不同,Webflux框架的全局异常扩展点代码ErrorFluxAutoConfigurer如下:
@AutoConfiguration(before = WebFluxAutoConfiguration.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) @ConditionalOnClass(WebFluxConfigurer.class) @EnableConfigurationProperties({ServerProperties.class, WebProperties.class}) public class ErrorFluxAutoConfigurer { private final ServerProperties serverProperties; public ErrorFluxAutoConfigurer(ServerProperties serverProperties) { this.serverProperties = serverProperties; } @Bean @ConditionalOnMissingBean(value = ErrorWebExceptionHandler.class, search = SearchStrategy.CURRENT) @Order(-1) public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errAttr, WebProperties webProperties, ObjectProvider<ViewResolver> viewResolvers, ServerCodecConfigurer serverCodecConfigurer, ApplicationContext context) { WebProperties.Resources resources = webProperties.getResources(); ErrorProperties errProperties = this.serverProperties.getError(); DefaultErrorWebExceptionHandler errHandler = new GlobalExceptionHandler(errAttr, resources, errProperties, context); errHandler.setViewResolvers(viewResolvers.orderedStream().collect(Collectors.toList())); errHandler.setMessageWriters(serverCodecConfigurer.getWriters()); errHandler.setMessageReaders(serverCodecConfigurer.getReaders()); return errHandler; } @Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(); } }
- 其中,我们自定义的全局异常GlobalExceptionHandler代码如下:
@Slf4j public class GlobalExceptionHandler extends DefaultErrorWebExceptionHandler { /** * Create a new {@code DefaultErrorWebExceptionHandler} instance. * * @param errorAttributes the error attributes * @param resources the resources configuration properties * @param errorProperties the error configuration properties * @param applicationContext the current application context * @since 2.4.0 */ public GlobalExceptionHandler(ErrorAttributes errorAttributes, WebProperties.Resources resources, ErrorProperties errorProperties, ApplicationContext applicationContext) { super(errorAttributes, resources, errorProperties, applicationContext); } /** * 覆盖默认的异常处理类别(屏蔽掉默认的响应值) * * @param errorAttributes 异常属性 * @return 路由异常的处理函数 */ @Override protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) { return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse); } @Override protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) { Map<String, Object> error = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL)); Throwable e = getError(request); log.error("happened exception by global catching:{}.", e.getMessage()); String code = ErrCodeEnum.SERVER_ERROR.getCode(); if (e instanceof CommonException) { code = ((CommonException)e).getErrCode().getCode(); } ResultCode<?> resultCode = ResultCode.error(code); int httpCode = getHttpStatus(error); if (httpCode == HttpStatus.OK.value()) { httpCode = HttpStatus.INTERNAL_SERVER_ERROR.value(); } else if (ErrCodeEnum.SIGNATURE_ERROR.getCode().equals(code) || ErrCodeEnum.AUTH_ERROR.getCode().equals(code)) { httpCode = HttpStatus.UNAUTHORIZED.value(); } ServerResponse.BodyBuilder bodyBuilder = ServerResponse.status(httpCode); bodyBuilder.contentType(MediaType.APPLICATION_JSON); return bodyBuilder.bodyValue(resultCode); } }
- 我们还要在网关的logback-spring.xml中配置全局异常打印到console/default和error日志文件中去。配置如下:
<?xml version="1.0" encoding="UTF-8"?> <!--日志级别以及优先级排序: FATAL > ERROR > WARN > INFO > DEBUG--> <configuration debug="false"> <springProperty scope="context" name="LOG_SERVICE" source="spring.application.name" defaultValue="bq-service"/> <!--控制台日志--> <appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> </appender> <!--default日志 --> <appender name="defaultAppender" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/default.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_PATH}/%d{yy-MM-dd}/default-%d{yy-MM-dd}.log</FileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> </appender> <appender name="asyncNettyLog" class="ch.qos.logback.classic.AsyncAppender"> <appender-ref ref="consoleAppender"/> <appender-ref ref="defaultAppender"/> </appender> <!--error日志 --> <appender name="errorAppender" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/error.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_PATH}/%d{yy-MM-dd}/error-%d{yy-MM-dd}.log</FileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <!--全局异常日志--> <logger name="com.biuqu.boot.service.gateway.handler.GlobalExceptionHandler" additivity="false"> <appender-ref ref="defaultAppender"/> <appender-ref ref="consoleAppender"/> <appender-ref ref="errorAppender"/> </logger> </configuration>
6. 微服务异常处理小结
- 本文从Java异常分类开始讲起,在分类的基础上,又按照场景维度对异常进行拆分,以便对异常有更深刻的认识;
- 在了解异常的基础上,又从开源微服务框架的技术栈构成讲起,分别讲述了SpringWeb/Spring-Authorization-Server/SpringWebFlux底座下的异常场景及处理思路,目标是模块化异常(服务的内部异常)尽量捕获归总到服务的最外层(如:过滤器/RestController),并通过Exception异常来实现,这样可以确保异常的统一;
- 但是并不是在所有的过滤器/RestController都捕获了Exception异常就能保证返回到客户端的异常是统一的,还必须得通过Spring框架的基础底座捕获全局未知异常。因为有些异常可能是异步的,比如说线程池中的线程异常;
- 开源微服务框架基本上涉及到了SpringCloud的大部分技术栈,通过对SpringBoot参数校验异常、Sentinel熔断降级和限流异常(基于SpringBoot/SpringCloud-Gateway
2种场景,3种实现方式)、Redis限流异常、Spring-Authorization-Server过滤器认证异常、SpringCloud-Gateway过滤器校验异常,以及SpringBoot全局异常、Spring-Authorization-Server全局异常、SpringCloud-Gateway全局异常的分析和最佳实践,也可以帮助大家站在更高的位置,更好更全面的思考问题。
7. 参考资料
- [1]java 异常分类和处理机制
- [2]SpringBoot参数校验Validator框架详解
到了这里,关于异常处理在开源SpringBoot/SpringCloud微服务框架的最佳实践的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!