如果你是后端新手,每天沉浸在用各种酷炫框架写出业务逻辑的快感里,那我必须给你当头一棒:你花80%精力写出来的“正确”代码,在线上故障面前可能不值一提。真正决定你系统可靠性的,不是那些精妙的算法或优雅的设计模式,而是日志和错误处理。我甚至敢说,写好日志和错误处理,比写代码本身重要十倍。没有它们,你的代码就像没有仪表盘的飞机——飞得再高,也不知道下一秒会不会坠毁。
日志是系统的“黑匣子”,没有它你只能靠猜
线上环境不是你的本地IDE,你不能随意打断点、不能print看变量。当用户报“系统报错了”,你第一个问题是什么?大多数新手会问:“什么错误?”用户答:“不知道,就是不能用了。”——此时,日志就是你唯一的救命稻草。
一个没有日志的系统,等于把故障排查变成了一场赌博。你只能复现、猜测、然后祈祷。而一个日志完备的系统,你可以直接搜索关键词,找到异常发生那一刻的上下文:用户ID、请求参数、请求耗时、数据库返回、调用链路……所有信息一目了然。日志的价值不是记录“发生了什么”,而是记录“为什么发生”以及“现场是什么样的”。
更可怕的是,很多新手以为“打了日志”就万事大吉。他们会在每个方法里疯狂输出log.info("xxx方法被调用"),但真正的故障来临时,这些日志毫无意义。比如:你打了「用户登录中」,但没打「用户登录成功」;用户报“登录失败”,你只看到“登录中”,不知道是密码错、账号锁、还是网络超时——无效日志比没有日志更致命,因为它浪费你时间去读一堆废话。
新手常犯的日志“七宗罪”
我见过太多新手的代码,日志写得像碎碎念。总结下来,有七种典型错误你必须避开:
1. 日志分级形同虚设:所有消息都打INFO,哪怕打印“循环第1次”这种无意义信息。出问题后,INFO日志泛滥成灾,真正的ERROR被淹没。请记住:ERROR只用于系统级故障(比如数据库连不上),WARN用于不期望但可容忍的异常(比如重试),INFO用于业务关键节点(用户创建成功),DEBUG用于开发期调试——上线后默认关闭。
2. 不打印关键上下文:只打“保存失败”,不打印哪个用户、什么订单、失败原因。加个userId和orderId,排查时间能缩短10倍。
3. 日志中混入敏感信息:把用户密码、手机号、身份证号直接打印出来,这是安全红线,直接违反GDPR或《个人信息保护法》。绝不在日志中输出明文密码或Token。
4. 不同步地疯狂打印:在超高并发下,System.out.println或不同步的日志框架会导致性能雪崩。务必使用异步日志(如Log4j2的AsyncLogger),并设置合理的队列容量和丢弃策略。
5. 不打关键链路的进入和退出:一个服务调用另一个服务,只打“调用开始”,不打“调用结束”或超时信息。一旦调用挂起,你只能看到开始,不知道是一直在处理还是死锁了。
6. 不使用结构化日志:纯文本日志难以解析。现在流行JSON格式日志,配合ELK(Elasticsearch, Logstash, Kibana)直接做全文搜索和聚合分析。即使不考虑ELK,至少也要用标准格式如[时间] [级别] [类名] [用户ID] [请求ID] 消息。
7. 不打异常栈的完整追踪:很多新手在catch里写log.info(e.getMessage())——这只会打印异常消息,丢掉了堆栈。必须用log.error("描述", exception),第二个参数传异常对象,才能看到完整的调用链。
金句:日志不是写给开发看的,是写给半夜被叫起来的运维看的。如果你不想半夜被叫起来,就请好好对待每一行日志。
错误处理:优雅地拥抱错误,系统才真正可靠
代码不可能不犯错。网络断开、磁盘满、并发竞争、外部接口超时……错误是常态,正确处理错误才是后端开发的专业体现。新手往往会把错误处理当作“不重要的收尾工作”,随便写个try-catch就了事。结果呢?线上系统一旦出现异常,要么直接崩溃,要么静默吞掉异常、数据不一致。
错误处理的核心目标不是“避免错误”,而是“在出现错误时系统还能维持可预期行为”。这就像飞行员训练:不是学怎么让飞机永远不坏,而是学发动机停转后怎么安全着陆。
新手错误处理的“四大雷区”
雷区一:吞掉异常。这是最常见的错误:try { doSomething(); } catch (Exception e) { // 什么也不做 }。你成功把bug藏起来了,但数据可能已经错乱了。等到用户发现,没人知道哪里出的问题。永远不要catch异常然后什么都不做。如果你确定这个异常可以忽略,至少打一条WARN日志,并说明忽略的原因。
雷区二:抛出笼统的异常。比如throw new RuntimeException("系统错误")。调用方看到这个异常,完全不知道是参数非法、业务规则被违反还是底层数据库挂了。应该使用自定义异常,比如IllegalArgumentException、BusinessException、SystemException,并带有错误码和错误详情。这样前端可以展示友好错误文案,运维可以快速归类。
雷区三:不区分业务异常和系统异常。业务异常(如“余额不足”)不需要发给运维告警,但系统异常(如“数据库连接池耗尽”)必须立刻通知。新手把所有异常都当成ERROR打印并告警,结果就是告警疲劳,真正出大事时反而没人关心。建立分类体系:BusinessException只打WARN日志,SystemException打ERROR并触发告警。
雷区四:不处理超时和重试。调用外部接口,如果不设置超时时间,一旦对方挂了,你的线程就会永远阻塞,直到系统资源耗尽。另外,对于幂等的操作(如查询、重复提交不会产生副作用),应当自动重试,但必须带上指数退避和最大重试次数,避免雪崩。
如何写出“漂亮”的错误处理代码?
最好的错误处理,是让调用方和运维都能一目了然。下面给出几个具体实践:
1. 使用“卫语句”提前返回:不要写几十层的if-else嵌套来处理错误。比如:
if (!validate(input)) { throw new BusinessException("参数非法", ErrorCode.INVALID_PARAM); } // 正常逻辑
这样代码扁平清晰,错误路径和正常路径完全分离。
2. 错误处理统一放在“门面层”:不要在每一个细小的方法里都try-catch。利用Java的@ControllerAdvice、Spring的@ExceptionHandler,在控制层统一捕获异常,翻译成用户友好的响应码和消息。业务层只需要抛出自定义异常即可。这样既避免代码冗余,又能保证错误处理的一致性。
3. 每次捕获异常时,必须同时记录日志:这两者要像连体婴儿一样。不管是吞掉还是重新抛出,都至少要留一条日志。尤其在你自定义错误处理逻辑时(比如发送告警邮件),一定要先打印ERROR日志再执行后续操作,以免告警逻辑本身失败导致信息丢失。
4. 设计有意义的错误码:错误码要分模块、分场景,比如ORDER_001代表订单创建失败。错误码是日志和监控的索引,有了错误码,你才能在ELK里快速统计某类错误出现的频率和趋势。
5. 注意幂等性:很多错误源于重复请求,比如用户双击提交。在错误处理时,如果业务本身需要幂等,可以在入口处通过唯一请求ID去重,返回已有的结果而不是重新处理。
金句:写得好的错误处理,让系统在被攻击时还能像没出事一样运行;写得差,一个普通的网络波动就能让整个服务雪崩。你选哪个?
日志与错误处理协同作战:1+1 > 2
日志和错误处理不是割裂的,它们必须协同。下面是几个协同规则:
规则一:在抛出异常之前,先打印日志。但注意不要重复打印。比如在catch里打了日志,然后又throw到上层,上层又打印一次——这会导致日志脏数据。最佳实践是:底层代码只抛出异常(带丰富信息),上层统一捕获并打印一次日志。如果需要保留底层细节,可以在异常消息中包含上下文。
规则二:日志中必须包含错误码。不要只打"处理失败",要打"处理失败,错误码:ORDER_005,订单ID:12345"。这样运维可以直接搜错误码,定位问题根因。
规则三:错误处理路径要有“保险丝”。比如当错误率超过阈值时(通过日志聚合监控自动计算),系统自动降级或熔断。这需要你事先在错误处理中埋好计数器,配合监控系统(如Prometheus + AlertManager)实现。
规则四:不要沉默错误,也不要过度抛出。有些错误可以局部容错,比如缓存穿透时可以回源拉取数据,而不是直接抛出异常。这种“优雅降级”是在错误处理中实现的,并且必须打WARN日志,通知运维“缓存失效但已降级”,避免运维误以为系统正常。
真实案例:一个日志齐全的团队是如何用两分钟解决故障的
前公司有两个团队,A团队和B团队,都做支付服务。某天晚上11点,两个服务同时出现支付超时。A团队的日志什么都有,B团队基本没日志。
A团队的做法:在日志平台输入“支付超时”和“ERROR”,立刻找到一条日志:[2023-11-15 23:00:12.345] [ERROR] [com.pay.service.PayService] [userId:10086] [orderId:PAY202311151234] [errorCode:PAY_TIMEOUT] [msg:调用外部银行接口超时] [stacktrace:...]。再看时间附近的其他日志,发现所有超时都指向同一个银行接口,且调用耗时从200ms飙到了5000ms。团队立即做出判断:银行接口故障。两分钟确认问题,五分钟发公告,半小时银行恢复后一切正常。
B团队的做法:没有结构化日志,只有零散打印。监控弹了告警,但日志全是“开始调用”“调用结束”。不知道哪个用户,不知道哪家银行,甚至不知道是哪个节点超时。几个开发开始手忙脚乱地复现,等到最终定位到问题,已经过去了两个半小时,用户跑了不少。
差距在哪里?不是代码写得多好,而是日志和错误处理让A团队拥有“上帝视角”,B团队只能“盲人摸象”。
金句:你写的99%的功能代码,在线上运行100%正确时,没人会表扬你;但一次严重的线上故障,就因为你漏了一条日志或错误处理不当,却能让你背锅到怀疑人生。
给新手的实战清单:从今天开始养成习惯
如果你现在开始做一个新项目,请牢记这个清单,并把它写进你的开发规范里:
项目初始化时,配置好异步日志、日志格式(含JSON选项),并且调试模式下不要关闭日志文件输出。
定义所有异常类:继承一个自定义基类BaseException,包含errorCode、errorMessage、httpStatus属性。让所有业务异常继承它。
统一全局异常处理:在框架层用@ControllerAdvice或中间件拦截所有异常,统一转化为JSON响应。确保每个异常都被捕获并记录日志。
在关键链路的入口和出口打日志:比如RPC调用、数据库操作、缓存读写。至少记下参数、结果、耗时。
每打印一行日志都要问自己:这条日志能让一个没看过代码的运维在10秒内定位到问题吗?如果不能,改之。
错误处理中必须有重试和熔断机制:对可重试的操作(如网络请求)使用Spring Retry或Hystrix,并配套日志。
设置日志告警:ERROR级别日志超过阈值(比如每分钟5次)立刻通知开发手机。但请确保告警频率合理,避免骚扰。
定期review日志:每月抽一天看看线上产生的日志,分析哪些日志是无效的、哪些错误模式重复出现,然后优化代码。
把你犯过的错误记录成“故障知识库”,包括错误日志截图和解决方案。下次其他人遇到类似问题,直接搜索就能解决。
最后、也是最重要的一点:把“写好日志和错误处理”当做一种肌肉记忆,写每个方法前先想好“这个方法如果出错,我应该怎么打印日志?怎么处理异常?”而不是等写完了再补。
我见过太多新手,业务代码写得飞快,但一到线上环境就抓瞎。而老手的区别在于:他们不用去“想”怎么写日志和错误处理,因为已经条件反射了。这种条件反射,是你从开始每一行代码就训练出来的。
多年后你会发现,让你少加班、少背锅的,不是那些华丽的框架和算法,而是凌晨三点还能清晰定位故障的日志,和那种“无论什么错误,系统都不会崩”的稳健错误处理。所以,别再只盯着业务逻辑了——去爱你的日志和错误处理吧,它们才是你真正的铠甲。