Java开发⼿册⼆(异常⽇志)
(⼀)错误码
1.【强制】错误码制定原则:快速溯源、沟通标准化
说明:错误码想得太过于完美和复杂,就像康熙字典中的⽣僻字⼀样,⽤词似乎精准,但是字典不容易随⾝携带并且简单易懂
正例:错误码回答的问题是谁的错?错在哪?1)错误码必须能够快速知晓错误来源,可快速判断是谁的问题。2)错误码必须能够进⾏清晰地⽐对(代码中容易equals)。3)错误码有利于团队快速对错误原因达到⼀致认知
2.【强制】错误码不体现版本号和错误等级信息
说明:错误码以不断追加的⽅式进⾏兼容。错误等级由⽇志和错误码本⾝的释义来决定
3.【强制】全部正常,但不得不填充错误码时返回五个零:00000
4.【强制】错误码为字符串类型,共5位,分成两个部分:错误产⽣来源+四位数字编号
说明:错误产⽣来源分为A/B/C,A表⽰错误来源于⽤户,⽐如参数错误,⽤户安装版本过低,⽤户⽀付超时等问题;B表⽰错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题;C表⽰错误来源于第三⽅服务,⽐如CDN服务出错,消息投递超时等问题;四位数字编号从0001到9999,⼤类之间的步长间距预留100
5.【强制】编号不与公司业务架构,更不与组织架构挂钩,以先到先得的原则在统⼀平台上进⾏,审批⽣效,编号即被永久固定
6.【强制】错误码使⽤者避免随意定义新的错误码
说明:尽可能在原有错误码附表中到语义相同或者相近的错误码在代码中使⽤即可
7.【强制】错误码不能直接输出给⽤户作为提⽰信息使⽤
说明:堆栈(stack_trace)、错误信息(error_message)、错误码(error_code)、提⽰信息(user_tip)是⼀个有效关联并互相转义的和谐整体,但是请勿互相越俎代庖
8.【推荐】错误码之外的业务独特信息由error_message来承载,⽽不是让错误码本⾝涵盖过多具体业务属性
9.【推荐】在获取第三⽅服务错误码时,向上抛出允许本系统转义,由C转为B,并且在错误信息上带上原有的第三⽅错误码
10.【参考】错误码分为⼀级宏观错误码、⼆级宏观错误码、三级宏观错误码
说明:在⽆法更加具体确定的错误场景中,可以直接使⽤⼀级宏观错误码,分别是:A0001(⽤户端错误)、B0001(系统执⾏出错)、C0001(调⽤第三⽅服务出错)
正例:调⽤第三⽅服务出错是⼀级,中间件错误是⼆级,消息服务出错是三级
11.【参考】错误码的后三位编号与HTTP状态码没有任何关系
12.【参考】错误码有利于不同⽂化背景的开发者进⾏交流与代码协作
说明:英⽂单词形式的错误码不利于⾮英语母语国家(如阿拉伯语、希伯来语、俄罗斯语等)之间的开发者互相协作
13.【参考】错误码即⼈性,感性认知+⼝⼝相传,使⽤纯数字来进⾏错误码编排不利于感性记忆和分类
说明:数字是⼀个整体,每位数字的地位和含义是相同的。
反例:⼀个五位数字12345,第1位是错误等级,第2位是错误来源,345是编号,⼈的⼤脑不会主动地拆开并分辨每位数字的不同含义
(⼆)异常处理
1.【强制】Java 类库中定义的可以通过预检查⽅式规避的RuntimeException异常不应该通过catch的⽅式来处理,⽐如:NullPointerException,IndexOutOfBoundsException等等
说明:⽆法通过预检查的异常除外,⽐如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过
catchNumberFormatException来实现
正例:if (obj != null) {...}
反例:try { hod(); } catch (NullPointerException e) {...}
2.【强制】异常捕获后不要⽤来做流程控制,条件控制
说明:异常设计的初衷是解决程序运⾏中的各种意外情况,且异常的处理效率⽐条件判断⽅式要低很多
3.【强制】catch时请分清稳定代码和⾮稳定代码,稳定代码指的是⽆论如何不会出错的代码。对于⾮稳定代码的catch尽可能进⾏区分异常类型,再做对应的异常处理
说明:对⼤段代码进⾏try-catch,使程序⽆法根据不同的异常做出正确的应激反应,也不利于定位问题,这是⼀种不负责任的表现
正例:⽤户注册的场景中,如果⽤户输⼊⾮法字符,或⽤户名称已存在,或⽤户输⼊密码过于简单,在程序上作出分门别类的判断,并提⽰给⽤户
4.【强制】捕获异常是为了处理它,不要捕获了却什么都不处理⽽抛弃之,如果不想处理它,请将该异常抛给它的调⽤者。最外层的业务使⽤者,必须处理异常,将其转化为⽤户可以理解的内容
5.【强制】事务场景中,抛出异常被catch后,如果需要回滚,⼀定要注意⼿动回滚事务
6.【强制】finally块必须对资源对象、流对象进⾏关闭,有异常也要做try-catch
说明:如果JDK7及以上,可以使⽤try-with-resources⽅式
7.【强制】不要在finally块中使⽤return
说明:try块中的return语句执⾏成功后,并不马上返回,⽽是继续执⾏finally块中的语句,如果此处存在return语句,则在此直接返回,⽆情丢弃掉try块中的返回点。
反例:
privateintx =0;
public int checkReturn(){
try{
// x等于1,此处不返回
return ++x;
} finally {
// 返回的结果是2
return++x;
}
}
8.【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的⽗类
说明:如果预期对⽅抛的是绣球,实际接到的是铅球,就会产⽣意外情况
9.【强制】在调⽤RPC、⼆⽅包、或动态⽣成类的相关⽅法时,捕捉异常必须使⽤Throwable类来进⾏拦截
说明:通过反射机制来调⽤⽅法,如果不到⽅法,抛出NoSuchMethodException。什么情况会抛出NoSuchMethodError呢?⼆⽅包在类冲突时,仲裁机制可能导致引⼊⾮预期的版本使类的⽅法签名不匹配,或者在字节码修改框架(⽐如:ASM)动态创建或修改类时,修改了相应的⽅法签名。这些情况,即使代码编译期是正确的,但在代码运⾏期时,会抛出NoSuchMethodError
10.【推荐】⽅法的返回值可以为null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回null值
说明:本⼿册明确防⽌NPE是调⽤者的责任。即使被调⽤⽅法返回空集合或者空对象,对调⽤者来说,也并⾮⾼枕⽆忧,必须考虑到远程调⽤失败、序列化失败、运⾏时异常等场景返回null的情况
11.【推荐】防⽌NPE,是程序员的基本修养,注意NPE产⽣的场景:
1)返回类型为基本数据类型,return包装数据类型的对象时,⾃动拆箱有可能产⽣NPE
反例:public int f() { return Integer对象},如果为null,⾃动解箱抛NPE
2)数据库的查询结果可能为null。
3)集合⾥的元素即使isNotEmpty,取出的数据元素也可能为null
4)远程调⽤返回对象时,⼀律要求进⾏空指针判断,防⽌NPE
5)对于Session中获取的数据,建议进⾏NPE检查,避免空指针
6)级联调⽤A().getB().getC();⼀连串调⽤,易产⽣NPE
正例:使⽤JDK8的Optional类来防⽌NPE问题
12.【推荐】定义时区分unchecked / checked 异常,避免直接抛出new RuntimeException(),更不允许抛出Exception或者Throwable,应使⽤有业务含义的⾃定义异常。推荐业界已定义过的⾃定义异常,如:DAOException / ServiceException等。
13.【参考】对于公司外的http/api开放接⼝必须使⽤errorCode;⽽应⽤内部推荐异常抛出;跨应⽤间R
PC调⽤优先考虑使⽤Result⽅式,封装isSuccess()⽅法、errorCode、errorMessage;⽽应⽤内部直接抛出异常即可。
说明:关于RPC⽅法返回⽅式使⽤Result⽅式的理由:
1)使⽤抛异常返回⽅式,调⽤⽅如果没有捕获到就会产⽣运⾏时错误。
2)如果不加栈信息,只是new⾃定义异常,加⼊⾃⼰的理解的error message,对于调⽤端解决问题的帮助不会太多。如果加了栈信息,在频繁调⽤出错的情况下,数据序列化和传输的性能损耗也是问题。
(三)⽇志规约
1.【强制】应⽤中不可直接使⽤⽇志系统(Log4j、Logback)中的API,⽽应依赖使⽤⽇志框架(SLF4J、JCL--Jakarta Commons Logging)中的API,使⽤门⾯模式的⽇志框架,有利于维护和各个类的⽇志处理⽅式统⼀。
说明:⽇志框架(SLF4J、JCL--Jakarta Commons Logging)的使⽤⽅式(推荐使⽤SLF4J)
使⽤SLF4J:
import org.slf4j.Logger;
importorg.slf4j.LoggerFactory;
private static final Logger logger = Logger(Test.class);
使⽤JCL:
import org.apachemons.logging.Log;
importorg.apachemons.logging.LogFactory;
private static final Log log =Log(Test.class);
2.【强制】所有⽇志⽂件⾄少保存15天,因为有些异常具备以“周”为频次发⽣的特点。对于当天⽇志,以“应⽤名.log”来保存,保存
session数据错误是什么意思在/home/admin/应⽤名/logs/⽬录下,过往⽇志格式为: {logname}.log.{保存⽇期},⽇期格式:yyyy-MM-dd
正例:以aap应⽤为例,⽇志保存在/home/admin/aapserver/logs/aap.log,历史⽇志名称为aap.log.2016-08-01
3.【强制】根据国家法律,⽹络运⾏状态、⽹络安全事件、个⼈敏感信息操作等相关记录,留存的⽇志不少于六个⽉,并且进⾏⽹络多机备份。
4.【强制】应⽤中的扩展⽇志(如打点、临时监控、访问⽇志等)命名⽅式:appName_logType_logName.log。logType:⽇志类型,如stats/monitor/access等;logName:⽇志描述。这种命名的好处:通过⽂件名就可知道⽇志⽂件属于什么应⽤,什么类型,什么⽬的,也有利于归类查
说明:推荐对⽇志进⾏分类,如将错误⽇志和业务⽇志分开存放,便于开发⼈员查看,也便于通过⽇志对系统进⾏及时监控
正例:mppserver应⽤中单独监控时区转换异常,如:mppserver_monitor_timeZoneConvert.log
5.【强制】在⽇志输出时,字符串变量之间的拼接使⽤占位符的⽅式。
说明:因为String字符串的拼接会使⽤StringBuilder的append()⽅式,有⼀定的性能损耗。使⽤占位符仅是替换动作,可以有效提升性能
正例:logger.debug("Processingtradewithid:{}andsymbol:{}",id,symbol);
6.【强制】对于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());
}
7.【强制】避免重复打印⽇志,浪费磁盘空间,务必在⽇志配置⽂件中设置additivity=false
正例:<logger name="com.fig" additivity="false">
8.【强制】⽣产环境禁⽌直接使⽤System.out 或 输出⽇志或使⽤e.printStackTrace()打印异常堆栈。
说明:标准⽇志输出与标准错误输出⽂件每次Jboss重启时才滚动,如果⼤量输出送往这两个⽂件,容易造成⽂件⼤⼩超过操作系统⼤⼩限制
9.【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字throws往上抛出
正例:("inputParams:{} and errorMessage:{}", 各类参数或者对象toString(), e.getMessage(), e);
10.【强制】⽇志打印时禁⽌直接⽤JSON⼯具将对象转换成String。
说明:如果对象⾥某些get⽅法被覆写,存在抛出异常的情况,则可能会因为打印⽇志⽽影响正常业务流程的执⾏
正例:打印⽇志时仅打印出业务相关属性值或者调⽤其对象的toString()⽅法。
11.【推荐】谨慎地记录⽇志。⽣产环境禁⽌输出debug⽇志;有选择地输出info⽇志;如果使⽤warn来记录刚上线时的业务⾏为信息,⼀定要注意⽇志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察⽇志。
说明:⼤量地输出⽆效⽇志,不利于系统性能提升,也不利于快速定位错误点。记录⽇志时请思考:这些⽇志真的有⼈看吗?看到这条⽇志你能做什么?能不能给问题排查带来好处?
12.【推荐】可以使⽤warn⽇志级别来记录⽤户输⼊参数错误的情况,避免⽤户投诉时,⽆所适从。如⾮必要,请不要在此场景打出error 级别,避免频繁报警。
说明:注意⽇志输出的级别,error级别只记录系统逻辑出错、异常或者重要的错误信息。
13.【推荐】尽量⽤英⽂来描述⽇志错误信息,如果⽇志中的错误信息⽤英⽂描述不清楚的话使⽤中⽂描述即可,否则容易产⽣歧义。
说明:国际化团队或海外部署的服务器由于字符集问题,使⽤全英⽂来注释和描述⽇志错误信息。
认真看多⼀遍,代码规范也不难

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。