阿里巴巴开发手册(二)异常日志个人解析

文章目录

异常日志

1. 【强制】 Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过

catch 的方式来处理,比如: NullPointerException, IndexOutOfBoundsException 等等。
说明: 无法通过预检查的异常除外,比如,在解析字符串形式的数字时,不得不通过 catch
NumberFormatException 来实现。
正例: if (obj != null) {…}
反例: try { obj.method(); } catch (NullPointerException e) {…}

为什么呢?其中一个比较重要的原因是,异常比较损耗性能,当Java捕捉到异常的时候,抛出异常时是找异常库里寻找异常,再抛去到catch里的
摘自:https://stackoverflow.com/questions/19765400/how-does-try-catch-work-in-details

"How does it know that the called function is "surrounded" with the try/catch block?"

The code of each method contains Exception Table which describes all try-catch blocks of that method

When a procedure (function, method) is called, the current stack frame is appended with the address of the calling 
instruction, so as to restore execution of that frame at the correct instruction (next after calling instruction).

When a throw statement is executed, the JVM examines each stack frame to find out if that frame can handle the exception. It can if its method contains a try-catch block which contains the calling instruction, and the type of block's 
exception is a supertype (or the same as) of the thrown exception. If such a frame is found, the frame restores its execution from the instruction pointed to from the try-catch block.

2. 【强制】异常不要用来做流程控制,条件控制。

说明: 异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式
要低很多

为什么?

跟上一条同理,寻找异常效率是比较慢的

3. 【强制】 catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。

对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。
说明: 对大段代码进行 try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利
于定位问题,这是一种不负责任的表现。
正例: 用户注册的场景中,如果用户输入非法字符, 或用户名称已存在, 或用户输入密码过于
简单,在程序上作出分门别类的判断,并提示给用户。

为什么?
这是为了易读性而做的妥协,每一个都有可能出错的话,线上定位问题要话费很多精力去猜想每一行的可能,是比较耗费时间的

4. 【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请

将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的
内容。

反例

try{
   int[] a = new int[3]
   a[4]=1 ;//溢出错误
}catch ( Exception e){
  e.printStackTrace()
}

这样子不处理又不抛给上一级,线上环境无法调试代码,导致定位问题困难

5. 【强制】有 try 块放到了事务代码中, catch 异常后,如果需要回滚事务,一定要注意手动回滚事务。

这个也没有为什么

6. 【强制】 finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。

说明: 如果 JDK7 及以上,可以使用 try-with-resources 方式。
try-with-resources 的解析
不使用try with-resources 直接try cath 的例子

FileOutputStream outStream = null;
try {
  outStream = new FileOutputStream("people.bin");
  ObjectOutputStream stream = new ObjectOutputStream(outStream);

  stream.writeObject(jar);
  stream.writeObject(can);

  stream.close();
} catch(FileNotFoundException e) {
    System.out.println("sorry it didn't work out");
} catch(IOException f) {
    System.out.println("sorry it didn't work out");
} finally {
  if (outStream != null) { 
    try { 
      outStream.close(); 
    } catch (Exception e) {
    } 
  }
}

与try-with-resources 等价,try-with-resources里try(条件) 条件里是交由JVM去办我们关闭。

try (FileOutputStream outStream = new FileOutputStream("people.bin");
     ObjectOutputStream stream = new ObjectOutputStream(outStream);) {
  stream.writeObject(jar);
  stream.writeObject(can);
  // stream.close(); // <-- closed by try-with-resources.
} catch(FileNotFoundException e) {
    System.out.println("sorry it didn't work out");
    e.printStackTrace();
} catch(IOException f) {
    System.out.println("sorry it didn't work out");
    e.printStackTrace();
}

7. 【强制】不要在 finally 块中使用 return。

说明: finally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句
try catch 必定会运行 finally 语句,所以finally的return会覆盖 try快

public class Test {

	public static void main(String[] args) {
		String a= methodTest();
		System.out.println(a);
	}

	private static String methodTest() {
		try {
			return "我是try块";
		} catch (Exception e) {
			// TODO: handle exception
		}
		finally{
			return "我是finally块";
		}
	}

}

8. 【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。

说明: 如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。捕捉的异常

public class Test {

	public static void main(String[] args) {
		String a= methodTest();
		System.out.println(a);
	}

	private static String methodTest() {
		try {
			throw new NullPointerException("我是Nullpointer");
		} catch (ArrayIndexOutOfBoundsException e) {
			System.out.println("捕捉到了数组溢出异常");
		}
		return null;
		
	}

}

这里没有 捕捉到Exception 导致这个异常漏掉,会报出nullpointer异常,在线上的时候会报出500页面,体验不好

9. 【推荐】方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分

说明什么情况下会返回 null 值。
说明: 本手册明确防止 NPE 是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、 序列化失败、 运行时异常等场景返回null 的情况。

9.1讨论为什么可以返回null值,返回一个非null对象不好吗?

10. 【推荐】防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:

1)box 返回类型为基本数据类型, return 包装数据类型的对象时,自动拆箱有可能产生 NPE。
反例: public int f() { return Integer 对象}, 如果为 null,自动解箱抛 NPE。
2) database 数据库的查询结果可能为 null。
3) collections 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
4) foreign(get from http)远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
5) session对于 Session 中获取的数据,建议 NPE 检查,避免空指针。
6) attribute 级联调用 obj.getA().getB().getC(); 一连串调用,易产生 NPE。
正例: 使用 JDK8 的 Optional 类来防止 NPE 问题

10.1 ABCDEF记忆

(object,session)attribute --> box —> collections -->database -->(collection element)element–>foreign
对象,session的属性,自动拆箱装箱(box),集合与(element)的元素,数据库get出来的对象,外部接口返回
Optional personOpt = Optional.ofNullable(person);

11. 【推荐】定义时区分 unchecked / checked 异常,避免直接抛出 new RuntimeException()

更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常。推荐业界已定义
过的自定义异常,如: DAOException / ServiceException 等。

自定义异常要有意义的才更好的理解,不要一大堆的自定义对象

12. 【参考】 对于公司外的 http/api 开放接口必须使用“错误码”; 而应用内部推荐异常抛出;

跨应用间 RPC 调用优先考虑使用 Result 方式,封装 isSuccess()方法、 “错误码”、 “错误简
短信息”。
说明: 关于 RPC 方法返回方式使用 Result 方式的理由:
1) 使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
2) 如果不加栈信息,只是 new 自定义异常,加入自己的理解的 error message,对于调用
端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输
的性能损耗也是问题。
一个Result的例子

public class Result<T> implements Serializable {
	/**
	 * 接口调用成功,不需要返回对象
	 */
	public static <T> Result<T> newSuccess(){
		Result<T> result = new Result<>();
		return result;
	}
	
	/**
	 * 接口调用成功,有返回对象
	 */
	public static <T> Result<T> newSuccess(T object) {
		Result<T> result = new Result<>();
		result.setObject(object);
		return result;
	}
	
	/**
	 * 接口调用失败,有错误码和描述,没有返回对象
	 */
	public static <T> Result<T> newFailure(int code, String message){
		Result<T> result = new Result<>();
		result.setCode(code!=0 ? code : -1);
		result.setMessage(message);
		return result;
	}
	
	/**
	 * 接口调用失败,有错误字符串码和描述,没有返回对象
	 */
	public static <T> Result<T> newFailure(String error, String message){
		Result<T> result = new Result<>();
		result.setCode(-1);
		result.setError(error);
		result.setMessage(message);
		return result;
	}
	
	/**
	 * 转换或复制错误结果
	 */
	public static <T> Result<T> newFailure(Result<?> failure){
		Result<T> result = new Result<>();
		result.setCode(failure.getCode()!=0 ? failure.getCode() : -1);
		result.setError(failure.getError());
		result.setMessage(failure.getMessage());
		result.setException(failure.getException());
		return result;
	}
	
	/**
	 * 接口调用失败,返回异常信息
	 */
	public static <T> Result<T> newException(Exception e){
		Result<T> result = new Result<>();
		result.setCode(-1);
		result.setException(e);
		result.setMessage(e.getMessage());
		return result;
	}
	
	private int code;
	private T object;
	private String error;
	private String message;
	private Exception exception;
	
	/** 判断返回结果是否成功 */
	public boolean success() {
		return code == 0;
	}
	/** 判断返回结果是否有结果对象 */
	public boolean hasObject() {
		return code==0 && object!=null;
	}
	/** 判断返回结果是否有异常 */
	public boolean hasException() {
		return exception != null;
	}
	
	public int getCode() {
		return code;
	}
	public void setCode(int code) {
		this.code = code;
	}
	public T getObject() {
		return object;
	}
	public void setObject(T object) {
		this.object = object;
	}
	public String getError() {
		return error;
	}
	public void setError(String error) {
		this.error = error;
	}
	public String getMessage() {
		return message;
	}
	public void setMessage(String message) {
		this.message = message;
	}
	public Exception getException() {
		return exception;
	}
	public void setException(Exception exception) {
		this.exception = exception;
	}

	public String toString() {
		StringBuilder result = new StringBuilder("Result");
		if(object!=null) result.append("<"+object.getClass().getSimpleName()+">");
		result.append(": {code="+code);
		if(object!=null) result.append(", object="+object);
		if(error!=null) result.append(", error="+error);
		if(message!=null) result.append(", message="+message);
		if(exception!=null) {
			StringWriter stringWriter = new StringWriter();
			exception.printStackTrace(new PrintWriter(stringWriter));
			result.append(", exception="+stringWriter.toString());
		}
		result.append(" }");
		return result.toString();
	}
}

13. 【参考】避免出现重复的代码(Don’t Repeat Yourself) ,即 DRY 原则。

说明: 随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副
本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。
正例: 一个类中有多个 public 方法,都需要进行数行相同的参数校验操作,这个时候请抽取:
private boolean checkParam(DTO dto) {…}

一旦复制粘贴,下次修复bug的时候可能要改多个地方,靠记忆的修改谁都觉的不可靠,这时候如果多个类用到同一个地方,就先随便起一个类名和公共方法,把公共的地方放去,再后面去改这个类名。

13.1为什么要随便起一个类名和方法

,我相信很多人都知道起一个类名是一个很累的事情。而这个很累的事情很经常会阻碍我们抽象出公共代码,所以要随便起一个类名和公共方法

(二)日志规约

1. 【强制】应用中不可直接使用日志系统(Log4j、 Logback) 中的 API,

而应依赖使用日志框架SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
log4j2的api全部变化了,如果不是用slf4j的统一标准接口api,到时候要到处改来改去,影响使用

2. 【强制】日志文件至少保存 15 天,因为有些异常具备以“周”为频次发生的特点。

这个反而我觉的根据定时任务来确定自己的节奏才对

3. 【强制】应用中的扩展日志(如打点、临时监控、访问日志等)

命名方式:
appName_logType_logName.log。
logType:日志类型, 如 stats/monitor/access 等; logName:日志描述。这种命名的好处:
通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。
正例: mppserver 应用中单独监控时区转换异常,如:
mppserver_monitor_timeZoneConvert.log
说明: 推荐对日志进行分类, 如将错误日志和业务日志分开存放,便于开发人员查看,也便于
通过日志对系统进行及时监控

4. 【强制】对 trace/debug/info 级别的日志输出,必须使用条件输出形式或者使用占位符的方式。

说明: logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);
如果日志级别是 warn,上述日志不会打印,但是会执行字符串拼接操作,如果 symbol 是对象,
会执行 toString()方法,浪费了系统资源,执行了上述操作,最终日志却没有打印。
正例: (条件) 建设采用如下方式
if (logger.isDebugEnabled()) {
logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);
}
正例: (占位符)
logger.debug("Processing trade with id: {} and symbol : {} ", id, symbol);

5. 【强制】避免重复打印日志,浪费磁盘空间,务必在 log4j.xml 中设置 additivity=false。

正例:

6. 【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过

关键字 throws 往上抛出。
正例: logger.error(各类参数或者对象 toString() + “_” + e.getMessage(), e);
通常是接口名+参数列表

7. 【推荐】谨慎地记录日志。

生产环境禁止输出 debug 日志; 有选择地输出 info 日志; 如果使
用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘
撑爆,并记得及时删除这些观察日志。
说明: 大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。 记录日志时请
思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
参考https://zhuanlan.zhihu.com/p/27363484

即使是TRACE或者DEBUG级别的日志,也应该有一定的规范,要保证除了开发人员自己以外,包括测试人员和运维人员都可以方便地通过日志定位问题;
对于日志级别的分类,有以下参考:
FATAL — 表示需要立即被处理的系统级错误。当该错误发生时,表示服务已经出现了某种程度的不可用,系统管理员需要立即介入。这属于最严重的日志级别,因此该日志级别必须慎用,如果这种级别的日志经常出现,则该日志也失去了意义。通常情况下,一个进程的生命周期中应该只记录一次FATAL级别的日志,即该进程遇到无法恢复的错误而退出时。当然,如果某个系统的子系统遇到了不可恢复的错误,那该子系统的调用方也可以记入FATAL级别日志,以便通过日志报警提醒系统管理员修复;
ERROR — 该级别的错误也需要马上被处理,但是紧急程度要低于FATAL级别。当ERROR错误发生时,已经影响了用户的正常访问。从该意义上来说,实际上ERROR错误和FATAL错误对用户的影响是相当的。FATAL相当于服务已经挂了,而ERROR相当于好死不如赖活着,然而活着却无法提供正常的服务,只能不断地打印ERROR日志。特别需要注意的是,ERROR和FATAL都属于服务器自己的异常,是需要马上得到人工介入并处理的。而对于用户自己操作不当,如请求参数错误等等,是绝对不应该记为ERROR日志的;
WARN — 该日志表示系统可能出现问题,也可能没有,这种情况如网络的波动等。对于那些目前还不是错误,然而不及时处理也会变为错误的情况,也可以记为WARN日志,例如一个存储系统的磁盘使用量超过阀值,或者系统中某个用户的存储配额快用完等等。对于WARN级别的日志,虽然不需要系统管理员马上处理,也是需要及时查看并处理的。因此此种级别的日志也不应太多,能不打WARN级别的日志,就尽量不要打;
INFO — 该种日志记录系统的正常运行状态,例如某个子系统的初始化,某个请求的成功执行等等。通过查看INFO级别的日志,可以很快地对系统中出现的 WARN,ERROR,FATAL错误进行定位。INFO日志不宜过多,通常情况下,INFO级别的日志应该不大于TRACE日志的10%;
DEBUG or TRACE — 这两种日志具体的规范应该由项目组自己定义,该级别日志的主要作用是对系统每一步的运行状态进行精确的记录。通过该种日志,可以查看某一个操作每一步的执 行过程,可以准确定位是何种操作,何种参数,何种顺序导致了某种错误的发生。可以保证在不重现错误的情况下,也可以通过DEBUG(或TRACE)级别的日志对问题进行诊断。需要注意的是,DEBUG日志也需要规范日志格式,应该保证除了记录日志的开发人员自己外,其他的如运维,测试人员等也可以通过 DEBUG(或TRACE)日志来定位问题;

日志理应在每一个方法 输入输出后进行记录,debug级别时

8. 【推荐】可以使用 warn 日志级别来记录用户输入参数错误的情况。

避免用户投诉时,无所适
从。如非必要,请不要在此场景打出 error 级别,避免频繁报警。
说明: 注意日志输出的级别, error 级别只记录系统逻辑出错、异常或者重要的错误信息。

9. 【推荐】尽量用英文来描述日志错误信息。

如果日志中的错误信息用英文描述不清楚的话使用
中文描述即可,否则容易产生歧义。 国际化团队或海外部署的服务器由于字符集问题,【强制】
使用全英文来注释和描述日志错误信息

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值