前言:本文章为瑞_系列专栏之《Java开发手册》的异常日志篇,本篇章主要介绍异常日志的错误码、异常处理、日志规约。由于博主是从阿里的《Java开发手册》学习到Java的编程规约,所以本系列专栏主要以这本书进行讲解和拓展,有需要的小伙伴可以点击链接下载。本文仅供大家交流、学习及研究使用,禁止用于商业用途,违者必究!
本系列第一篇链接:(一)编程规约
在Java开发中,异常日志是非常重要的。它记录了程序运行过程中的异常信息,可以帮助开发人员快速定位和解决代码中的问题,提高程序的性能和稳定性。当然了,异常日志也不是万能的。它只能告诉你程序出了什么问题,但没法告诉你为什么会出现这个问题,但有分析方向总比没有强呐。不要把异常日志当成是洪水猛兽,相反,应该把它当成是你的得力助手。当你遇到问题时,让它帮你找出问题的所在;当写完代码时,让它检查可能存在的问题。
异常日志就是我们程序员的“救命稻草”,想象一下,你正在编写一个复杂的程序,突然出现了一个莫名其妙的错误。这时,优秀的异常日志就像是一根救命稻草,可以帮助我们找到问题的根源。没有异常日志,你可能要因为这个bug困扰几个小时甚至❌通宵❌。所以想要保住发际线,就需要我们遵守开发手册中的异常日志规约,养成良好的异常日志编写习惯。
瑞:注意错误码的后三位编号与 HTTP 状态码没有任何关系
瑞:文末有附错误码列表,可供参考
瑞:这种约定属大家公认的约定,不要将其用作其它含义,防止误导他人
瑞:文末附表为阿里的《Java开发手册》泰山版《附表3:错误码列表》,仅供参考
【强制】编号不与公司业务架构,更不与组织架构挂钩,一切与平台先到先申请的原则进行,审批生效,编号即被永久固定。
【强制】错误码使用者避免随意定义新的错误码。
说明:尽可能在原有错误码附表中找到语义相同或者相近的错误码在代码中使用即可。
瑞:CV(复制粘贴)大法好
瑞:尤其是错误信息,经常遇到层层嵌套的情况,建议开发人员不要只测试正例,也要多进行异常值测试
【推荐】错误码之外的业务独特信息由 error_message 来承载,而不是让错误码本身涵盖过多具体业务属性。
【推荐】在获取第三方服务错误码时,向上抛出允许本系统转义,由 C 转为 B,并且在错误信息上带上原有的第三方错误码。
【参考】错误码分为一级宏观错误码、二级宏观错误码、三级宏观错误码。
说明:在无法更加具体确定的错误场景中,可以直接使用一级宏观错误码,分别是:A0001(用户端错误)、B0001(系统执行出错)、C0001(调用第三方服务出错)。
正例:调用第三方服务出错是一级,中间件错误是二级,消息服务出错是三级。
【参考】错误码的后三位编号与 HTTP 状态码没有任何关系。
瑞:常见的 HTTP 状态码可查看Spring框架中的类
org.springframework.http.HttpStatus
,包含 OK(200)、BAD_REQUEST(400)、NOT_FOUND(404)等
【参考】错误码尽量有利于不同文化背景的开发者进行交流与代码协作。
说明:英文单词形式的错误码不利于非英语母语国家(如阿拉伯语、希伯来语、俄罗斯语等)之间的开发者互相协作。
【参考】错误码即人性,感性认知+口口相传,使用纯数字来进行错误码编排不利于感性记忆和分类。
说明:数字是一个整体,每位数字的地位和含义是相同的。
反例:一个五位数字 12345,第 1 位是错误等级,第 2 位是错误来源,345 是编号,人的大脑不会主动地分辨每位数字的不同含义
【强制】Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等。
说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过 catch NumberFormatException 来实现。
正例:if (obj != null) {…}
反例:try { obj.method(); } catch (NullPointerException e) {…}
【强制】异常不要用来做流程控制,条件控制。
说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
瑞:应该使用条件语句(如if-else语句)来实现流程控制和条件控制。条件语句的执行效率比异常处理机制要高得多,因为它们不需要创建新的异常对象、抛出异常、捕获异常等操作。此外,条件语句也更加直观和易于理解,可以更好地表达程序的逻辑关系
瑞:总有大聪明为了省事,直接大段catch然后丢Exception但是要注意别因为遵守本条而违反上一条
瑞:最外层的业务使用者常见为Controller层,即在Controller层要对catch中的error_message进行转义
瑞:CRUD工程师最基本操作
瑞:在JDK7 及以上的版本中,推荐使用 try-with-resources 语句来自动关闭资源对象和流对象。这种方式可以简化代码并确保资源被正确关闭,即使在处理异常时也是如此。如下代码所示:
try (FileInputStream fis = new FileInputStream("input.txt")) {
// 在这里使用 fis 进行文件读取操作
// ...
} catch (IOException e) {
// 处理异常
// ...
}
瑞:❗️ 务必记住本条,巨坑之一❗️
瑞:尽量是完全匹配,无脑抛
Exception
快乐是快乐,但如果出错,异常问题类型无法精准定位
【强制】在调用 RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用 Throwable类来进行拦截。
说明:通过反射机制来调用方法,如果找不到方法,抛出 NoSuchMethodException。什么情况会抛出NoSuchMethodError 呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出 NoSuchMethodError。
【推荐】方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回 null 值。
说明:本手册明确防止 NPE 是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回 null 的情况。
瑞:NPE(java.lang.NullPointerException): 空指针异常。
瑞:真的防止NPE,猿猿有责。不过个人觉得 Optional 类设计有点繁琐,博主一般都是使用条件语句进行判断
【推荐】定义时区分 unchecked / checked 异常,避免直接抛出 new RuntimeException(),更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException 等。
【参考】对于公司外的 http/api 开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间 RPC 调用优先考虑使用 Result 方式,封装 isSuccess()方法、“错误码”、“错误简短信息”;而应用内部推荐异常抛出。
说明:关于 RPC 方法返回方式使用 Result 方式的理由:
1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
2)如果不加栈信息,只是 new 自定义异常,加入自己的理解的 error message,对于调用端解决问题
的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。
【参考】避免出现重复的代码(Don’t Repeat Yourself),即 DRY 原则。
说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。
正例:一个类中有多个 public 方法,都需要进行数行相同的参数校验操作,这个时候请抽取:private boolean checkParam(DTO dto) {…}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Test.class);
使用 JCL:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
private static final Log log = LogFactory.getLog(Test.class);
瑞:吹爆SLF4J,以下为 logback.xml 配置示例
<?xml version="1.0" encoding="UTF-8"?>
<configuration
xmlns="http://ch.qos.logback/xml/ns/logback"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback logback.xsd">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{HH:mm:ss.SSS} [%t] %logger - %m%n</pattern>
</encoder>
</appender>
<logger name="com.ray.yourpackage" level="debug" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>
<root level="ERROR">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
瑞:其实15天都不够,空间条件允许的情况下,建议增加天数,毕竟不怕一万就怕万一
【强制】应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:appName_logType_logName.log。logType:日志类型,如 stats/monitor/access 等;logName:日志描述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。
说明:推荐对日志进行分类,如将错误日志和业务日志分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。
正例:mppserver 应用中单独监控时区转换异常,如:mppserver_monitor_timeZoneConvert.log
【强制】在日志输出时,字符串变量之间的拼接使用占位符的方式。
说明:因为 String 字符串的拼接会使用 StringBuilder 的 append()方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。
正例:logger.debug(“Processing trade with id: {} and symbol: {}”, id, symbol);
【强制】对于 trace/debug/info 级别的日志输出,必须进行日志级别的开关判断。
说明:虽然在 debug(参数)的方法体内第一行代码 isDisabled(Level.DEBUG_INT)为真时(Slf4j 的常见实现Log4j 和 Logback),就直接 return,但是参数可能会进行字符串拼接运算。此外,如果 debug(getName())这种参数内有 getName()方法调用,无谓浪费方法调用的开销。
正例:
// 如果判断为真,那么可以输出 trace 和 debug 级别的日志
if (logger.isDebugEnabled()) {
logger.debug("Current ID is: {} and name is: {}", id, getName());
}
瑞:可参考本小节第一条,土豪请随意
瑞:不仅仅是在生产环境,开发环境也建议少用这两条输出指令,很多时候出问题就是由于某个死循环中存在这两条语句
【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字 throws 往上抛出。
正例:logger.error(各类参数或者对象 toString() + “_” + e.getMessage(), e);
【强制】日志打印时禁止直接用 JSON 工具将对象转换成 String。
说明:如果对象里某些 get 方法被重写,存在抛出异常的情况,则可能会因为打印日志而影响正常业务流程的执行。
正例:打印日志时仅打印出业务相关属性值或者调用其对象的 toString()方法。
【推荐】谨慎地记录日志。生产环境禁止输出 debug 日志;有选择地输出 info 日志;如果使用warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。
说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
【推荐】可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。如非必要,请不要在此场景打出 error 级别,避免频繁报警。
说明:注意日志输出的级别,error 级别只记录系统逻辑出错、异常或者重要的错误信息。
【推荐】尽量用英文来描述日志错误信息,如果日志中的错误信息用英文描述不清楚的话使用中文描述即可,否则容易产生歧义。
说明:国际化团队或海外部署的服务器由于字符集问题,使用全英文来注释和描述日志错误信息。
以下为阿里的《Java开发手册》泰山版《附表3:错误码列表》
错误码 | 中文描述 | 说明 |
---|---|---|
00000 | 一切 ok | 正确执行后的返回 |
A0001 | 用户端错误 | 一级宏观错误码 |
A0100 | 用户注册错误 | 二级宏观错误码 |
A0101 | 用户未同意隐私协议 | |
A0102 | 注册国家或地区受限 | |
A0110 | 用户名校验失败 | |
A0111 | 用户名已存在 | |
A0112 | 用户名包含敏感词 | |
A0113 | 用户名包含特殊字符 | |
A0120 | 密码校验失败 | |
A0121 | 密码长度不够 | |
A0122 | 密码强度不够 | |
A0130 | 校验码输入错误 | |
A0131 | 短信校验码输入错误 | |
A0132 | 邮件校验码输入错误 | |
A0133 | 语音校验码输入错误 | |
A0140 | 用户证件异常 | |
A0141 | 用户证件类型未选择 | |
A0142 | 大陆身份证编号校验非法 | |
A0143 | 护照编号校验非法 | |
A0144 | 军官证编号校验非法 | |
A0150 | 用户基本信息校验失败 | |
A0151 | 手机格式校验失败 | |
A0152 | 地址格式校验失败 | |
A0153 | 邮箱格式校验失败 | |
A0200 | 用户登陆异常 | 二级宏观错误码 |
A0201 | 用户账户不存在 | |
A0202 | 用户账户被冻结 | |
A0203 | 用户账户已作废 | |
A0210 | 用户密码错误 | |
A0211 | 用户输入密码次数超限 | |
A0220 | 用户身份校验失败 | |
A0221 | 用户指纹识别失败 | |
A0222 | 用户面容识别失败 | |
A0223 | 用户未获得第三方登陆授权 | |
A0230 | 用户登陆已过期 | |
A0240 | 用户验证码错误 | |
A0241 | 用户验证码尝试次数超限 | |
A0300 | 访问权限异常 | 二级宏观错误码 |
A0301 | 访问未授权 | |
A0302 | 正在授权中 | |
A0303 | 用户授权申请被拒绝 | |
A0310 | 因访问对象隐私设置被拦截 | |
A0311 | 授权已过期 | |
A0312 | 无权限使用 API | |
A0320 | 用户访问被拦截 | |
A0321 | 黑名单用户 | |
A0322 | 账号被冻结 | |
A0323 | 非法 IP 地址 | |
A0324 | 网关访问受限 | |
A0325 | 地域黑名单 | |
A0330 | 服务已欠费 | |
A0340 | 用户签名异常 | |
A0341 | RSA 签名错误 | |
A0400 | 用户请求参数错误 | 二级宏观错误码 |
A0401 | 包含非法恶意跳转链接 | |
A0402 | 无效的用户输入 | |
A0410 | 请求必填参数为空 | |
A0411 | 用户订单号为空 | |
A0412 | 订购数量为空 | |
A0413 | 缺少时间戳参数 | |
A0414 | 非法的时间戳参数 | |
A0420 | 请求参数值超出允许的范围 | |
A0421 | 参数格式不匹配 | |
A0422 | 地址不在服务范围 | |
A0423 | 时间不在服务范围 | |
A0424 | 金额超出限制 | |
A0425 | 数量超出限制 | |
A0426 | 请求批量处理总个数超出限制 | |
A0427 | 请求 JSON 解析失败 | |
A0430 | 用户输入内容非法 | |
A0431 | 包含违禁敏感词 | |
A0432 | 图片包含违禁信息 | |
A0433 | 文件侵犯版权 | |
A0440 | 用户操作异常 | |
A0441 | 用户支付超时 | |
A0442 | 确认订单超时 | |
A0443 | 订单已关闭 | |
A0500 | 用户请求服务异常 | 二级宏观错误码 |
A0501 | 请求次数超出限制 | |
A0502 | 请求并发数超出限制 | |
A0503 | 用户操作请等待 | |
A0504 | WebSocket 连接异常 | |
A0505 | WebSocket 连接断开 | |
A0506 | 用户重复请求 | |
A0600 | 用户资源异常 | 二级宏观错误码 |
A0601 | 账户余额不足 | |
A0602 | 用户磁盘空间不足 | |
A0603 | 用户内存空间不足 | |
A0604 | 用户 OSS 容量不足 | |
A0605 | 用户配额已用光 | 蚂蚁森林浇水数或每天抽奖数 |
A0700 | 用户上传文件异常 | 二级宏观错误码 |
A0701 | 用户上传文件类型不匹配 | |
A0702 | 用户上传文件太大 | |
A0703 | 用户上传图片太大 | |
A0704 | 用户上传视频太大 | |
A0705 | 用户上传压缩文件太大 | |
A0800 | 用户当前版本异常 | 二级宏观错误码 |
A0801 | 用户安装版本与系统不匹配 | |
A0802 | 用户安装版本过低 | |
A0803 | 用户安装版本过高 | |
A0804 | 用户安装版本已过期 | |
A0805 | 用户 API 请求版本不匹配 | |
A0806 | 用户 API 请求版本过高 | |
A0807 | 用户 API 请求版本过低 | |
A0900 | 用户隐私未授权 | 二级宏观错误码 |
A0901 | 用户隐私未签署 | |
A0902 | 用户摄像头未授权 | |
A0903 | 用户相机未授权 | |
A0904 | 用户图片库未授权 | |
A0905 | 用户文件未授权 | |
A0906 | 用户位置信息未授权 | |
A0907 | 用户通讯录未授权 | |
A1000 | 用户设备异常 | 二级宏观错误码 |
A1001 | 用户相机异常 | |
A1002 | 用户麦克风异常 | |
A1003 | 用户听筒异常 | |
A1004 | 用户扬声器异常 | |
A1005 | 用户 GPS 定位异常 | |
B0001 | 系统执行出错 | 一级宏观错误码 |
B0100 | 系统执行超时 | 二级宏观错误码 |
B0101 | 系统订单处理超时 | |
B0200 | 系统容灾功能被触发 | 二级宏观错误码 |
B0210 | 系统限流 | |
B0220 | 系统功能降级 | |
B0300 | 系统资源异常 | 二级宏观错误码 |
B0310 | 系统资源耗尽 | |
B0311 | 系统磁盘空间耗尽 | |
B0312 | 系统内存耗尽 | |
B0313 | 文件句柄耗尽 | |
B0314 | 系统连接池耗尽 | |
B0315 | 系统线程池耗尽 | |
B0320 | 系统资源访问异常 | |
B0321 | 系统读取磁盘文件失败 | |
C0001 | 调用第三方服务出错 | 一级宏观错误码 |
C0100 | 中间件服务出错 | 二级宏观错误码 |
C0110 | RPC 服务出错 | |
C0111 | RPC 服务未找到 | |
C0112 | RPC 服务未注册 | |
C0113 | 接口不存在 | |
C0120 | 消息服务出错 | |
C0121 | 消息投递出错 | |
C0122 | 消息消费出错 | |
C0123 | 消息订阅出错 | |
C0124 | 消息分组未 | |
C0130 | 缓存服务出错 | |
C0131 | key 长度超过限制 | |
C0132 | value 长度超过限制 | |
C0133 | 存储容量已满 | |
C0134 | 不支持的数据格式 | |
C0140 | 配置服务出错 | |
C0150 | 网络资源服务出错 | |
C0151 | VPN 服务出错 | |
C0152 | CDN 服务出错 | |
C0153 | 域名解析服务出错 | |
C0154 | 网关服务出错 | |
C0200 | 第三方系统执行超时 | 二级宏观错误码 |
C0210 | RPC 执行超时 | |
C0220 | 消息投递超时 | |
C0230 | 缓存服务超时 | |
C0240 | 配置服务超时 | |
C0250 | 数据库服务超时 | |
C0300 | 数据库服务出错 | 二级宏观错误码 |
C0311 | 表不存在 | |
C0312 | 列不存在 | |
C0321 | 多表关联中存在多个相同名称的列 | |
C0331 | 数据库死锁 | |
C0341 | 主键冲突 | |
C0400 | 第三方容灾系统被触发 | 二级宏观错误码 |
C0401 | 第三方系统限流 | |
C0402 | 第三方功能降级 | |
C0500 | 通知服务出错 | 二级宏观错误码 |
C0501 | 短信提醒服务失败 | |
C0502 | 语音提醒服务失败 | |
C0503 | 邮件提醒服务失败 |
如果觉得这篇文章对您有所帮助的话,请动动小手点波关注,你的点赞收藏⭐️转发评论都是对博主最好的支持~