尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java开发如何做好异常处理与日志记录

Java开发如何做好异常处理与日志记录 异常处理与日志记录在Java开发里就像呼吸一样频繁却又常被当作例行公事。很多人写try-catch只是为了让代码不崩记日志只是为了出问题时能有个交代。但这种“防御性编程”的惰性恰恰是生产事故的温床。真正的高手会把异常和日志当作系统的一部分来设计而不是事后补丁。异常处理的本质不是捕获错误而是定义系统的边界行为。每一次catch都是在对调用者承诺“这里发生了什么接下来该怎么办”。想清楚这个你的代码质量会完全不同。先扔掉“吞异常”的恶习最让人头疼的代码不是没有异常处理而是catch块里只有一行e.printStackTrace()或者logger.error(e.getMessage())。前者把堆栈打到标准输出在容器里几乎等于没记后者只记信息不记堆栈排查问题时你连错误发生在哪行都不知道。吞异常是最隐蔽的技术债务它让系统看起来正常运行实际上早已千疮百孔。常见的吞法还有catch Exception后返回null、返回空集合、或者干脆不写任何语句。这种代码一旦上线就像埋下一颗颗定时炸弹等到某个深夜触发时你连从哪里下手都不知道。正确的做法是要么处理要么抛出要么包装后抛出。如果当前方法确实无法处理这个异常就别硬接。让异常沿着调用链向上传播由真正有上下文的地方去决策。如果觉得底层异常太“裸”就包装成业务异常并附上关键参数。记住异常信息里应该包含“发生了什么”和“当时的状态”否则就是废日志。异常粒度别用一个catch吞掉所有可能很多人在方法外面套一个大大的try-catch把整个方法体包住然后catch (Exception e)。这种写法省事但等于把错误诊断的线索全部剪断。你用一个大网捞鱼捞上来的全是混在一起的泥沙。正确的粒度是尽量让try块覆盖“可能抛出异常的那一行或那几行操作”而不是整个业务逻辑。比如网络调用、数据库操作、文件读写、JSON解析这些都是独立的“风险点”应该分别处理或分别定义异常场景。再看异常类型的选择。自定义异常固然好但别滥用。如果JDK已经有合适的异常类型优先用现成的。IllegalArgumentException、IllegalStateException、UnsupportedOperationException这些足以表达大部分参数错误和状态错误。需要带业务语义时再定义BusinessException。另外受检异常和非受检异常的选择要克制——过度使用受检异常会让调用方被迫catch或throws代码里全是机械的声明确实烦人但完全不检查也不对。原则是如果是调用方可以通过正确调用避免的问题用受检异常如果是系统内部无法预料的错误用非受检异常。日志不是越多越好而是越有“信息密度”越好谈到日志记录很多项目的现状是要么打了大量INFO日志全是没有意义的“进入方法”“退出方法”要么日志稀疏出了事根本找不到蛛丝马迹。日志的价值不在数量而在每条日志能否回答“系统当时在做什么、状态如何、数据是什么”。一个有用的日志至少要包含时间戳、线程名、日志级别、Logger名称、具体消息以及关键上下文。那种只写“处理成功”的日志等于没写。日志级别是给监控系统看的不是给程序员看的。ERROR不一定代表系统挂了WARN也不一定是坏事。你要定义好团队内部的级别语义ERROR是“需要立即人工介入”的错误WARN是“不影响当前请求但值得关注”的异常情况INFO是“关键业务节点”的正常流程DEBUG是“开发期排查问题”的细节。别把DEBUG信息打成INFO否则日志量爆炸真正有用的信息被淹没在噪声里。也别把INFO打成ERROR否则监控疯狂告警时间长了大家就不再信任告警了。上下文信息让每条日志都能串成一条线排查生产问题时最崩溃的莫过于日志里只有一条孤零零的异常堆栈你根本不知道这个异常是哪个用户、哪次请求、哪笔订单触发的。没有上下文的日志就像没有病历号的病历。解决办法很简单在入口处生成一个traceId贯穿整个请求链路然后通过MDCMapped Diagnostic Context把它塞进日志上下文。所有日志框架Logback、Log4j2都支持MDC只要在拦截器或Filter里写入日志pattern中加上%X{traceId}就能让一条链路的所有日志拥有同一个标识。进一步地把关键业务参数也放进MDC或日志消息里。比如用户ID、订单号、操作类型。日志中带业务主键是排查问题的第二把钥匙。光有traceId还不够如果你的订单号不是traceId那你还是不知道这单是哪位用户的。所以最好两个都要traceId解决“请求链路”维度业务主键解决“业务实体”维度。别忘了在finally里清理MDC否则线程池复用会导致上下文串号。打日志的时机与性能陷阱很多人担心打日志影响性能尤其在循环里、高频调用路径上。这是真问题。但解决方案不是不打日志而是聪明的打。对于高频路径用isDebugEnabled()或isInfoEnabled()先判断级别再拼接消息避免无谓的字符串拼接。如果使用SLF4J的参数化日志log.debug(param{}, value)其实已经比字符串拼接高效得多只有真正需要长耗时的大对象序列化时才值得手动判断。另外永远不要在日志消息里调用远程接口或数据库查询哪怕是为了补充上下文。这会让日志系统反噬业务线程。还有一个常见误伤在catch块里打日志后又抛出新异常导致同一错误出现两条日志。要么打日志要么抛异常二者取其一。如果你选择抛出一个包装后的异常那么原始异常要作为cause传进去这样堆栈不会断。如果你选择打日志那就别再往上抛一个一样的异常。否则日志系统里会出现大量重复信息让真正的问题被掩盖。记住日志是给人类看的异常是给代码看的两者的职责交织但不重叠。子标题从一次线上故障看两者如何协作假设一个典型的场景用户下单成功但支付回调处理失败。如果你只在catch里打了“支付回调失败”六个字没有订单号、没有回调报文、没有失败原因你根本无从下手。反过来如果你在入口处记录收到的回调报文在业务处理前后分别记录订单状态变更在异常处记录堆栈和关键字段那么你可以在两分钟内定位到是签名校验失败还是金额不匹配。好的日志不是事后证据而是事前的探照灯。这正是异常处理与日志记录的协作异常负责“控制流程”日志负责“留下痕迹”两者一起构建出系统的可观测性。再考虑一种微妙的情况你catch了异常然后打日志最后返回一个友好的错误响应给前端。这是最标准的流程。但这里的日志级别怎么选如果你预期这个异常会发生比如参数校验失败用WARN合适如果你没预期到的系统异常比如数据库连接断了用ERROR。级别的选择就是你的判断力——它告诉监控平台“这个错误是否需要被处理”。这种判断力不是天生的来自对业务语义的理解和对运行环境的掌握。子标题让异常在合适的位置被捕获很多新手喜欢在Service里catch所有异常然后在Controller里又catch一遍。异常在不同的抽象层之间传播时应该被不断“翻译”成更适合该层语义的形式。比如在Repository层抛出的SQLException到了Service层应该变成数据访问异常并附带SQL状态到了Controller层应该变成业务错误码返回给前端。每一层都只关心自己能处理的和需要暴露的。如果你在底层已经把异常吞掉那上层就失去了判断依据如果你在每一层都catch那就会产生大量转场噪声。更好的做法是让异常在“业务边界”被捕获和处理。边界就是你能对外部世界给出明确响应的位置。比如RESTful Controller就是HTTP边界消息队列的listener就是消息边界定时任务的执行器是调度边界。在这些边界上确保异常被完整记录并返回或提交一个可理解的失败信号。这比在每个方法里都catch要清晰得多。子标题设计一个可靠的异常处理框架有了这些理念落地的时候可以建一个统一异常处理器。在Spring Boot中RestControllerAdvice配合ExceptionHandler可以集中处理异常返回结构。但别把统一异常处理器当成万能垃圾桶。它只负责处理Controller层暴露出来的异常业务代码里的异常仍然要清晰抛出。同时要设计一个统一的错误响应体包含错误码、错误消息、traceId、时间戳。这样前端和调用方都能按规范处理。日志方面建议建立日志规范文档定义统一的pattern格式时间 | 线程 | 级别 | traceId | logger名称 | 消息 | 异常堆栈。每个团队可以有自己的变体但核心字段必须齐备。格式统一是日志分析的前提否则就等着写一堆正则去解析吧。另外要考虑日志的可搜索性。生产环境中你可能没有权限去服务器上grep日志文件所以日志平台的索引字段是什么、怎么按traceId查、怎么按错误码聚合都需要在开发时就有意识地适配。子标题异常处理与日志记录的终极目标说到底异常处理的终极目标不是让系统不崩溃而是让系统即便宕机也能被快速理解。每次catch语句都在做一种“翻译”把技术异常翻译成业务语言把错误细节翻译成可读线索。日志记录的终极目标是让“时间线”能够被重建。哪一刻发生了什么请求哪一刻失败了哪一刻重试成功这些串联起来就是系统运行的完整故事。高手写代码时会像导演一样为每一个关键节点设计“台词”而不是随意丢几句“报错”“成功”的废话。试想一下你接手一个老系统没有文档没有注释只有日志。如果日志清晰你依旧能快速定位问题如果日志全是“exception occurred”那基本等于没有。代码和日志是你留给未来同事包括未来自己的忠诚遗嘱。所以每次写try-catch时多问一句我留下的这条日志能让一个毫无背景的人看懂发生了什么吗如果不行就重写。从今天开始把这些要点用起来检查你的catch块把空的、只打e.getMessage()的全部改成带堆栈和上下文信息的日志。在每个业务关键节点创建、更新、删除、调用第三方、异步任务放一条INFO日志记录业务主键和操作结果。引入traceId并确保日志pattern包含它。统一异常处理定义清晰的错误码不要让前端收到一堆500和空指针信息。为日志级别写团队规范每个人都要清楚ERROR意味着什么。定期检查日志量如果INFO日志里90%都是没用的说明你该削减噪声了。这些不是额外工作而是工程质量的基本盘。当你习惯了这种写法你会发现排查问题的时间从“半天”变成了“十分钟”。你写的代码也会更受同事信任。异常处理与日志记录是Java开发中最不起眼却最能拉开差距的能力。别让它成为你项目中永远的痛。
返回列表