- 开发工具
- 可观测性
- 后端
【免费下载链接】vjtools
The vip.com's java coding standard, libraries and tools
导读
本文以《唯品会Java开发手册》(vjtools 仓库 docs/standard 章节)中的“异常处理”一章为骨架,系统讲解 Java 异常创建成本、静态异常复用、异常抛出/捕获/处理及 finally 块等 8 大规约,并逐一对应到仓库中 vjkit 工具库(ExceptionUtil、CloneableException、IOUtil 等)与 sonar-vj 定制规则的源码实现。读完本文,你将掌握一套可直接落地的异常编码规范,并理解其底层原理与配套的静态检查落地方式。
一、异常处理规范总览
本章(docs/standard/chapter10.md)是《唯品会Java开发手册》(docs/standard/README.md)的第十章,共 8 条规则,其中【强制】4 条、【推荐】4 条,覆盖从异常“产生”到“消亡”的完整生命周期:
| 规则 | 关注点 | 级别 |
|---|---|---|
| Rule 1 | 创建异常的消耗大,只用在真正异常的场景 | 强制 |
| Rule 2 | 特定场景避免每次构造异常(静态异常复用) | 推荐 |
| Rule 3 | 自定义异常建议继承 RuntimeException | 推荐 |
| Rule 4 | 异常日志应包含排查问题的足够信息 | 推荐 |
| Rule 5 | 异常抛出的原则(标准异常优先、按需定义) | 推荐 |
| Rule 6 | 异常捕获的原则(按需捕获、多异常合并) | 推荐 |
| Rule 7 | 异常处理的原则(不可吞异常、不捕获则处理) | 强制 |
| Rule 8 | finally 块的处理原则(资源关闭、禁止 return) | 强制 |
值得注意的是,本章在阿里手册基础上做了“增补与删减”(完整对照见 docs/standard/ali.md),其核心思路是:异常是昂贵的控制流,能规避就规避,能复用就复用,捕获后必须负责到底。
二、Rule 1(强制):创建异常的消耗大,只用在真正异常的场景
构造异常对象时,JVM 需要获得整个调用栈(fillInStackTrace),这是一笔不小的开销。因此异常不应被用来做流程控制或条件控制——条件判断的效率远高于异常处理。
典型反例:用捕获NullPointerException来做空值判断:
//WRONG try { return obj.method(); } catch (NullPointerException e) { return false; }正确做法:对发生概率较高的条件,先做检查规避:
//RIGHT if (obj == null) { return false; }如果代码里频繁捕获IndexOutOfBoundsException、NullPointerException这类“本可预防”的异常,通常意味着存在坏味道。这条规则对应 Sonar 规则 RSPEC-1696: "NullPointerException" should not be caught。
三、Rule 2(推荐):在特定场景,避免每次构造异常
承接 Rule 1,如果异常频繁发生且不需要打印完整调用栈,可以考虑绕过异常构造函数。文档给出三种手段:
1)message 不变:将异常定义为静态成员变量
private static RuntimeException TIMEOUT_EXCEPTION = ExceptionUtil.setStackTrace(new RuntimeException("Timeout"), MyClass.class, "mymethod"); ... throw TIMEOUT_EXCEPTION;这里的ExceptionUtil即 vjkit 工具库中的 ExceptionUtil.java。其setStackTrace(Throwable, Class, String)方法参考 Netty 的做法,为静态异常设置仅一层的 StackTrace(抛出点类名 + 方法名),替代完整调用栈:
public static <T extends Throwable> T setStackTrace(@NotNull T throwable, Class<?> throwClass, String throwClazz) { throwable.setStackTrace( new StackTraceElement[] { new StackTraceElement(throwClass.getName(), throwClazz, null, -1) }); return throwable; }该方法返回异常对象本身,便于静态初始化时链式赋值。对应的测试见 ExceptionUtilTest.java 的staticException()用例:它断言静态异常的 StackTrace 文本只有 2 行,且指向ExceptionUtilTest.hello这个自定义抛出点,证明“单层 StackTrace”效果确实生效。
补充说明:若异常可能在多个地方抛出,应使用setStackTrace显式指定抛出类与方法;而 ExceptionUtil.clearStackTrace() 则用于“无法控制生成端,但能控制打印端”的场景——它沿 Cause 链逐层清空 StackTrace(注意 Cause 链本身无法清除)。
2)message 会变化:对静态异常实例 clone() 后再修改 message
private static CloneableException TIMEOUT_EXCEPTION = new CloneableException("Timeout") .setStackTrace(My.class, "hello"); ... throw TIMEOUT_EXCEPTION.clone("Timeout for 40ms");Java 默认异常并不实现 Cloneable,vjkit 为此提供了 CloneableException.java(继承了Exception):
clone():基于super.clone()复制实例,不经过构造函数,也就避免了重新获得 StackTrace;clone(String message):克隆后直接设置新 message;setStackTrace(Class, String):内部委托给ExceptionUtil.setStackTrace,用于静态初始化。
@Override public CloneableException clone() { // NOSONAR try { return (CloneableException) super.clone(); } catch (CloneNotSupportedException e) {// NOSONAR return null; } }测试用例同样验证了该行为:TIMEOUT_EXCEPTION2.clone("Timeout for 30ms")后,打印的 StackTrace 仍只有 2 行(消息变为新值,抛出点保持ExceptionUtilTest.hello),即克隆没有重新生成完整调用栈。
如果希望异常直接继承RuntimeException(契合 Rule 3),仓库还提供了对应的 CloneableRuntimeException.java,API 与 CloneableException 完全一致,可按需选用。
3)重载 fillInStackTrace() 为空函数
自定义异常也可以重载fillInStackTrace()为空函数来跳过调用栈生成,但相对不够灵活——无法像方案 1/2 那样按场景指定一层 StackTrace。
四、Rule 3(推荐):自定义异常,建议继承 RuntimeException
详见《Clean Code》,该争论已经结束,不再推荐初衷很好的 CheckedException。原因在于:
- CheckedException 需要在“抛出异常的地方”与“捕获处理异常的地方”之间层层定义
throws XXX来传递,底层代码一旦改动,将影响所有上层函数的签名,导致编译出错,对封装的破坏严重; - 对 CheckedException 的处理也给上层程序员带来额外负担;
- 其他主流语言都没有 CheckedException 的设计。
这与 vjkit 的设计一脉相承:CloneableRuntimeException、UncheckedException(见 UncheckedException.java)均继承RuntimeException。其中UncheckedException是 CheckedException 的包装器,ExceptionUtil.unchecked(t)会把 CheckedException 包装后重新抛出,减少函数签名中的 CheckedException 定义,其测试用例(ExceptionUtilTest.java 的unchecked())确认:RuntimeException 与 Error 原样抛出,仅普通 Exception 被包装。
五、Rule 4(推荐):异常日志应包含排查问题的足够信息
异常信息应包含排查问题时足够的上下文;捕获并记录异常日志的地方,还需要记录“未包含在异常信息中、但排查问题需要的信息”,比如捕获处的上下文。
//WRONG new TimeoutException("timeout"); logger.error(e.getMessage(), e); //RIGHT new TimeoutException("timeout:" + eclapsedTime + ", configuration:" + configTime); logger.error("user[" + userId + "] expired:" + e.getMessage(), e);这条规则对应 Facebook-Contrib 的 Style 规则:Method throws exception with static message string。要点:异常 message 携带业务度量(耗时、配置值),日志再补充调用上下文(用户 ID),二者配合才能快速定位问题。
六、Rule 5(推荐):异常抛出的原则
5.1 尽量使用 JDK 标准异常与项目标准异常
优先使用 JDK 标准的 RuntimeException,如IllegalArgumentException、IllegalStateException、UnsupportedOperationException;业务上使用项目定义的ServiceException。标准异常语义清晰,调用方无需额外学习成本。
5.2 根据调用者的需要来定义异常类
是否定义独立的异常类,关键看调用者会如何处理这个异常。如果没有特殊处理需求,直接抛出RuntimeException也是允许的。这避免了为异常而异常、无谓地扩充异常类层级。
七、Rule 6(推荐):异常捕获的原则
6.1 按需要捕获异常,捕获 Exception 或 Throwable 是允许的
如果无特殊处理逻辑,统一捕获Exception统一处理是允许的。捕获Throwable则用于捕获Error类异常,包括其实无法处理的OOM、StackOverflow、ThreadDeath,以及类加载/反射时可能抛出的NoSuchMethodError、NoClassDefFoundError等。
6.2 多个异常处理逻辑一致时,使用 JDK7 的多 catch 语法
try { ... } catch (AException | BException | CException ex) { handleException(ex); }对应 Sonar 规则 RSPEC-2147: Catches should be combined,避免重复代码。
八、Rule 7(强制):异常处理的原则
7.1 捕获异常一定要处理;故意忽略须注释写明原因
空 catch 块是典型的坏味道。若确实需要忽略(比如循环中的单项失败),必须用注释说明原因,方便阅读者确认“此处不是漏了处理”:
//WRONG try { } catch(Exception e) { } //RIGHT try { } catch(Exception ignoredExcetpion) { //continue the loop }vjtools 在 Sonar 落地时进一步放宽了这一条:定制规则 CatchUsesExceptionWithContextCheck.java 在实现 Sonar S1166 时,忽略异常变量名含 "ignore" 字样的检查(catch(Exception ignore)视为有意忽略,不再报警),见 sonar-vj/README.md 规则表第 1166 行。
7.2 不能吞掉原异常:要么打日志,要么在重新抛出的异常里包含原异常
//WRONG throw new MyException("message"); //RIGHT 记录日志后抛出新异常,向上次调用者屏蔽底层异常 logger.error("message", ex); throw new MyException("message"); //RIGHT 传递底层异常 throw new MyException("message", ex);对应 Sonar 规则 RSPEC-1166: Exception handlers should preserve the original exceptions。该规则默认包含若干例外:InterruptedException、NumberFormatException、NoSuchMethodException等。在 CatchUsesExceptionWithContextCheck.java 中,这些例外类型被显式配置为exceptions属性默认值,同时额外加入了ParseException、MalformedURLException、DateTimeParseException等解析类异常——这些场景捕获后不处理通常可接受。
7.3 最外层业务使用者必须处理异常
如果不想处理异常,可以不捕获(让异常向上传播);但最外层的业务使用者必须处理异常,将其转化为用户可以理解的内容,而不是把堆栈直接抛给用户。
九、Rule 8(强制):finally 块的处理原则
8.1 必须关闭资源对象/流对象,或使用 try-with-resource
关闭动作必须放在 finally 块,不能放在 try 块或 catch 块(这是经典错误)。更推荐直接使用 JDK7 的 try-with-resource 语法自动关闭 Closeable 资源:
try (Writer writer = ...) { writer.append(content); }8.2 处理过程中如有抛出异常的可能,也要 try-catch,防止 finally 中的异常顶替原异常
//WRONG try { ... throw new TimeoutException(); } finally { file.close();//如果file.close()抛出IOException, 将代替TimeoutException } //RIGHT, 在finally块中try-catch try { ... throw new TimeoutException(); } finally { IOUtil.closeQuietly(file); //该方法中对所有异常进行了捕获 }规则背后的原理:finally 块中抛出的异常会顶替try 块中尚未抛出的异常。对应 Sonar 规则 RSPEC-1163: Exceptions should not be thrown in finally blocks。
文档示例中的IOUtil.closeQuietly(file)正是 vjkit 的 IOUtil.java 中提供的“安静关闭”方法:内部捕获 IOException 并仅打 warn 日志,保证不干扰原有异常流:
public static void closeQuietly(Closeable closeable) { if (closeable == null) { return; } try { closeable.close(); } catch (IOException e) { logger.warn("IOException thrown while closing Closeable.", e); } }它还兼容closeable == null的情况(资源可能未实际创建),可以放心地在 finally 中使用。
8.3 禁止在 finally 块中使用 return
finally 块中的 return 将代替try 块中的 return 及 throw Exception:
//WRONG try { ... return 1; } finally { return 2; //实际return 2 而不是1 } try { ... throw TimeoutException(); } finally { return 2; //实际return 2 而不是TimeoutException }对应 Sonar 规则 RSPEC-1143: Jump statements should not occur in "finally" blocks。
十、规范落地:Sonar 定制规则如何支撑本章
《唯品会Java开发手册》的落地主要依赖代码格式模板与 Sonar 代码规则检查(见 docs/standard/README.md 的“规范落地”一节)。由于官方 Sonar 规则存在误报,vjtools 在 standard/sonar-vj 中定制了与本章直接相关的若干规则:
| 编号 | 规则描述 | 与本章的对应修改 |
|---|---|---|
| S1166 | Exception handlers should preserve the original exceptions | 对应 Rule 7.2;忽略异常变量名含 ignore 字样的检查 |
| S1163 | Exceptions should not be thrown in finally blocks | 对应 Rule 8.2 |
| S1143 | Jump statements should not occur in "finally" blocks | 对应 Rule 8.3 |
| S2147 | Catches should be combined | 对应 Rule 6.2 |
| S1696 | "NullPointerException" should not be caught | 对应 Rule 1 |
这些规则以源码形式存放在 standard/sonar-vj/src/main/java/com/vip/vjkit/sonarvj/checks/ 目录下,编译后放入 Sonar 的 lib 目录并重启,即可用“带 VJ 字样”的规则替代官方同编号规则(具体步骤见 sonar-vj/README.md)。此外,CatchUsesExceptionWithContextCheck.java 还额外处理了Enum.valueOf()捕获 IllegalArgumentException 的合法场景,可见落地时对真实业务场景的细致考量。
十一、总结:异常处理的八条心法
- 异常创建昂贵,只用于真正异常的场景,可预防的条件先检查规避(Rule 1);
- 高频异常可静态复用:message 不变用
ExceptionUtil.setStackTrace,message 变化用CloneableException.clone()(Rule 2); - 自定义异常继承 RuntimeException,避免 CheckedException 污染签名(Rule 3);
- 异常信息与日志都要包含足够的排查上下文(Rule 4);
- 抛出:优先 JDK 标准异常,按调用者需要定义异常类(Rule 5);
- 捕获:按需捕获,逻辑一致的多异常用多 catch 合并(Rule 6);
- 处理:捕获必处理,忽略需注释,永不吞异常,最外层必须面向用户转化(Rule 7);
- finally:资源必关(优先 try-with-resource)、不抛异常、不 return(Rule 8)。
以上八条,配合 vjtools 仓库中 ExceptionUtil.java、CloneableException.java、IOUtil.java 等工具类以及 sonar-vj 的定制规则,即可在日常开发中实现从“编写规范”到“自动检查”的完整闭环。
- 开发工具
- 可观测性
- 后端
【免费下载链接】vjtools
The vip.com's java coding standard, libraries and tools
相关推荐
唯品会 Java 开发手册(vjtools)之集合处理:12 条规约与 vjkit 源码级实践
唯品会 Java 开发手册(vjtools)之集合处理:12 条规约与 vjkit 源码级实践 导读 本文基于《唯品会 Java 开发手册》(vjtools 仓
开发工具可观测性后端唯品会Java开发手册(vjtools)命名规约全解:13条强制与推荐规则及Sonar落地实践
唯品会Java开发手册(vjtools)命名规约全解:13条强制与推荐规则及Sonar落地实践 《唯品会Java开发手册》1.0.3版(本仓库 vjtools
开发工具可观测性后端唯品会 Java 开发手册之注释规约:10 条规则详解与落地实践
唯品会 Java 开发手册之注释规约:10 条规则详解与落地实践 导读 本文基于《唯品会Java开发手册》(vjtools 仓库 docs/standard/c
开发工具可观测性后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考