四时宝库

程序员的知识宝库

002-slf4j+log4j 使用及源码分析(log4j-to-slf4j.jar)

log4j、log4j2、logback日志关系

1、Apache Log4j 是一个老的日志框架,并且是多年来最受欢迎的日志框架。2015 年 8 月 5 日,该项目管理委员会宣布 Log4j 1.x 已达到使用寿命。 建议用户使用 Log4j 1 升级到 Apache Log4j2。

2、logback 是由 log4j 创始人设计的又一个开源日志组件,作为流行的 log4j 项目的后续版本,从而替代 log4j。原生实现了SLF4J。

3、Apache Log4j 2是对 Log4j 的升级,它比其前身 Log4j 1.x 提供了重大改进,并提供了 Logback 中可用的许多改进,同时修复了 Logback 架构中的一些固有问题。

log4j2包含基于lmax disruptor库的下一代异步记录器(Asynchronous Loggers)。在多线程场景中,异步日志记录器的吞吐量是log4j1.x和Logback的18倍,延迟也要低几个数量级。log4j2的性能明显优于log4j1.x、Logback和java.util.logging,尤其是在多线程应用程序中。

4,队列的选择对于峰值吞吐量非常重要。log4j2的异步记录器使用无锁数据结构(disruptor),而Logback、log4j1.2和log4j2的异步附加器使用ArrayBlockingQueue。对于阻塞队列,多线程应用程序在尝试将日志事件排队时经常遇到锁争用


性能比较:http://logging.apache.org/log4j/2.x/performance.html

1,异步日志吞吐量

Y轴:单位msg/sec , 每秒消息数。

X轴:线程数。

说明:随着线程数的增加,log4j2 使用全部异步loggers ,性能达到每秒处理1800万消息。


2,异步日志记录响应时间


Y轴:单位:延迟毫秒数

X轴:响应时间百分比。


下面以springboot项目来介绍日志用法

新建一个项目,只引入spring-boot-starter-web stater.


1,Log4j 用法

说明:虽然新项目一般都不会用log4j了,但是一些老项目还在使用。学习也是必要的。

根据前面介绍:SpringBoot选用 SLF4j和logback默认实现;


(1)使用slf4j+log4j的方式,需要排除默认logback日志依赖,引入slf4j+log4j依赖.

jul-to-slf4j,jcl-over-slf4j 包是替换其他日志框架,使使用jul,jcl日志框架的类也通过slf4jAPI 输出日志,由log4j 来实现。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <artifactId>spring-boot-starter-logging</artifactId>
            <groupId>org.springframework.boot</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.12</version>
    <!--包含了slf4j+log4j包-->
</dependency>

(2)在resources目录下创建log4j.properties文件

### 设置###
log4j.rootLogger = debug,console,File1,File2

### 输出信息到控制抬 ###
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern = [%d{yyyy-MM-dd HH:mm:ss:SSS}] [%p] [%t]- %l - %m%n

### 输出DEBUG 级别以上的日志到D:/logs/debug.log ###
# 日志记录到多个文件
log4j.appender.File1 = org.apache.log4j.RollingFileAppender
# 日志文件的名称
log4j.appender.File1.File = D:/logs/debug.log
# 默认设置为true,这意味着记录的信息被附加到同一文件的末尾
log4j.appender.File1.Append = true
# 标志的默认设置为true,这意味着输出流的文件被刷新,在每个追加操作
log4j.appender.File1.ImmediateFlush=true
log4j.appender.File1.Threshold = DEBUG
# 文件的回滚临界尺寸。默认值是10MB
# 此示例配置说明每个日志文件的最大允许大小为5MB。当超过最大尺寸,新的日志文件将被创建
# 因为maxBackupIndex被定义为2,当第二个日志文件达到最大值,第一个日志文件将被删除,
# 之后所有的日志信息将被回滚到第一个日志文件。
log4j.appender.File1.MaxFileSize = 5MB
# 此属性表示要创建的备份文件的数量。默认值是1
log4j.appender.File1.MaxBackupIndex = 10
log4j.appender.File1.Encoding = UTF-8
log4j.appender.File1.layout = org.apache.log4j.PatternLayout
log4j.appender.File1.layout.ConversionPattern = [%d{yyyy-MM-dd HH:mm:ss:SSS}] [%p] [%t]- %l - %m%n

### 输出ERROR 级别以上的日志到D:/logs/error.log ###
# 每天生成一个日志文件
log4j.appender.File2 = org.apache.log4j.DailyRollingFileAppender
log4j.appender.File2.File =D:/logs/error
log4j.appender.File2.Append = true
log4j.appender.File2.DatePattern = '.'yyyy-MM-dd-HH-mm'.log'
log4j.appender.File2.Threshold = ERROR 
log4j.appender.File2.layout = org.apache.log4j.PatternLayout
log4j.appender.File2.layout.ConversionPattern = [%d{yyyy-MM-dd HH:mm:ss:SSS}] [%p] [%t]- %l - %m%n


log4j.logger.com.example.springboot=debug,myLog
log4j.additivity.com.example.springboot=false
log4j.appender.myLog=org.apache.log4j.ConsoleAppender
log4j.appender.myLog.Target=System.out
log4j.appender.myLog.Threshold=debug
log4j.appender.myLog.layout=org.apache.log4j.PatternLayout
log4j.appender.myLog.layout.ConversionPattern=[%d{yyyy-MM-dd HH:mm:ss:SSS}] %5p %c{1}:%L - %m%n

(3)代码中使用

public class Log4JTest {
    private static final Logger logger = LoggerFactory.getLogger(Log4JTest.class);
    public static void main(String[] args) {
        // 记录debug级别的信息
        logger.debug("This is debug message.");
        // 记录info级别的信息
        logger.info("This is info message.");
        // 记录error级别的信息
        logger.error("This is error message.");
    }
}

Log4J框架的不同组件的虚拟图:

Filter对象:

可以过滤日志级别,比如只想输出INFO,不想输出WARN,ERROR到文件。

一个appender对象可以有与之关联的几个Filter对象。


日志管理:

日志管理对象管理的日志框架。它负责从一个系统级的配置文件或配置类读取初始配置参数。比如从resources目录下读取log4j.properties


log4j.properties文件是一个键 - 值对保存 log4j 配置属性文件。默认情况下,日志管理在CLASSPATH 查找一个名为 log4j.properties 的文件。

Log4j由三个重要的组件构成:

  • 日志信息的优先级:log4j.rootLogger = [ level ],xxxx 配置 level = debug
  • 日志信息的输出目的地:log4j.appender.xxxx = value 配置 ,value 就是目的地,比如org.apache.log4j.ConsoleAppender 表示输出到控制台。
  • 日志信息的输出格式。

日志信息的优先级从高到低有ERROR、WARN、 INFO、DEBUG,分别用来指定这条日志信息的重要程度;日志信息的输出目的地指定了日志将打印到控制台还是文件中;而输出格式则控制了日志信息的显示内容。


1.1. 配置根Logger,其语法为:

log4j.rootLogger = [ level ] , appenderName, appenderName, …

其中,level 是日志记录的优先级,分为OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL或者您定义的级别。

Log4j建议只使用四个级别,优先级从高到低分别是ERROR、WARN、INFO、DEBUG。

比如:指定level = INFO , INFO级别以下日志不输出,输出自身及以上日志。appenderName就是指把日志信息输出到哪个地方,比如上面定义了输出到控制台(console),文件1(File1),文件2(File2)


定义非根Logger(很有用)

log4j.logger.loggerName1 = [ level ], appendName1,…appendNameN

比如我们只想输出程序中com.example.springboot包下的INFO日志,可以配置

log4j.logger.com.example.springboot=info  
该级别日志优先级大于根level (如果根level=debug 或者 level = error, 以info为准) 

当然也可以自定义输出appender为myLog

log4j.logger.com.example.springboot=info,myLog
log4j.additivity.com.example.springboot=false
log4j.appender.myLog=org.apache.log4j.ConsoleAppender
log4j.appender.myLog.Target=System.out
log4j.appender.myLog.Threshold=INFO
log4j.appender.myLog.layout=org.apache.log4j.PatternLayout
log4j.appender.myLog.layout.ConversionPattern=[%d{yyyy-MM-dd HH:mm:ss:SSS}] %5p %c{1}:%L - %m%n
#    默认情况下子Logger会继承父Logger的appender,也就是说子Logger会在父Logger的appender里输出。
#若是additivity设为false,则子Logger只会在自己的appender里输出,而不会在父Logger的appender里输出。


1.2. 配置文件的输出目的地Appender,一般,配置代码的格式如下

log4j.appender.appenderName = fully.qualified.name.of.appender.class  
log4j.appender.appenderName.[option] = [value] 
log = /usr/home/log4j
log4j.appender.FILE.File=${log}/log.out
需要注意的是log4j支持UNIX风格的变量替换,如 ${variableName}.

其中,Log4j提供的appender有以下几种:

  • org.apache.log4j.ConsoleAppender(控制台),
  • org.apache.log4j.FileAppender(文件),
  • org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件),
  • org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件),
  • org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)


Appender默认配置:



1.3.配置日志信息的格式(布局),其语法为

log4j.appender.appenderName.layout = fully.qualified.name.of.layout.class  
log4j.appender.appenderName.layout.option1 = value1  
log4j.appender.appenderName.layout.[option] = [value]

其中,Log4j提供的layout有以下几种:

  • org.apache.log4j.HTMLLayout(以HTML表格形式布局),
  • org.apache.log4j.PatternLayout(可以灵活地指定布局模式),
  • org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串),
  • org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)
# 上面log4j.properties的console 的布局就是PatternLayout
log4j.appender.console.layout = org.apache.log4j.PatternLayout
# layout输出格式
log4j.appender.console.layout.ConversionPattern = [%d{yyyy-MM-dd HH:mm:ss:SSS}] [%p] [%t]- %l - %m%n


Log4J的printf函数的打印格式格式化日志信息,打印的参数含义如下:

  • %m 输出代码中指定的消息
  • %p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL
  • %r 输出自应用启动到输出该log信息耗费的毫秒数
  • %c 输出所属的类目,通常就是所在类的全名
  • %t 输出产生该日志事件的线程名
  • %n 输出一个回车换行符
  • %d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyyy-MM-dd HH:mm:ss,SSS},输出类似:2020-10-11 12:25:12,345
  • %l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。举例:Testlog4.main(TestLog4.java:10)
[%d{yyyy-MM-dd HH:mm:ss:SSS}] [%p] [%t]- %l - %m%n
输出类似:[2020-12-11 22:40:02:803] [INFO] [main]- com.example.springboot.Log4JTest.main(Log4JTest.java:18) - This is info message.


1.4.日志记录到多个文件

log4j.appender.File1 = org.apache.log4j.RollingFileAppender
# 日志文件的名称
log4j.appender.File1.File = D:/logs/debug.log
# 默认设置为true,这意味着记录的信息被附加到同一文件的末尾
log4j.appender.File1.Append = true
# 标志的默认设置为true,这意味着输出流的文件被刷新,在每个追加操作
log4j.appender.File1.ImmediateFlush=true
log4j.appender.File1.Threshold = DEBUG
# 文件的回滚临界尺寸。默认值是10MB
# 此示例配置说明每个日志文件的最大允许大小为5MB。当超过最大尺寸,新的日志文件将被创建
# 因为maxBackupIndex被定义为2,当第二个日志文件达到最大值,第一个日志文件将被删除,
# 之后所有的日志信息将被回滚到第一个日志文件。
log4j.appender.File1.MaxFileSize=5
# 此属性表示要创建的备份文件的数量。默认值是1
log4j.appender.File1.MaxBackupIndex=2
log4j.appender.File1.Encoding = UTF-8
log4j.appender.File1.layout = org.apache.log4j.PatternLayout
log4j.appender.File1.layout.ConversionPattern = [%d{yyyy-MM-dd HH:mm:ss:SSS}] [%p] [%t]- %l - %m%n

1.5. 每天生成日志文件

DatePattern控制使用下列滚动的时间表方式之一:


# 每天生成一个日志文件
log4j.appender.File2 = org.apache.log4j.DailyRollingFileAppender
log4j.appender.File2.File =D:/logs/error
log4j.appender.File2.Append = true
log4j.appender.File2.DatePattern = '.'yyyy-MM-dd'.log'
log4j.appender.File2.Threshold = ERROR
log4j.appender.File2.layout = org.apache.log4j.PatternLayout
log4j.appender.File2.layout.ConversionPattern = [%d{yyyy-MM-dd HH:mm:ss:SSS}] [%p] [%t]- %l - %m%n


学到这里,思考一下?如何配置只输出INFO 信息到指定文件,也就是该文件中不包含WARN,ERROR级别日志?

log4j提供了Appender的Threshold属性,可以设置该appender输出什么级别的日志。

log4j.appender.File1.Threshold=INFO 表示打印大于、等于该级别的日志,会输出了info、warn和error级别的日志

解决方案,这时要用到Filter

log4j.appender.File2 = org.apache.log4j.DailyRollingFileAppender
log4j.appender.File2.File =D:/logs/info.log
log4j.appender.File2.Threshold = INFO
log4j.appender.File2.layout = org.apache.log4j.PatternLayout
log4j.appender.File2.layout.ConversionPattern = [%d{yyyy-MM-dd HH:mm:ss:SSS}] [%p] [%t]- %l - %m%n
# 通过Filter 过滤设置最大,最小日志级别
log4j.appender.File2.filter.infoFilter = org.apache.log4j.varia.LevelRangeFilter
log4j.appender.File2.filter.infoFilter.LevelMin = INFO
log4j.appender.File2.filter.infoFilter.LevelMax = INFO

另一种解决方法:重写RollingFileAppender类中的isAsSevereAsThreshold方法:

// 默认实现 ,如果没有设置threshold属性则全部打印,否则打印大于等于threshold属性的日志。

public boolean isAsSevereAsThreshold(Priority priority) {
    return this.threshold == null || priority.isGreaterOrEqual(this.threshold);
}

public class MyInfoLog4jAppender extends RollingFileAppender {
@Override
    public boolean isAsSevereAsThreshold(Priority priority) {
        return priority != null && this.threshold == null && priority.isGreaterOrEqual(this.threshold) &&                this.getThreshold().isGreaterOrEqual(priority);
    }
}

在log4j.properties文件中修改log4j.appender.File1=com.xxx.MyInfoLog4jAppender即可。


rootcategory与rootlogger区别?

在配置文件有时候会看到这样写法:

log4j.rootLogger = debug,console,File1,File2
log4j.rootcategory= debug,console,File1,File2

通过官网查证:rootCategory 是废弃API , 建议使用rootLogger。为了保持兼容:Logger is a subclass of Category。所以请使用log4j.rootLogger。

到这里log4j的基本用法和常见问题就讲完了。如果遇到新的问题,可自行google.

接下来讲讲源码:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.12</version>
</dependency>
以1.7.12版本讲解。
心得:学习源码的方式,就是通过一个小demo,debug一下。
public class Slf4jTest {
   private static Logger logger = LoggerFactory.getLogger(Slf4jTest.class);
   public static void main(String[] args){
      if(logger.isDebugEnabled()){
         logger.debug("slf4j-log4j debug message");
      }
      if(logger.isInfoEnabled()){
         logger.debug("slf4j-log4j info message");
      }
      if(logger.isTraceEnabled()){
         logger.debug("slf4j-log4j trace message");
      }
   }
}

1,LoggerFactory.getLogger(Slf4jTest.class); 得到Logger的对象。

1.1 方法链:getILoggerFactory->performInitialization->bind->findPossibleStaticLoggerBinderPathSet()

findPossibleStaticLoggerBinderPathSet:从类路径中寻找org/slf4j/impl/StaticLoggerBinder.class类,可能有多个,比如你有log4j 和 logback 实现包,都包含StaticLoggerBinder.class。

reportMultipleBindingAmbiguity(staticLoggerBinderPathSet); 打印绑定情况如下

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [slf4j-log4j12-1.7.12.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [logback-classic-1.1.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]

说明:slf4j与其他实际的日志框架的集成jar包中,都会含有这样的一个org/slf4j/impl/StaticLoggerBinder.class类文件,并且提供一个ILoggerFactory的实现。

// the next line does the binding 随机选取绑定 StaticLoggerBinder

StaticLoggerBinder.getSingleton();

// 这里只有log4j ,所有返回Log4jLoggerFactory

return StaticLoggerBinder.getSingleton().getLoggerFactory();


1.2 iLoggerFactory.getLogger(name); 得到logger对象

->log4jLogger = LogManager.getLogger(name);

->logger = factory.makeNewLoggerInstance(name)->new Logger(name);

public class Log4jLoggerFactory implements ILoggerFactory {
    ConcurrentMap<String, Logger> loggerMap = new ConcurrentHashMap();
    ...
    public Logger getLogger(String name) {
        org.apache.log4j.Logger log4jLogger;
        if (name.equalsIgnoreCase("ROOT")) {
            log4jLogger = LogManager.getRootLogger();
        } else {
            // 得到log4jLogger对象
            // LogManager.java静态代码块会初始化Log4j,读取配置log4j.properties文件信息
            log4jLogger = LogManager.getLogger(name);
        }
        // 适配器模式转换
        Logger newInstance = new Log4jLoggerAdapter(log4jLogger);
        // 缓存Logger对象
        Logger oldInstance = (Logger)this.loggerMap.putIfAbsent(name, newInstance);
        return (Logger)(oldInstance == null ? newInstance : oldInstance);
    }
}

2 logger.debug("slf4j-log4j debug message"); 如何实现输出的呢?

查看调用链路

可以找到下面代码:

this.qw.write(this.layout.format(event));--> Writer out; out.write(string);

使用layout格式化传入的message, 用输出流写入。

this.qw.flush();

qw=QuietWriter(osw,handler)

OutputStreamWriter osw = new OutputStreamWriter(System.out);

到此logger.debug方法输出,主线源码已分析完。

更多参考:https://my.oschina.net/xianggao/blog/519199

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接