大家好,我是晚安code。
假设一个场景:支付订单批量失败,就一个 NPE,堆栈被吞得干干净净。查了很久,最后发现是第三方回调超时,被中间一层 catch 住悄悄吞掉了。这种锅,八成不是运气差,是 Java 异常与日志规范没立好。

这套规范,核心就一句话:报错要能讲清楚「是谁的错、错在哪、怎么定位」。本文按错误码、异常处理、日志规约三个维度展开,每条都配正反例,照着抄就行。先点个收藏,我们开始。
一、错误码:让每个报错都有「来源」
好的错误码设计,解决的不是"给错误起名字",而是让任何一个人看到一行报错,十秒内能说清"这是谁的错、错在哪"。
错误码:用于标识错误来源与类型的固定编码,由「错误来源 + 数字编号」组成。你可以把它理解成快递单号------拿到单号扫一眼,就知道是哪家快递、哪个环节出了问题。
先说一个我见过的反例。有团队把错误码设计得极其精致,一个码里恨不得塞进模块、等级、场景三层含义,五位数字每位各代表一种意思,美其名曰"结构化"。结果线上报错没人看得懂,排查要先翻半小时文档对照位定义。这就像生僻字字典------字造得越准,越没人随身带着查。所以我把错误码设计原则压成一条:来源可判断、比对可 equals、语义团队认知一致。
错误码前一位字母标来源,后四位是数字编号:

- A 用户端:参数传错、支付超时这类"用户那边"的问题;
- B 本系统:业务逻辑出错、程序健壮性不足;
- C 第三方:CDN 故障、消息投递超时。
四位编号从 0001 到 9999,大类之间步长留 100,方便以后插新类别。编号以先到先得的方式在统一平台申请,审批通过就永久固定------别跟业务架构、组织架构挂钩,否则团队一调整,错误码全作废。
几个容易踩的坑,我直接说结论:
- 一切正常但必须填错误码时,用 00000 占位;
- 错误码不体现版本号和错误等级,等级由日志和释义决定;
- 别随手造新码,先在已有附表里找语义相近的用;
- 错误码不能直接输出给用户 。错误码(error_code) 、堆栈(stack_trace) 、错误信息(error_message) 、提示信息(user_tip) 各司其职------错误码给开发看,提示语给用户看,越俎代庖两边都看不懂;
- 调用第三方出错时,允许转义成 B(本系统),但错误信息里要带上原始第三方错误码,不然找对方对线都没依据。

可能有人会问:错误码为什么不直接展示给用户?
错误码是给开发、运维看的定位标识,直接甩给用户,得到的反馈大概率是"这串数字是啥"。用户需要的是能指导下一步的提示语(比如"订单号不存在,请核对"),错误码 + 堆栈 + 提示语各管一段,才是一套完整的沟通。
二、异常处理:该兜的兜住,该暴露的暴露
异常处理的第一原则不是"怎么 catch",而是这个错误该不该由我这一层来接。接得住就接,接不住就往上抛,最怕半吊子------catch 住又不处理,等于把炸弹捂在怀里。
checked 异常 :编译期强制检查的异常(如 IOException),不处理过不了编译。 unchecked 异常 :运行时异常(RuntimeException),编译期不查,跑起来才炸。 NPE:NullPointerException,空指针异常,Java 里最常见的运行时异常。
Java 异常处理规范里被讨论最多的一条,就是"别拿异常当 if 用"。能预检查的先预检查------NPE、IndexOutOfBoundsException 这类本可以用判空避免,就别用 catch 兜,性能差一个量级不说,还掩盖了真正的逻辑问题:
java
// 正例:预检查
if (obj != null) { obj.method(); }
// 反例:拿 catch 当判断
try { obj.method(); } catch (NullPointerException e) { /* 静默吞掉 */ }
catch 时要分清稳定代码和非稳定代码。一大段代码包一层 try-catch,出任何问题都走同一个分支,程序没法按不同异常做不同反应,也定位不了问题。正确做法是区分异常类型分别处理------比如用户注册:输入非法字符、用户名已存在、密码太简单,在代码里分门别类判断并给出对应提示。

捕获了就必须处理。不想处理就 throws 抛给调用者,最外层业务使用者必须转成用户能看懂的内容。几个高频坑单独拎出来说:
- 事务场景:catch 后要回滚,记得手动回滚事务,别以为框架会替你擦屁股;
- finally 块 :关资源要 try-catch 兜住;不要在 finally 里 return,它会无情吞掉 try 里的返回值;
- catch 类型要匹配:捕获的异常要么与抛出的一致,要么是它的父类。对方抛绣球你偏伸手接铅球,接不住就砸脸上;
- RPC / 二方包 / 反射调用:用 Throwable 拦截。我踩过一个真坑------二方包版本冲突,运行期抛 NoSuchMethodError,catch 用的却是 Exception,异常往上冒,把一个非核心功能点炸成了核心链路事故。

防止 NPE 是调用者的责任,这点写进方法注释说明白。级联调用 obj.getA().getB().getC() 最容易炸,JDK8 的 Optional 是现成的护栏:
java
// 级联调用易 NPE,用 Optional 兜底
Optional.ofNullable(user)
.map(User::getAddress)
.map(Address::getCity)
.orElse("未知城市");
另外两个容易忽略的 NPE 来源:基本类型返回值拆箱(return Integer 对象为 null 时自动拆箱炸),以及远程调用返回对象------一律先判空。

自定义异常别图省事抛 new RuntimeException(),更别抛裸的 Exception / Throwable,用有业务含义的自定义异常,业界有现成的 DAOException / ServiceException 直接用。跨应用 RPC 优先用 Result 封装(isSuccess + 错误码 + 简短信息),应用内部推荐异常抛出------抛异常在频繁调用场景下序列化传输损耗不小,这也是我后来才想明白的。
可能有人会问:catch Throwable 会不会把 OOM 这种致命错误也吞了?
会,所以 Throwable 拦截只用在 RPC、二方包、反射这类"编译期对但运行期会变"的场景,捕获后立刻判断并抛出,而不是捂住。Error 类致命错误本来也不该静默处理。
三、日志规约:排查问题的唯一线索
日志不是"记录现场",而是让三个月后接手的人能看懂现场。你记录的每条日志,都要对得上"当时发生了什么、为什么这么走"这两个问题。(2026 年 8 月整理,基于 Java 8+ 与 SLF4J 2.x)
日志门面(SLF4J):统一日志 API 的抽象层,底层实现可换成 Logback、Log4j2 而不用改业务代码。你可以理解为"插座"------换电器(实现)不用重新布线。
SLF4J 日志规范的第一条铁律:代码里只依赖 SLF4J / JCL 门面 API,别直接调 Log4j、Logback 的类。字符串拼接用占位符,别用加号------logger.info("订单 {} 已支付,金额 {}", id, amount) 只是替换动作,比 StringBuilder append 快得多。trace / debug / info 级日志还要先做开关判断,否则 debug(getName()) 这种写法每次都会白跑 getName():
java
// 参数可能拼接开销大,先判断开关
if (logger.isDebugEnabled()) {
logger.debug("当前 ID:{},名称:{}", id, getName());
}
生产环境日志第一条铁律,是禁 System.out / System.err / e.printStackTrace()。标准输出文件通常要等 Jboss 重启才滚动,量大一点直接把系统文件撑爆。我接手过一个服务,代码里几十处 System.out.println,排查问题全靠肉眼盯控制台,改完规范的第一周,磁盘告警就消停了。

日志要带上"案发现场 + 异常堆栈"两件套:入参、关键上下文、e.getMessage()、异常对象本身,缺一不可。还有个反直觉的坑:打印日志时别直接拿 JSON 工具把对象转 String------对象里某个 get 方法被覆写过,打印时可能抛异常,直接让正常业务流程挂掉。要打印就只打业务相关属性,或者调它自己的 toString()。
日志级别怎么选,我做了张表,照着用就行:
| 级别 | 什么时候用 | 典型场景 |
|---|---|---|
| error | 系统逻辑出错、异常、重要故障 | 异常堆栈、核心链路失败 |
| warn | 可恢复的异常情况、参数错误 | 用户输入非法、第三方超时重试 |
| info | 有选择地记录业务行为 | 关键流程节点、订单状态流转 |
| debug | 本地调试,生产禁开 | 开发期排查逻辑 |
最后几条容易被忽略的:日志文件至少留 15 天,涉及网络运行状态、个人敏感信息操作的记录要留不少于六个月并多机备份;敏感信息要脱敏,排查问题用订单号、UUID 这类唯一编号查,别直接打手机号、身份证;日志错误信息优先英文,英文说不清楚再用中文,避免歧义。

说到底,Java 异常与日志规范就一句核心:错误码定责任,异常定流向,日志定证据。三者合起来,线上问题从"两小时大海捞针"变成"十分钟精准定位"。这套规范照抄进团队,第一周就能看到效果------前提是 code review 时真的去查,别让规范躺在文档里吃灰。
对新手我的建议是:先别纠结规范全不全,从"catch 不吞异常、日志用占位符、生产禁 println"这三条抓起,就已经赢过一大半项目了。
我是晚安code,持续分享编程干货。觉得有用的话记得点赞收藏和关注~也欢迎在评论区聊聊:你的团队是怎么落地 Java 异常与日志规范的?有没有哪条是踩了坑才补上的?