log4j日志封装说明—slf4j对于log4j的日志封装-正确获取调用堆栈

转载 2013年12月05日 21:58:02

日志是项目中必用的东西,日志产品里最普及应该就是log4j了。(logback这里暂不讨论。) 先看一下常用的log4j的用法,一般来说log4j都会配合slf4j或者common-logging使用,这里已slf4j为例。添加gradle依赖:

?
1
2
3
4
5
dependencies {
compile('log4j:log4j:1.2.17',
'org.slf4j:slf4j-api:1.7.5',
'org.slf4j:slf4j-log4j12:1.7.5')
}

最直接的用法就是在每个需要记录日志的类里,构造一个属于自己类的log实例,实际上很多著名的开源项目也是这么做的。如spring。

?
1
private static final Log logger = LogFactory.getLog(BeanUtils.class);

那么我们也先从这种用法开始,先配置好最基本的log4j.xml配置文件。

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
<?xml version= "1.0" encoding ="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "http://log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" >
 
     <appender name="Console" class="org.apache.log4j.ConsoleAppender" >
           <layout class= "org.apache.log4j.PatternLayout" >
               <param name= "ConversionPattern" value ="%d >> %-5p >> %t >> %l >> %m%n" />
           </layout>
     </appender >
     <root >
           <level value= "info" />
           <appender-ref ref= "Console" />
     </root >
 
</log4j:configuration>

参数说明:

%p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL
%r 输出自应用启动到输出该log信息耗费的毫秒数
%c 输出所属的类目,通常就是所在类的全名
%t 输出产生该日志事件的线程名
%n 输出一个回车换行符,Windows平台为“\r\n”,Unix平台为“\n”
%d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921
%l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。

日志测试类:

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
package com.coderli.log4jpackage;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* log4j封装实验室中的用户模拟类
*
* @author OneCoder
* @date 2013年11月25日 下午4:39:19
*/
public class UserClass {
 
     private static final Logger logger = LoggerFactory
              . getLogger(UserClass.class);
     public static void main(String[] args) {
           logger.info("这是一个Info级别的log4j日志。" );
     }
}

输出日志:

2013-11-26 11:09:21,305 >> INFO  >> main >> com.coderli.log4jpackage.UserClass.main(UserClass.java:18) >> 这是一个Info级别的log4j日志。

这里包含的日志发生时的类、线程、行号等信息。很完整。本身这么做没什么问题,只是可能有的项目考虑如果每个类里都写这样一个开头,有点麻烦,同时,如果每个类一个独立的声明,log4j内存会缓存很多的实例,占用内存,可能有时候也不便于统一配置管理。所以,有些项目里考虑了对log进行封装,提供统一的一个静态方法调用:

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
package com.coderli.log4jpackage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
/**
* @author lihzh(OneCoder)
* @date 2013年11月25日 下午4:39:54
*/
public class MyLog4j {
     private static final Logger logger = LoggerFactory.getLogger("MyLog4j");
 
     public static void info(String msg) {
           logger.info(msg);
     }
}

记录日志代码变为:

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
package com.coderli.log4jpackage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
/**
* @author lihzh(OneCoder)
* @date 2013年11月25日 下午4:39:54
*/
public class MyLog4j {
     private static final Logger logger = LoggerFactory.getLogger("MyLog4j");
 
     public static void info(String msg) {
           logger.info(msg);
     }
}

日志输出:

2013-11-26 11:23:29,936 >> INFO  >> main >> com.coderli.log4jpackage.MyLog4j.info(MyLog4j.java:16 ) >> 这是一个Info级别的log4j日志。

咋一看没什么问题,仔细分析就发现,对我们调试很有帮助的日志发生时的类名、行号都编程了封装类里面的类和行,这对于依靠日志进行错误调试来说是悲剧的。这种封装虽然解决了实例过多的问题,但是也是失败的,甚至是灾难性的。

也有人把发生日志的Class信息也传递进来一起打印。如:

?
1
2
3
public static void info(String className, String msg) {
           logger.info(className + ">> " + msg);
     }

这种方式显然是治标不治本的。于是很多人想到了另外的封装方式,即提供一个统一获取logger实例的位置,然后在自己的类里进行嗲用:

?
1
2
3
public static Logger getLogger() {
           return logger;
     }

调用代码:

?
1
2
3
public static void main(String[] args) {
          MyLog4j. getLogger().info("这是一个Info级别的log4j日志。" );
     }

这种方法,日志虽然恢复了正常,但是对于开发者实际又增加了麻烦,打印一个日志需要两部操作了。当然你可以使用import static。或者每个类里还是全局声明一个logger实例。

那么有没有想过,slf4j内部是怎么对log4j封装的呢?我们通过slf4j调用为什么就可以之间获得你实际打印日志的行号,同时又不会把自己类给暴露出来呢?你可能还没明白我在说什么,细说一下,如果我们直接使用log4j的logger,打印出来的是我们调用类的行号这没什么问题,但是这里我们获得的是slf4j的logger实例,它底层调用的是log4j的logger实例,那么为什么不会打印出slf4j内部调用类的行号呢?这就是我关心的问题。知道了这个,也许我们就可以封装出更好用的全局log组件了。

其实,这个问题的关键就集中在log4j是如何获取你调用日志的代码的类和行号的,在Java中可以通过Throwable来获取调用堆栈, 例如我们将如下代码,放在MyLog4j类的info方法中:

?
01
02
03
04
05
06
07
08
09
10
11
12
13
public static void info(String msg) {
   Throwable throwable = new Throwable();
   StackTraceElement[] ste = throwable.getStackTrace();
   for (StackTraceElement stackTraceElement : ste) {
      System.out
            .println("ClassName: " + stackTraceElement.getClassName());
      System.out.println("Method Name: "
            + stackTraceElement.getMethodName());
      System.out.println("Line number: "
            + stackTraceElement.getLineNumber());
   }
   logger.info(msg);
}

再次通过UserClass调用,就可获得如下输出:

ClassName: com.coderli.log4jpackage.MyLog4j
Method Name: info
Line number: 28
ClassName: com.coderli.log4jpackage.UserClass
Method Name: main
Line number: 12

由此可见只要在调用堆栈里找到用户的类,就可以获得所有我们需要的信息。有了这个基础,我们再来看看slf4j和log4j是怎么做的。

在log4j的Logger中,实际对外提供了用于封装的统一的log方法。  

?
01
02
03
04
05
06
07
08
09
10
11
/**
 
    This is the most generic printing method. It is intended to be
    invoked by <b>wrapper</b> classes.
 
    @param callerFQCN The wrapper class' fully qualified class name.
    @param level The level of the logging request.
    @param message The message of the logging request.
    @param t The throwable of the logging request, may be null.  */
 public
 void log(String callerFQCN, Priority level, Object message, Throwable t)

而第一个参数callerFQCN,就是关键的决定调用者位置的参数。在LocationInfo中,可看到对该参数的使用方式为:

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public LocationInfo(Throwable t, String fqnOfCallingClass) {
      if(t == null || fqnOfCallingClass == null)
return;
      if (getLineNumberMethod != null) {
          try {
              Object[] noArgs = null;
              Object[] elements =  (Object[]) getStackTraceMethod.invoke(t, noArgs);
              String prevClass = NA;
              for(int i = elements.length - 1; i >= 0; i--) {
                  String thisClass = (String) getClassNameMethod.invoke(elements[i], noArgs);
                  if(fqnOfCallingClass.equals(thisClass)) {
                      int caller = i + 1;
                      if (caller < elements.length) {
                          className = prevClass;
                          methodName = (String) getMethodNameMethod.invoke(elements[caller], noArgs);
                          fileName = (String) getFileNameMethod.invoke(elements[caller], noArgs);
                          if (fileName == null) {
                              fileName = NA;
                          }
                          int line = ((Integer) getLineNumberMethod.invoke(elements[caller], noArgs)).intValue();
                          if (line < 0) {
                              lineNumber = NA;
                          } else {
                              lineNumber = String.valueOf(line);
                          }
                          StringBuffer buf = new StringBuffer();
                          buf.append(className);
                          buf.append(".");
                          buf.append(methodName);
                          buf.append("(");
                          buf.append(fileName);
                          buf.append(":");
                          buf.append(lineNumber);
                          buf.append(")");
                          this.fullInfo = buf.toString();
                      }
                      return;
                  }
                  prevClass = thisClass;
              }
              return;
….//省略若干

可见,log4j把传递进来的callerFQCN在堆栈中一一比较,相等后,再往上一层即认为是用户的调用类。所以,在slf4j封装的logger中是这样封装的:

?
1
2
3
4
final static String FQCN = Log4jLoggerAdapter.class .getName();
public void info(String msg) {
    logger.log(FQCN, Level. INFO, msg, null );
  }

用户的代码调用的正是调用的这个info,所以就会正常的显示用户代码的行号了。那么对于封装来说,你只需要注意调用log4j的log方法,并传递进去正确的FQCN即可。

?
1
2
3
4
final static String FQCN = Log4jLoggerAdapter.class .getName();
public void info(String msg) {
    logger.log(FQCN, Level. INFO, msg, null );
  }

java日志(slf4j, log4j, jdk4j,slf4j_simple,)

以前自学java的时候,听圣思源的张龙讲到过slf4j,由于年代过于久远,都不知道这个jar是干嘛的,直到今天做了这个笔记之后..... 引用资源:http://baike.baidu.com/vie...
  • u012049463
  • u012049463
  • 2013年10月11日 15:54
  • 2597

Java日志 - log4简单使用实例及其再次封装

程序中记录日志一般目的:    * Troubleshooting(故障定位):向文件或控制台打印代码的调试信息    * 显示程序运行状态:周期性的记录到文件中供其他应用进行统计分析工作    * ...
  • dietime1943
  • dietime1943
  • 2017年03月10日 18:04
  • 1576

Slf4j Logger 的封装——Log

很多时候我们为了在类中加日志不得不写一行,而且还要去手动改XXX这个类名 ? 1 private static Logger log =...
  • z69183787
  • z69183787
  • 2015年10月07日 14:19
  • 6140

log4j日志封装说明—slf4j对于log4j的日志封装-正确获取调用堆栈

日志是项目中必用的东西,日志产品里最普及应该就是log4j了。(logback这里暂不讨论。) 先看一下常用的log4j的用法,一般来说log4j都会配合slf4j或者common-logging使用...
  • u014437455
  • u014437455
  • 2014年04月19日 22:53
  • 1774

log4j中isDebugEnabled(), log.isInfoEnabled()和log.isTraceEnabled的封装

转载自:http://blog.csdn.net/greencacti/article/details/5134921 1. log4j中log.isDebugEnabled(), log.is...
  • hgsunyong
  • hgsunyong
  • 2014年02月24日 14:56
  • 1275

JavaWeb日志管理---@Slf4j注解

@Slf4j注解的正确使用1、对于一个maven项目。首先要在pom.xml中加入以下依赖项: org.slf4j slf4j-api ...
  • wangjie123end
  • wangjie123end
  • 2017年08月16日 11:38
  • 3285

Log4j简单封装及配置

大家在项目中经常会用到Log4j,一般包括4个步骤: 1.编写log4j配置文件; 2.初始化log4j; 3.获取一个logger实例; 4.调用logger.debug() 等记录日志。 ...
  • jmppok
  • jmppok
  • 2015年04月02日 10:18
  • 2336

日志管理框架:Log4j工具封装类

做这个例子的初衷就是,把任何一个对象丢进框架中的对应的输出日志的方法中,都能输出这个对象所对应的toString重写方法,来输出对象的包含的数据,现在能实现:字符串,数组,List,Map集合的输出:...
  • zp357252539
  • zp357252539
  • 2016年07月07日 08:22
  • 3241

SLF4J日志类库

SLF4J不同于其他日志类库,与其它有很大的不同。SLF4J(Simple logging Facade for Java)不是一个真正的日志实现,而是一个抽象层( abstraction la...
  • xiaozhu0301
  • xiaozhu0301
  • 2016年06月23日 18:40
  • 232

log4j日志封装说明—slf4j对于log4j的日志封装-正确获取调用堆栈

日志是项目中必用的东西,日志产品里最普及应该就是log4j了。(logback这里暂不讨论。) 先看一下常用的log4j的用法,一般来说log4j都会配合slf4j或者common-logging使用...
  • e_wsq
  • e_wsq
  • 2013年12月05日 21:58
  • 1505
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:log4j日志封装说明—slf4j对于log4j的日志封装-正确获取调用堆栈
举报原因:
原因补充:

(最多只允许输入30个字)