Java 业务开发常见错误 100 例
朱晔
贝壳金服资深架构师
52944 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 48 讲
代码篇 (23讲)
Java 业务开发常见错误 100 例
15
15
1.0x
00:00/00:00
登录|注册

12 | 异常处理:别让自己在出问题的时候变为瞎子

通过拿到的Future调用其get方法来获得任务运行结果和可能出现的异常
确保任务不出异常
设置自定义的异常处理程序
以execute方法提交到线程池的异步任务
通过不同的方法把每一种异常都new出来抛出
把try中的异常作为主异常抛出
finally代码块自己负责异常捕获和处理
Controller层
Service层
Repository层
修复方式
任务本身出现异常
任务提交到线程池处理
修复方式
异常定义为静态变量的坑
修复方式
finally代码块中的异常
抛出异常时不指定任何消息
两不太合适的异常处理方式
捕获异常后直接生吞
不应该生吞异常
不建议在框架层面进行异常的自动、统一处理
应用于三层架构
不在业务代码层面考虑异常处理
提交线程池的任务出了异常
把异常定义为静态变量
finally中的异常
丢弃异常的原始信息
生吞异常
统一异常处理方式
提交线程池的任务出了异常会怎么样?
千万别把异常定义为静态变量
小心finally中的异常
异常处理容易犯的错
异常处理

该思维导图由 AI 生成,仅供参考

你好,我是朱晔。今天,我来和你聊聊异常处理容易踩的坑。
应用程序避免不了出异常,捕获和处理异常是考验编程功力的一个精细活。一些业务项目中,我曾看到开发同学在开发业务逻辑时不考虑任何异常处理,项目接近完成时再采用“流水线”的方式进行异常处理,也就是统一为所有方法打上 try…catch…捕获所有异常记录日志,有些技巧的同学可能会使用 AOP 来进行类似的“统一异常处理”。
其实,这种处理异常的方式非常不可取。那么今天,我就和你分享下不可取的原因、与异常处理相关的坑和最佳实践。

捕获和处理异常容易犯的错

“统一异常处理”方式正是我要说的第一个错:不在业务代码层面考虑异常处理,仅在框架层面粗犷捕获和处理异常
为了理解错在何处,我们先来看看大多数业务应用都采用的三层架构:
Controller 层负责信息收集、参数校验、转换服务层处理的数据适配前端,轻业务逻辑;
Service 层负责核心业务逻辑,包括各种外部服务调用、访问数据库、缓存处理、消息处理等;
Repository 层负责数据访问实现,一般没有业务逻辑。
每层架构的工作性质不同,且从业务性质上异常可能分为业务异常和系统异常两大类,这就决定了很难进行统一的异常处理。我们从底向上看一下三层架构:
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

异常处理在软件开发中至关重要。本文介绍了在异常处理过程中容易犯的一些错误以及最佳实践。作者指出了一些常见的错误,比如仅在框架层面粗犷捕获和处理异常、捕获了异常后直接生吞以及丢弃异常的原始信息等。针对这些错误,作者提出了一些最佳实践,比如不建议在框架层面进行异常的自动、统一处理,捕获异常后应该进行合理的处理而不是直接生吞,以及在抛出异常时应该指定明确的异常消息等。文章通过具体的代码示例和分析,帮助读者更好地理解异常处理的重要性以及如何避免常见的错误。异常处理是软件开发中不可或缺的一部分,通过本文的总结,读者可以快速了解异常处理的关键要点,提高自己在异常处理方面的技能水平。文章还提到了一些具体的技术细节,如在finally代码块中的异常处理、避免把异常定义为静态变量等。这些内容对于读者快速了解异常处理的关键技术点具有很大帮助。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 业务开发常见错误 100 例》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(36)

  • 最新
  • 精选
  • Darren
    这篇文章收获很大,因为我们现在的系统就是用的统一异常处理,使用的就是老师提到的兜底异常,就是简单的分为业务异常和非业务异常,提示语不同而已。 试着回答下问题: 第一个问题: 肯定是以finally语句块为准。 原因:首先需要明白的是在编译生成的字节码中,每个方法都附带一个异常表。异常表中的每一个条目代表一个异常处理器,并且由 from 指针、to 指针、target 指针以及所捕获的异常类型构成。这些指针的值是字节码索引(bytecode index,bci),用以定位字节码。其中,from 指针和 to 指针标示了该异常处理器所监控的范围,例如 try 代码块所覆盖的范围。target 指针则指向异常处理器的起始位置,例如 catch 代码块的起始位置; 当程序触发异常时,Java 虚拟机会从上至下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异常表条目的监控范围内,Java 虚拟机会判断所抛出的异常和该条目想要捕获的异常是否匹配。如果匹配,Java 虚拟机会将控制流转移至该条目 target 指针指向的字节码。如果遍历完所有异常表条目,Java 虚拟机仍未匹配到异常处理器,那么它会弹出当前方法对应的 Java 栈帧,并且在调用者(caller)中重复上述操作。在最坏情况下,Java 虚拟机需要遍历当前线程 Java 栈上所有方法的异常表。所以异常操作是一个非常耗费性能的操作; finally 代码块的原理是复制 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中。所以不管是是正常还是异常执行,finally都是最后执行的,所以肯定是finally语句块中为准。 第二个问题: IllegalArgumentException:不合法的参数异常,比如说限制不能为空或者有指定的发小范围,调用方没有按照规定传递参数,就可以抛出这个异常; IllegalStateException:如果有状态流转的概念在里面(比如状态机),状态只能从A->B->C,若状态直接从A->C,就可以抛出该异常; UnsupportedOperationException:不支持该操作异常,比如非抽象父类中有个方法,子类必须实现,父类中的方法就可以抛出次异常。老师在集合坑中提到的Arrays.asList 返回的 List 并不是我们期望的 java.util.ArrayList,而是 Arrays 的内部类 ArrayList。ArrayList 内部类继承自 AbstractList 类,并没有覆写父类的 add 方法,而父类中 add 方法的实现,就是抛出 UnsupportedOperationException。

    作者回复: 👍🏻,你总结了比较重要的两点,JVM采用异常表控制try-catch的跳转逻辑;对于finally中的代码块其实是复制到try和catch中的return和throw之前的方式来处理的。

    2020-04-05
    16
    104
  • 扎紧绷带
    老师,为了便于定位异常请求bug,我希望url请求后端出现异常后,框架拦截打印出请求的参数信息,因为body里的参数在流中,只能消费一次,想要在出错后打印,必须要提前复制出一份保存。老师有什么好的实践吗?

    作者回复: 这是一个很常见的需求,使用HttpServletRequestWrapper即可,参考 https://github.com/JosephZhu1983/appinfocenter/blob/master/client/src/main/java/me/josephzhu/appinfocenter/client/DumpFilter.java

    2020-07-11
    25
  • 梦倚栏杆
    遇到一个坑(也可以说不理解),和该篇文章没关系,反馈一下 mysql 占位符问题 prepare sqltpl from 'select id,name from table1 where id in (?)'; set @a='1,2,3,4,5,6,7,8,9'; execute sqltpl using @a; 结果:只输出第一条符合条件的记录 prepare sqltpl from 'select name from table1 where name in (?)'; set @a='zhangsan,lisi,wangwu'; execute sqltpl using @a; 结果没有记录

    作者回复: 嗯,原因是: 1、id是数字型,传入的1,2,3,4,5,6,7,8,9的并不能转换为数字,所以截断为1(第一个逗号之前的数字) 2、zhangsan,lisi,wangwu整个当做一个字符串来查询了,所以数据库中根本查不到name='zhangsan,lisi,wangwu'这样的记录

    2020-04-06
    3
    18
  • 行者
    老师,关于 千万别把异常定义为静态变量,麻烦分析下为什么cancelOrderRight抛出的异常信息指向createOrderWrong所在的行~

    作者回复: 创建异常的时候一次性fillInStackTrace了,除非这样: BusinessException ex = Exceptions.ORDEREXISTS; ex.fillInStackTrace(); throw ex; (这样同样不是线程安全的)

    2020-04-04
    5
    15
  • 行者
    IllegalArgumentException: 入参错误,比如参数类型int输入string。 IllegalStateException: 状态错误,比如订单已经支付完成,二次请求支付接口。 UnsupportedOperationException: 不支持操作错误,比如对一笔不能退款的订单退款。 其他异常 SecurityException: 权限错误,比如未登陆用户调用修改用户信息接口。

    作者回复: 不错

    2020-04-04
    14
  • 终结者999号
    关于行者的问题,请老师再解释一下可以吗?貌似没有看懂那个回复,什么叫一次性fillstacktrace?

    作者回复: 就是Throwable的stacktrace只是在其new出来的时候才初始化(调用fillInStackTrace方法)是一次性的(除非你手动调用那个方法),而非getStackTrace的时候去获得stacktrace,毕竟我们关心的是异常抛出时的栈。

    2020-04-05
    2
    8
  • 小杰
    老师,看不懂那个异常定义为静态变量的例子,异常打印的是堆栈信息,堆栈信息必须是对象是这个意思吗?

    作者回复: 仔细观察一下栈,数据串了,取消订单的异常栈显示了下单的方法

    2020-04-05
    3
  • yan_12345
    老师,在业务代码当中,在catch之后,在什么情况下需要重新抛出新的异常,这样做有什么意义和作用?

    作者回复: 有意义,把偏向于底层的异常转换为高层异常。换句话说把偏向于内部处理自己才能理解的异常转换为一个外部可以理解的异常抛出。

    2020-04-17
    2
  • 看不到de颜色
    好文,目前生产环境就存在出现异常只向上返回e.getMessage(),导致出现预期外的问题时一脸懵逼,这块日后一定要注意。statis Exception这块学习到了,对日后闭坑有很大帮助。通过这篇文章还学到了try-with-resources语法,收货颇丰。

    作者回复: 有收货就好。statis->static。

    2020-04-11
    1
  • skying
    你好,关于 提交到线程池的线程发生异常的场景。 我本地环境 添加了UncaughtExceptionHandler,但后续的5次循环中,打印的线程还是test1. 本地环境:spring boot2.2.5+undertow

    作者回复: 异常处理程序只是作为保底出了异常可以自己来处理,不能防止异常产生。 文中修复方式有 2 步,第一步是比较重要的:以 execute 方法提交到线程池的异步任务,最好在任务内部做好异常处理;设置自定义的异常处理程序作为保底,比如在声明线程池时自定义线程池的未捕获异常处理程序:

    2020-04-10
    4
    1
收起评论
显示
设置
留言
36
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部