今天是孤尽T31训练营第五天,老师讲授了异常处理与日志的知识
强烈推荐孤尽老师写的《Java开发手册》
规约保证健壮性,少出BUG
Java异常体系
使用异常为系统保驾护航
异常应当描述导致当前异常发生的原因
根据异常栈快速定位到异常发生的位置
结合异常描述和异常栈解决异常
很好地处理异常和日志,是企业级项目跟Demo项目的分水岭
C语言的“异常”烦恼
l 代码可读性差
l 当返回值与异常值相近时,容易混淆
l 需要调用方来分析异常,增加多余的工作量
JAVA异常处理流程
没有try,交由JVM默认处理
写了try,但是有些异常没有捕获,会走到兜底的程序代码finally,没有finally处理,交由JVM默认处理
异常全部捕获,仍然会走finally。执行完毕,finally中有异常,且没有合理的捕获处理。交由JVM默认处理
异常没有对等捕获或真实的处理,导致finally中仍有异常,程序中埋了雷
合理处理异常,程序少埋雷,review代码需要关注
避免漏网之鱼,对后续程序产生影响
异常处理
捕获异常:不想/没能力处理向上抛出;
抛出异常比较少,同样的异常出现多次,可以在方法定义时声明异常
将方法的正确返回和异常返回分开处理,正确返回用return,异常返回用throw
try抛出多种异常,如何捕获
共同父类异常,处理流程一致:可以捕获一次父类异常
共同父类异常,处理流程不一致:加多个catch,低层异常先抛出,再提升异常等级()
不同父类异常,处理流程一致:用 或 连接多个异常
不同父类异常,处理流程不一致:对等catch处理
枝繁叶茂的JAVA异常体系
谨慎使用Throwable,合理地使用能避免错误和异常直接露出
【强制】在调用 RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用 Throwable类来进行拦截。
使用不当时,捕获Error无法处理,错误地使用会导致程序终止
Error:致命错误,不可控不可处理
JVM异常,堆栈溢出、内存溢出
Exception:不致命,能自动恢复
非受检异常:可控范围 RuntimeException Unchecked异常
可预测异常、需捕获异常、可透出异常
受检异常:可控范围外,可提前计划处理 Checked异常
引起注意型、坦然处置型
受检怎么理解
针对Java编译器来说,编译时会提示的就是 受检异常
checked异常,比如IO异常,数据库异常等。需要提前检查,不做处理编译无法通过
编译器一般都有自动提醒,需要你处理,try 或者 throws
异常处理设计与实践
异常抛出与捕获的原则
try…catch…finally
try with resource(JDK7)
特殊异常NPE场景及其处理对策
异常抛出与捕获的原则**
非必要不使用异常
区分出稳定性代码,不放在try中
误区:天王盖地虎,一个方法一个try。但是可能发生多个异常,但只捕获一个
造成异常向上抛出,导致程序终止。
稳定代码要养成习惯不去catch。因为catch就需要MONITER ERNTER和monitor exit, 不需要这样无谓的成本
异常创建和抛出消耗性能,使用条件判断解决节省性能。
写代码需要自信的态度。形式的全覆盖,并无必要
【强制】catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。
对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。
说明:对大段代码进行 try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。
正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。
使用描述性消息抛出异常
封装异常上下文参数(操作、特殊指数值、局部变量、运行时间、环境…)
力所能及的异常一定要处理
本层能处理的异常不要抛给上层处理,本层(服务)最了解该怎样处理该异常
上层依赖调用方不知道如何处理或不恰当
异常忽略要有理有据
重视异常可以捕获的信息
捕获异常之后,catch块什么也没有写,异常可以捕获的信息被忽略了
跟没捕获区别不大,异常造成程序的终止,对异常解决没有益处
日志规约
• 日志的功能
使用日志提高系统运营与维护的能力
监控告警:
健康检查、指标监控(告警)
记录行为轨迹:指标监控(查询)、链路追踪
快速定位问题:日志
• 日志时效规约
当天日志命名:以“应用名.log”来保存
过往日志命名:以{logname}.log.{保存日期}命名,日期格式yyyy-MM-dd
对于当天日志,以“应用名.log”来保存,保存在/home/admin/应用名/logs/目录下,过往日志
格式为: {logname}.log.{保存日期},日期格式:yyyy-MM-dd
正例:以 aap 应用为例,日志保存在/home/admin/aapserver/logs/aap.log,历史日志名称为aap.log.2016-08-01
日志文件至少保存15天:便于排查某些以“周”为频次发生的异常
【强制】所有日志文件至少保存 15 天,因为有些异常具备以“周”为频次发生的特点。
敏感操作信息联机存储6个月:网络安全相关法律的规定。要多机备份,最好使用warn日志级别输出
【强制】根据国家法律,网络运行状态、网络安全事件、个人敏感信息操作等相关记录,留存的日志不少于六个月,并且进行网络多机备份。
发生纠纷打官司能用
记录用户敏感信息或者操作的日志,并多机备份?该类日志最好使用
对这类敏感信息,最好使用WARN日志级别输出
不要使用ERROR级别:ERROR会有一个独特的处理机制——即时通知机制
• 日志记录规约
- 系统应依赖使用日志框架(SLF4J、JCL)的API而不是具体日志库中的
- 在日志输出时,字符串变量之间的拼接使用占位符的方式
- 日志打印时禁止直接使用JSON工具将对象转换成String
- 尽量用英语来描述日志错误信息
【强制】应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架(SLF4J、JCL--Jakarta Commons Logging)中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
【强制】在日志输出时,字符串变量之间的拼接使用占位符的方式。
说明:因为 String 字符串的拼接会使用 StringBuilder 的 append()方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。
正例:logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);
【强制】日志打印时禁止直接用 JSON 工具将对象转换成 String。
说明:如果对象里某些 get 方法被覆写,存在抛出异常的情况,则可能会因为打印日志而影响正常业务流程的执行。
正例:打印日志时仅打印出业务相关属性值或者调用其对象的 toString()方法。
【推荐】尽量用英文来描述日志错误信息,如果日志中的错误信息用英文描述不清楚的话使用
中文描述即可,否则容易产生歧义。
说明:国际化团队或海外部署的服务器由于字符集问题,使用全英文来注释和描述日志错误信息。
logback框架使用之核心配置对象及属性分析
// 入口 getLogger
@Slf4j
public class CatchExceptionTest {
private static final Logger logger = LoggerFactory.getLogger(CatchExceptionTest.class);
// 入口 getLogger getILoggerFactory
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
public static Logger getLogger(Class<?> clazz) {
Logger logger = getLogger(clazz.getName());
if (DETECT_LOGGER_NAME_MISMATCH) {
Class<?> autoComputedCallingClass = Util.getCallingClass();
if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(), autoComputedCallingClass.getName()));
Util.report("See http://www.slf4j.org/codes.html#loggerNameMismatch for an explanation");
}
}
return logger;
}
public class LoggerContext extends ContextBase implements ILoggerFactory, LifeCycle {
public final Logger getLogger(Class<?> clazz) {
return this.getLogger(clazz.getName());
}
public final Logger getLogger(String name) {
LoggerFactory.class
// 入口 getLoggerFactory
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == 0) {
Class var0 = LoggerFactory.class;
synchronized(LoggerFactory.class) {
if (INITIALIZATION_STATE == 0) {
INITIALIZATION_STATE = 1;
performInitialization();
}
}
}
switch(INITIALIZATION_STATE) {
case 1:
return SUBST_FACTORY;
case 2:
throw new IllegalStateException("org.slf4j.LoggerFactory in failed state. Original exception was thrown EARLIER. See also http://www.slf4j.org/codes.html#unsuccessfulInit");
case 3:
// 获取日志工厂
// 指定StaticLoggerBinder,具体实现和加载的是logback-class的实现类
return StaticLoggerBinder.getSingleton().getLoggerFactory();
case 4:
return NOP_FALLBACK_FACTORY;
default:
throw new IllegalStateException("Unreachable code");
}
}
定位到了 logback-classic
StaticLoggerBinder是logback-classic的具体实现
// StaticLoggerBinder
public ILoggerFactory getLoggerFactory() {
if (!this.initialized) {
return this.defaultLoggerContext;
} else if (this.contextSelectorBinder.getContextSelector() == null) {
throw new IllegalStateException("contextSelector cannot be null. See also http://logback.qos.ch/codes.html#null_CS");
} else {
return this.contextSelectorBinder.getContextSelector().getLoggerContext();
}
}
日志输出规约
日志级别开关判断
对于 trace/debug/info 级别的日志输出,必须进 行日志级别的开关判断
异常日志信息要完整
异常日志信息应该包括 以下两类信息: 案发现场信息,异常堆栈信息
避免重复打印日志
重复打印日志,浪费磁盘空间,务必在日志配置文件中设置additivity=false
扩展日志与规约
扩展日志单独存储
应用中的扩展日志(如打点、临时 监控、访问日志等)单独存储
错误日志单独存储
业务日志与错误日志分开存储