处理后不再将异常传给上层。其中包括:
- catch 到异常并处理(打印日志、发通知等)后不再扔给上层
- 捕捉到异常后给上层返回 null 值
- …
第一小节的强制 5就属于这种case。
2.2 为何要手动回滚事务执行入口:
TransactionInterceptor#invoke
TransactionAspectSupport#invokeWithinTransaction:
@Nullable protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable { // If the transaction attribute is null, the method is non-transactional. TransactionAttributeSource tas = getTransactionAttributeSource(); final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null); final PlatformTransactionManager tm = determineTransactionManager(txAttr); final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) { // Standard transaction demarcation with getTransaction and commit/rollback calls. TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); Object retVal; try { // This is an around advice: Invoke the next interceptor in the chain. // This will normally result in a target object being invoked. retVal = invocation.proceedWithInvocation(); } catch (Throwable ex) { // target invocation exception completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); } commitTransactionAfterReturning(txInfo); return retVal; } ...
带 @Transaction 注解的事务函数中捕获到异常后,执行
TransactionAspectSupport#completeTransactionAfterThrowing:
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) { if (txInfo != null && txInfo.getTransactionStatus() != null) { if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) { try { txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); } catch (TransactionSystemException ex2) { ex2.initApplicationException(ex); throw ex2; } catch (RuntimeException | Error ex2) { throw ex2; } } else { // We don't roll back on this exception. // Will still roll back if TransactionStatus.isRollbackOnly() is true. try { txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); } catch (TransactionSystemException ex2) { ex2.initApplicationException(ex); throw ex2; } catch (RuntimeException | Error ex2) { throw ex2; } } } }
可见,若设置了事务属性,且当前异常满足 rollbackOn 指定异常(默认RuntimeException 类型及其子类&&Error及其子类),则会将当前事务回滚,否则提交。
因此如果 catch 异常后没有将异常抛出或不手动回滚,将会导致事务提交。
封装二方接口时,很多人会吞异常:
@Component public class DemoClient { @Resource private XXServcie xxServcie; public XXInfo someMethod(Long id) { try { return xxServcie.getXXInfo(id); } catch (Exception e) { log.warn("调用xx服务异常,参数:{}", id, e); return null; } } }
当调用异常时打印异常信息后直接返回 null。此时如果调用方直接拿到返回值对象而未做判空处理直接使用其属性,易 NPE。
吞二方接口异常,有些业务异常中包含的错误原因(如包含xxx敏感词汇、标题不能超过20个字等)无法传给上层再封装给前端,可能会造成出错后用户懵逼。
如用户输入某敏感词汇,调用二方接口时 “吞掉” 敏感词汇的业务异常提示(输入中包含 xx敏感词),用户通过技术支持咨询,开发人员要查日志才能知道具体的错误原因(若此处还没打印日志,连日志都没得查),很低效。
所以要根据具体业务场景慎重确定是否要吞异常。
3 循环中的异常处理问题 3.1 案例1
若不对循环代码进行捕捉,当循环出现异常,后续代码则无法执行。
但若在 for 循环外部捕捉异常,虽然for循环后如果有代码依然可执行,但list中的非最后一个元素作为参数调用 doSomeRemoteInvoke 出现异常,list 中后续的数据则无法继续执行。
try { for (String str : data) { // 远程方法调用(可能出现异常) String result = doSomeRemoteInvoke(str); System.out.println(result); } } catch (Exception e) { log.error("程序出错,参数data:{},错误详情", JSON.toJSONString(data), e); }
因此需对 for 循环代码内对可能出现的异常进行捕捉:
for (String str : data) { try { // 远程方法调用(可能出现异常) String result = doSomeRemoteInvoke(str); System.out.println(result); } catch (Exception e) { log.error("程序出错,参数data:{},错误详情", JSON.toJSONString(data), e); } }3.2 案例 2
思考:
- 分别调用两个函数 pirntList1 和 printList2,结果有何不同
- 哪个不需要捕捉异常也不会造成中间有一个出错后续处理中断
pirntList1:
for 循环在线程池代码外部,每次循环调用线程池去执行判断、打印。
此时依次传入a、ab、abc、abcd字符串;当执行到ab,抛IllegalArgumentException,此时线程池中的唯一的线程销毁;当执行到abc,再在线程池执行,线程池创建新的线程来执行,依然可正常执行。
pirntList2:
for 循环在 线程池 execute 参数的lambda表达式内,所有的循环执行都在同一线程。当执行到ab抛异常,导致整个线程销毁,无法继续执行。
为避免一个数据出错而导致后续代码都无法执行,若采用第二种方式来执行可以对代码做出如下修改:
在实际业务开发过程中,这种问题比较隐蔽,尤其是在异步线程中执行时,不加留意,易出现问题。
二方服务封装时,如捕捉异常,应打印查询参数和异常详情。 一般都不会吞异常,遇到吞异常场景要慎重思考合理性。 如第二部分的案例,若调用二方接口出现异常没有打印日志,将对排查问题造成困难。
4.2 受检异常、非受检异常Java 中的异常主要分为两类:受检异常和非受检异常。
根据 JLS 异常部分的描述
- 受检异常,主要指编译时强制检查的异常,包括非受检异常之外的其他 Throwable 的子类
- 非受检异常,主要指编译器免检异常,通常包括运行时异常类和Error相关类
Error和Exception都是Throwable子类:
-
RuntimeException和其子类都属运行时异常
-
Error 类和其子类都属于错误类
-
RuntimeException及其子类和 Error类及其子类属于非受检异常
-
此外的 其他 Throwable 子类属受检异常
开发中自定义的业务异常(BusinessException)属非受检异常,会定义为 RuntimeException 的子类。
有人将业务异常定义为受检异常,会导致底层抛出后上层调用每层都要被迫处理。
努力使失败保持原子性。《Effective Java》第 3 版 第 76条 努力使失败保持原子性[^2] 所提到的那样。
可在函数核心代码执行前对参数进行检查,对不满足的条件抛适当异常。
实际开发中通常可使用:
- com.google.common.base.Preconditions
- 或org.apache.commons.lang3.Validate第三方库提供的参数检查工具类来实现
如果 catch 住异常却没有进行编写任何处理代码,请在注释中给出充分的理由,避免其他人产生困惑,避免留坑。
参考 org.springframework.context.support.AbstractApplicationContext#close:
上面的源码捕捉到 IllegalStateException 异常以后没有处理,给出了处理方式和原因: 忽略此异常,因为虚拟机已经正在关闭。
5 总结本节主要讲异常的一些处理建议,包括是否要 “吞掉” 异常,循环中的异常处理,以及一些补充建议。希望大家可以重视异常,少趟坑。
参考
- 阿里巴巴Java 开发手册 华山版
- 《Effective Java 中文版 (原书第 3 版)》