赞
踩
日志:记录程序的运行轨迹,方便查找关键信息,也方便快速定位解决问题
通常,Java程序员在开发项目时都是依赖Eclipse/IDEA等集成开发工具的Debug 调试功能来跟踪解决Bug,但项目发布到了测试、生产环境怎么办?你有可能会说可以使用远程调试,但实际并不能允许让你这么做。
所以,日志的作用就是在测试、生产环境没有 Debug 调试工具时开发和测试人员定位问题的手段。日志打得好,就能根据日志的轨迹快速定位并解决线上问题,反之,日志输出不好,不仅无法辅助定位问题反而可能会影响到程序的运行性能和稳定性。
很多介绍 AOP 的地方都采用日志来作为介绍,实际上日志要采用切面的话是极其不科学的!对于日志来说,只是在方法开始、结束、异常时输出一些什么,那是绝对不够的,这样的日志对于日志分析没有任何意义。如果在方法的开始和结束整个日志,那方法中呢?如果方法中没有日志的话,那就完全失去了日志的意义!如果应用出现问题要查找由什么原因造成的,也没有什么作用。这样的日志还不如不用!
不管是使用何种编程语言,日志输出几乎无处不再。总结起来,日志大致有以下几种用途:
总结:日志在应用程序中是非常非常重要的,好的日志信息能有助于我们在程序出现BUG时能快速进行定位,并能找出其中的原因。作为一个有修养的程序猿,对日志这个东西应当引起足够的重视。
1、日志门面
2、日志实现:JUL(java.util.logging)、Log4j、Log4j2、Logback
SLF4J、Log4j、Logback都是同一个人Ceki写的。为什么写三个呢?那是因为Ceki先写了Log4j,但后面觉得Log4j太烂了,他已经不想改了。所以后面写了logback。Log4j2呢,从设计上来说很优秀,但太优秀了,太先进了,很多开源框架对其支持有限(Log4j2性能优于logback,但为了有高性能,设计得过于复杂,没必要,用Logback就够了)
如下是日志结构框架:
┌───────────────────┐ ┌────────────────────────────────────┐
│ 日志适配器 │ »»»»»»»»» │ 日志门面 │
│ │ »»»»»»»»» │ slf4j commons-logging │
│ │ └────────────────────────────────────┘
│ jul-to-slf4j │ ┌────────────────────────────────────┐
│ log4j-over-slf4j │ │ 日志门面的适配器 │
│ jcl-to-slf4j │ │ slf4j-jdk slf4j-jcl slf4j-log4j12 |
└───────────────────┘ └────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────┐
│ 日志库的具体实现 │
│ log4j log4j2 logback log-jdk otherLogging │
└───────────────────────────────────────────────────────────────────────────┘
选项太多了的后果就是选择困难症,我的看法是没有最好的,只有最合适的:
2001年以前,Java是没有日志库的,打印日志全凭:System.out 和 System.err。缺点:
2001年,一个叫Ceki Gulcü的大佬搞了一个日志框架 log4j后来(Log4j成为Apache项目),Ceki加入Apache组织Apache还曾经建议Sun引入Log4j到Java的标准库中,但Sun拒绝了。
Sun有自己的小心思,2002年2月JDK1.4发布,Sun推出了自己的日志标准库JUL(Java Util Logging),其实是照着Log4j抄的,而且还没抄好,还是在JDK1.5以后性能和可用性才有所提升。由于Log4j比JUL好用,并且成熟,所以Log4j在选择上占据了一定的优势。
2002年8月Apache推出了JCL(Jakarta Commons Logging),也就是日志抽象层,支持运行时动态加载日志组件的实现,当然也提供一个默认实现Simple Log(在ClassLoader中进行查找,如果能找到Log4j则默认使用Log4j实现,如果没有则使用JUL实现,再没有则使用JCL内部提供的Simple Log实现)
项目
⬇
JCL
⬇
┌────────────────────┬───────────────────────────┬──────────────────────────────┐
Log4JLogger JDK14Logger Jdk13LumberjackLogger SimpleLog
JUL有三个缺点:
1.效率较低
2.容易引发混乱
3.使用了自定义ClassLoader的程序中,使用JCL会引发内存泄露
SLF4J的由来与背景:在Log4j开发出来出来之后,Log4j就受到了广大开发者的爱好,纷纷开始使用Log4j,但是后来Log4j的创始人跟Apache因为一些矛盾从Apache辞职自己去创业了,创始人为了给自己的公司打出一点名声,所以就基于Log4j又开发出了一个新的日志框架Logback,Logback不管是性能还是功能都比Log4j强,但是却很少人使用, 因为JCL门面不支持Logback,所以这个创世人又设计出了slf4j,slf4j支持市面上所有主流的日志框架;所以目前为止,使用最多的就是SLF4J了; 后来Apache又基于Logback的源码设计出了Log4j2日志,性能上Log4j2比Logback更胜一筹,并且Log4j2既是日志框架,也是门面技术,但是Log4j2的门面技术很少人使用,大多还是使用SLF4J。
2006年巨佬Ceki(Log4j的作者)因为一些原因离开了Apache组织,之后Ceki觉得JCL不好用,自己重写了一套新的日志标准接口规范Slf4j(Simple Logging Facacfor Java),也可以称为日志门面,很明显Slf4j是对标JCL,后面也证明了Slf4j比JCL更优秀。
大佬Ceki提供了一系列的桥接包来帮助Slf4j接口与其他日志库建立关系,这种方式称桥接设计模式。代码使用Slf4j接口,就可以实现日志的统一标准化,后续如果想要更换日志实现,只需引入Slf4j与相关的桥接包,再引入具体的日志标准库即可。
Ceki巨佬觉得市场上的日志标准库都是间接实现Slf4j接口,也就是说每次都需要配合桥接包,因此在2006年,Ceki巨佬基于Slf4j接口写出了Logback日志标准库,做为Slf4j接口的默认实现,Logback也十分给力,在功能完整度和性能上超越了所有已有的日志标准库。
产生背景:Logback算是JAVA 里一个老牌的日志框架,从06年开始第一个版本,迭代至今也十几年了。不过logback最近一个最稳定版本还停留在 2017 年,好几年都没有更新大版本了Llogback的兄弟SLF4J最近一个稳定版也是2017年。而且Logback的异步性能实在拉跨,功能简陋,配置又繁琐,远不及Apache的新一代日志框架:Log4j2。目前来看,Log4j2就是王者,其他日志框架都不是对手。
2012年,Apache直接推出新项目Log4j2(不兼容Log4j),Log4j2全面借鉴Slf4j+Logback 。Log4j2不仅仅具有Logback的所有特性,还做了分离设计,分为log4j-api和log4j-core,log4j-api是日志接口,log4j-core是日志标准库,并且Apache也为Log4j2提供了各种桥接包。而且log4j2 的性能提升很大,而且支持异步日志打印。增加很多新的特性
日志是快速定位问题的好帮手,是撕逼和甩锅的利器!打印好日志非常重要。今天我们来聊聊日志打印的15个好建议
1、选择恰当的日志级别
常见的日志级别有5种,分别是error、warn、info、debug、trace。日常开发中,我们需要选择恰当的日志级别,不要直接打印info
2、日志要打印出方法的入参和出参
我们并不需要打印很多很多日志,只需要打印可以快速定位问题的有效日志。有效的日志,才能有效的查到问题!哪些算得的上有效关键的日志呢?比如说,方法进来的时候,打印入参。再然后呢,在方法返回的时候,就是打印出参,返回值。入参的话,一般就是userId或者bizSeq这些关键信息。正例如下:
public String testLogMethod(Document doc, Mode mode){
log.debug(“method enter param:{}”,userId);
String id = "666";
log.debug(“method exit param:{}”,id);
return id;
}
3、选择合适的日志格式
理想的日志格式,应当包括这些最基本的信息:如当前时间戳(一般毫秒精确度)、日志级别,线程名字等等。在logback日志里可以这么配置:
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level [%thread][%logger{0}] %m%n</pattern>
</encoder>
</appender>
4、遇到if…else…时,每个分支首行都尽量打印日志
当你碰到if…else…或者switch这样的条件时,可以在分支的首行就打印日志,这样排查问题时,就可以通过日志,确定进入了哪个分支,代码逻辑更清晰,也更方便排查问题了。
if(user.isVip()){
log.info("该用户是会员,Id:{},开始处理会员逻辑",user,getUserId());
// 会员逻辑
}else{
log.info("该用户是非会员,Id:{},开始处理非会员逻辑",user,getUserId());
// 非会员逻辑
}
5、日志级别比较低时,进行日志开关判断
对于trace/debug这些比较低的日志级别,必须进行日志级别的开关判断。
User user = new User(666L, "公众号", "捡田螺的小男孩");
if (log.isDebugEnabled()) {
log.debug("userId is: {}", user.getId());
}
因为当前有如下的日志代码:
logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);
如果配置的日志级别是warn的话,上述日志不会打印,但是会执行字符串拼接操作,如果symbol是对象, 还会执行toString()方法,浪费了系统资源,执行了上述操作,最终日志却没有打印,因此建议加日志开关判断。
6、不能直接使用日志系统(Log4j、Logback)中的 API,而是使用日志框架SLF4J中的API
SLF4J 是门面模式的日志框架,有利于维护和各个类的日志处理方式统一,并且可以在保证不修改代码的情况下,很方便的实现底层日志框架的更换。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(TianLuoBoy.class);
7、建议使用参数占位{},而不是用+拼接
反例:
logger.info("Processing trade with id: " + id + " and symbol: " + symbol);
上面的例子中,使用+
操作符进行字符串的拼接,有一定的性能损耗。
正例:
logger.info("Processing trade with id: {} and symbol : {} ", id, symbol);
我们使用了大括号{}
来作为日志中的占位符,比于使用+
操作符,更加优雅简洁。并且,相对于反例,使用占位符仅是替换动作,可以有效提升性能。
8、建议使用异步的方式来输出日志。
<appender name="FILE_ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="ASYNC"/>
</appender>
9、不要使用e.printStackTrace()
反例:
try {
// 业务代码处理
} catch(Exception e) {
e.printStackTrace();
}
正例:
try {
// 业务代码处理
} catch(Exception e) {
log.error("你的程序有异常啦",e);
}
理由:
10、异常日志不要只打一半,要输出全部错误信息
反例1:
try {
//业务代码处理
} catch (Exception e) {
// 错误
LOG.error('你的程序有异常啦');
}
异常e都没有打印出来,所以压根不知道出了什么类型的异常。
反例2:
try {
//业务代码处理
} catch (Exception e) {
// 错误
LOG.error('你的程序有异常啦', e.getMessage());
}
e.getMessage()不会记录详细的堆栈异常信息,只会记录错误基本描述信息,不利于排查问题。
正例:
try {
//业务代码处理
} catch (Exception e) {
// 错误
LOG.error('你的程序有异常啦', e);
}
11、禁止在线上环境开启 debug
禁止在线上环境开启debug,这一点非常重要。因为一般系统的debug日志会很多,并且各种框架中也大量使用 debug的日志,线上开启debug不久可能会打满磁盘,影响业务系统的正常运行。
12、不要记录了异常,又抛出异常
反例如下:
log.error("IO exception", e);
throw new MyException(e);
13、避免重复打印日志
避免重复打印日志,酱紫会浪费磁盘空间。如果你已经有一行日志清楚表达了意思,避免再冗余打印,反例如下:
if (user.isVip()) {
log.info("该用户是会员,Id:{}",user,getUserId());
// 冗余,可以跟前面的日志合并一起
log.info("开始处理会员逻辑,id:{}",user,getUserId());
// 会员逻辑
} else {
// 非会员逻辑
}
如果你是使用log4j日志框架,务必在log4j.xml
中设置 additivity=false,因为可以避免重复打印日志。正例:
<logger name="com.taobao.dubbo.config" additivity="false">
14、日志文件分离
15、核心功能模块,建议打印较完整的日志
运维利器:9个Java日志分析工具:https://mp.weixin.qq.com/s/cvnA_HiglxAd5x6XtmnOIQ
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。