史上最全日志框架整理

最近准备看一下各个日志框架能否以及如何实现多线程下写入自定义文件。同时深入的整理并学习一下这些日志框架。

一、 目前主流日志框架主要包含以下几种:

  1. JUL
  2. Log4j
  3. Log4j2
  4. Conmmons-logging
  5. Slf4j
  6. Logback

二、框架简介

  1. JUL:
    JUL 全称 java.util.logging.Logger,JDK 自带的日志系统,从 JDK1.4 就有了。因为 Log4j 的存在,这个 Logger 一直沉默着,其实在一些测试性的代码中,JDK 自带的 Logger 比 Log4j 更方便。JUL 是自带具体实现的,与 Log4j、Logback 等类似,而不是像 Conmmons-logging、Slf4j 那样的日志接口封装。
  1. Log4j:
    <1> Log4j 是 Apache 的一个开放源代码项目,通过使用 Log4j ,我们可以控制日志信息输送的目的地是控制台、文件、数据库等;
    <2> 我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。
    <3> Log4j 有 7 种不同的log 级别,按照等级从低到高依次为:TRACE、DEBUG、INFO、WARN、ERROR、FATAL、OFF。如果配置为 OFF 级别,表示关闭 log。
    <4> Log4j 支持两种格式的配置文件:properties 和 xml。
    <5> 包含三个主要的组件:Logger、appender、Layout。
    <6> Log4j 产生的原因:
    在 JDK 1.3 及以前,Java 打日志依赖 System.out.println(), System.err.println() 或者 e.printStackTrace(),Debug 日志被写到 STDOUT 流,错误日志被写到 STDERR 流。这样打日志有一个非常大的缺陷,即无法定制化,且日志粒度不够细。Log4j 是在这样的环境下诞生的,它是一个里程碑式的框架,它定义的 Logger、Appender、Level 等概念如今已经被广泛使用。
    <7> 注意:2015年8月5日,项目管理委员会宣布 Log4j已达到使用寿命。建议用户使用 Log4j 升级到 Apache Log4j2。
  1. Log4j2:
    <1> Log4j2 是 Log4j 和 Logback 的改进版。据说采用了一些新技术(无锁异步等等),使得日志的吞吐量、性能比 Log4j 提高10倍,并解决了一些死锁的bug,而且配置更加简单灵活。
    <2> Log4j2 支持插件式结构,可以根据需要自行扩展 Log4j2,实现自己的 appender、logger、filter 等。
    <3> Log4j2 在配置文件中可以引用属性,还可以直接替代或传递到组件,而且支持 json 格式的配置文件。不像其他的日志框架,它在重新配置的时候不会丢失之前的日志文件。
    <4> Log4j2 利用 Java5 中的并发特性支持,尽可能地执行最低层次的加锁。解决了在 Log4j 中存留的死锁的问题。
    <5> Log4j2 是基于 LMAX Disruptor 库的。在多线程的场景下,和已有的日志框架相比,异步 logger 拥有 10 倍左右的效率提升。

Log4j2 与其他框架共同使用的体系结构

Log4j2 体系结构

  1. Conmmons-logging:
    <1> 之前叫 Jakarta Commons Logging,简称 JCL,是 Apache 提供的一个通用日志门面接口 API,可以让应用程序不再依赖于具体的日志实现工具。
    <2> Commons-logging 包中对其它一些日志工具,包括 Log4J、Avalon LogKit、JUL 等,进行了简单的包装,可以让应用程序在运行时,直接将 JCL API 打点的日志适配到对应的日志实现工具中。
    <3> Commons-logging 通过动态查找的机制,在程序运行时自动找出真正使用的日志库。这一点与 Slf4j 不同,Slf4j 是在编译时静态绑定真正的 Log 实现库。
    <4> 如果只引入 Apache Commons Logging,也没有通过配置文件 commons-logging.properties 进行适配器绑定,也没有通过系统属性或者 SPI 重新定义 LogFactory 实现,默认使用的就是 JDK 自带的 java.util.logging.Logger 来进行日志输出。
    <4> Commons-logging 提供简单的日志实现以及日志解耦功能。
    <5> Commons-logging 是 Apache commons 类库中的一员。Apache commons 类库是一个通用的类库,提供了基础的功能,比如说 commons-fileupload,commons-httpclient,commons-io,commons-codes 等。
  1. Commons-logging包里的包装类和简单实现列举如下:
    <1> org.apache.commons.logging.impl.Jdk14Logger,适配 JDK1.4 里的 JUL;
    <2> org.apache.commons.logging.impl.Log4JLogger,适配 Log4j;
    <3> org.apache.commons.logging.impl.LogKitLogger,适配 avalon-Logkit;
    <4> org.apache.commons.logging.impl.SimpleLog,Commons-logging 自带日志实现类,它实现了 Log 接口,把日志消息都输出到系统错误流 System.err 中;
    <5> org.apache.commons.logging.impl.NoOpLog,Commons-logging 自带日志实现类,它实现了 Log 接口,其输出日志的方法中不进行任何操作;
  1. Slf4j:
    <1> SLF4J 全称 The Simple Logging Facade for Java,简单日志门面,这个不是具体的日志解决方案,而是通过门面模式提供一些 Java Logging API,类似于 JCL。
    <2> SLF4J 提供的核心 API 是一些接口以及一个 LoggerFactory 的工厂类。在使用 SLF4J 的时候,不需要在代码中或配置文件中指定你打算使用哪个具体的日志系统,可以在部署的时候不修改任何配置即可接入一种日志实现方案,在编译时静态绑定真正的 Log 库。
    <3> 题外话,作者当时创建 SLF4J 的目的就是为了替代 Jakarta Commons Logging(JCL)。
    <4> 使用 SLF4J 时,如果你需要使用某一种日志实现,那么你必须选择正确的 SLF4J 的 jar 包的集合(各种桥接包)。
    <5> SLF4J 提供了统一的记录日志的接口,只要按照其提供的方法记录即可,最终日志的格式、记录级别、输出方式等通过具体日志系统的配置来实现,因此可以在应用中灵活切换日志系统。
  1. Slf4j 的一些桥接包:
    <1> slf4j-log4j12:可以使用log4j进行底层日志输出。
    <2> slf4j-jdk14:可以使用JUL进行日志输出。
  1. Logback:
    <1> Logback,一个“ 可靠、通用、快速而又灵活的 Java 日志框架 ”。
    <2> Logback当前分成三个模块:logback-core,logback- classic和logback-access:
    logback-core 模块为其他两个模块奠定了基础。
    logback-classic 模块可以被同化为 Log4j 的显着改进版本。logback-classic 本身实现了 slf4j-api,因此我们可以在 logback 和其他日志框架(如 Log4j 或java.util.logging(JUL))之间来回切换。
    logback-access 模块​​与 Servlet 容器(如 Tomcat 和 Jetty)集成,以提供 HTTP 访问日志功能。可以在 logback-core 之上轻松构建自己的模块。
    <3> Logback依赖配置文件logback.xml,当然也支持groovy方式。
    <4> Logback 的核心对象:Logger、Appender、Layout
    Logback 主要建立于 Logger、Appender 和 Layout 这三个类之上。
    Logger:日志的记录器,把它关联到应用的对应的 context 上后,主要用于存放日志对象,也可以定义日志类型、级别。Logger 对象一般多定义为静态常量.
    Appender:用于指定日志输出的目的地,目的地可以是控制台、文件、远程套接字服务器、 MySQL、 PostreSQL、Oracle 和其他数据库、 JMS 和远程 UNIX Syslog 守护进程等。
    Layout:负责把事件转换成字符串,格式化的日志信息的输出。
  1. Logback相比log4j,有很多很多的优点:
    <1> Logback的内核重写了,在一些关键执行路径上性能提升10倍以上。而且logback不仅性能提升了,初始化内存加载也更小了。
    <2> Logback经过了几年,数不清小时的测试。Logback的测试完全不同级别的。在作者的观点,这是简单重要的原因选择logback而不是log4j。
    <3> 当配置文件修改了,Logback-classic能自动重新加载配置文件。扫描过程快且安全,它并不需要另外创建一个扫描线程。这个技术充分保证了应用程序能跑得很欢在JEE环境里面。
    <4> RollingFileAppender在产生新文件的时候,会自动压缩已经打出来的日志文件。压缩是个异步过程,所以甚至对于大的日志文件,在压缩过程中应用不会受任何影响等。
    ……

三、这些日志框架的历史

  1. 1996 年早期,欧洲安全电子市场项目组决定编写它自己的程序跟踪 API( Tracing API )。经过不断的完善,这个 API 终于成为一个十分受欢迎的 Java 日志软件包,即 Log4j。后来 Log4j 成为 Apache 基金会项目中的一员。
  2. 期间 Log4j 近乎成了 Java 社区的日志标准。据说 Apache基金会还曾经建议 Sun 引入 Log4j 到 java 的标准库中,但 Sun 拒绝了。
  3. 2002 年 Java1.4 发布,Sun 推出了自己的日志库 JUL ( Java Util Logging ),其实现基本模仿了 Log4j 的实现。在 JUL 出来以前,Log4j 就已经成为一项成熟的技术,使得Log4j 在选择上占据了一定的优势。
  4. 接着,Apache 推出了 Jakarta Commons Logging,JCL 只是定义了一套日志接口(其内部也提供一个 Simple Log 的简单实现),支持运行时动态加载日志组件的实现,也就是说,在你应用代码里,只需调用 Commons Logging 的接口,底层实现可以是 Log4j,也可以是 Java Util Logging。
  5. 后来( 2006 年),Ceki Gülcü 不适应 Apache 的工作方式,离开了 Apache 。然后先后创建了 Slf4j (日志门面接口,类似于 Commons Logging )和 Logback (Slf4j的实现)两个项目,并回瑞典创建了 QOS 公司,QOS 官网上是这样描述 Logback 的:The Generic,Reliable Fast&Flexible Logging Framework (一个通用,可靠,快速且灵活的日志框架)。
  6. 现今,Java 日志领域被划分为两大阵营:Commons Logging 阵营和 Slf4j 阵营。
    Commons Logging 在 Apache 大树的笼罩下,有很大的用户基数。但有证据表明,形式正在发生变化。2013 年底有人分析了 GitHub 上 30000 个项目,统计出了最流行的 100 个 Libraries,可以看出 Slf4j 的发展趋势更好:
    (图片来自https://www.cnblogs.com/chenhongliang/p/5312517.html
各个日志框架的使用热度
  1. Apache 眼看有被 Logback 反超的势头,于 2012-07 重写了 Log4j1.x,成立了新的项目 Log4j2 , Log4j2 具有 Logback 的所有特性。

四、Java 常用日志框架关系

(图片来自 https://www.jianshu.com/p/bbbdcb30bba8

常用日志框架关系
  1. Commons-logging、Slf4j 遵循面向接口编程的原则,这两大框架是统一抽象出来的一些接口。
  2. JUL、Log4j、Log4j2、logback 等框架已定制了日志API以及自己的实现。
  3. 因此,基本上就是(Commons-logging/Slf4j)接口+(JUL/Log4j/Log4j2/logback 等)实现的方式来使用。
  4. Log4j2 与 Log4j1 发生了很大的变化,基本所有核心全都重构了一遍,因此Log4j2 不兼容 Log4j1。
  5. Commons Logging 和 Slf4j 是日志门面(门面模式是软件工程中常用的一种软件设计模式,也被称为正面模式、外观模式。它为子系统中的一组接口提供一个统一的高层接口,使得子系统更容易使用)。Log4j 和 Logback 则是具体的日志实现方案。可以简单的理解为接口与接口的实现,调用者只需要关注接口而无需关注具体的实现,做到解耦。
  6. 比较常用的组合使用方式是 Slf4j 与 Logback 组合使用,Commons Logging 与 Log4j 组合使用,但并不绝对,主要看自己选择。
  7. Logback 必须配合Slf4j 使用。由于 Logback 和 Slf4j 同一个作者,其兼容性不言而喻。

五、各个框架所需要的 jar 包

写在前面:
在使用各种日志框架的过程中,避免不了要引入各种依赖,而多种依赖之间很有可能会产生冲突,因此我们在引入一些日志的依赖库后,需要将其他依赖库中的一些依赖排除,而如果依赖较多时,一个个寻找比较麻烦,因此可以在项目根目录下进入CMD,用以下命令检查:

mvn dependency:tree
  1. Log4j:只需要导入一个依赖即可。(已停止更新)
<dependency>
   <groupId>log4j</groupId>
   <artifactId>log4j</artifactId>
   <version>1.2.17</version>
</dependency>
  1. Log4j2:则需要导入两个依赖。
<dependency>
   <groupId>org.apache.logging.log4j</groupId>
   <artifactId>log4j-core</artifactId>
   <version>2.13.3</version>
</dependency>
<dependency>
   <groupId>org.apache.logging.log4j</groupId>
   <artifactId>log4j-api</artifactId>
   <version>2.13.3</version>
</dependency>

不过如果我们使用 SpringBoot 开发的话,我们会看到在 SpringBoot 的 jar 包中已经包含了 Log4j2 的 log4j-api.jar 包,但是,这里的 log4j-api 是为了将 Log4j2 的接口适配到 Slf4j 上而存在的,如果想单独使用 Log4j2,则需要单独引入 log4j-api.jar 并且将 log4j-to-slf4j 下的 log4j-api 排除掉。
log4j-api 包含的是 .class 一堆接口,实际使用需要 log4j-core,log4j-core 包含 .class 与 .java 也就是源码。

  • SpringBoot 包含的 jar 包
  1. Logback:分为三个模块,需要同时导入三个依赖。
<dependency>
   <groupId>ch.qos.logback</groupId>
   <artifactId>logback-classic</artifactId>
   <version>1.2.3</version>
</dependency>
<dependency>
   <groupId>ch.qos.logback</groupId>
   <artifactId>logback-core</artifactId>
   <version>1.2.3</version>
</dependency>
<dependency>
   <groupId>ch.qos.logback</groupId>
   <artifactId>logback-access</artifactId>
   <version>1.2.3</version>
</dependency>

注:如果在 SpringBoot 下是不需要引入 Logback 的,因为在 spring-boot-starter-logging 中已经内置了该依赖。

  1. Slf4j:一般不会导入 slf4j-api jar 包,而是导入针对另一个具体实现的 jar 包,例如 slf4j-lo4j12,里面包含了 log4j:1.2.17。
<dependency>
   <groupId>org.slf4j</groupId>
   <artifactId>slf4j-api</artifactId>
   <version>1.7.25</version>
</dependency>

通常输出日志开销非常大,Slf4j 通过 {} 作为占位符的方式输出字符串,相比字符串拼接的方式,效率有显著的提升。

  1. Commons-logging:这个框架已经停止更新了。
<dependency>
   <groupId>commons-logging</groupId>
   <artifactId>commons-logging</artifactId>
   <version>1.2</version>
</dependency>

Commons-logging 能够选择使用 Log4j 还是 JUL,但是不依赖 Log4j、JUL 的API。
如果项目的 classpath 中包含了 Log4j 的类库,就会使用 Log4j,否则就使用
JUL。
使用 Commons-logging 能够灵活的选择使用那些日志方式,而且不需要修改源代码。

  1. JUL 不需要导入任何依赖,JDK 中已包含 JUL 的接口和具体实现。

下面是几种日志框架的详细内容以及使用场景。

六、JUL

由于小白人员对日志方面并不熟悉,因此先了解 JUL,并看一下底层源码实现,提升自己的基础了解,后续再对其余几个框架进行分析整理。

1. 在.../jre/lib下找到logging.properties
############################################################
#   Default Logging Configuration File 
#
# You can use a different file by specifying a filename
# with the java.util.logging.config.file system property.  
# For example java -Djava.util.logging.config.file=myfile
# 你可以通过 java.util.logging.config.file 属性指定一个文件名作为日志配置文件。
# 例如:java -Djava.util.logging.config.file=myfile
############################################################

############################################################
#   Global properties
############################################################

# "handlers" specifies a comma separated list of log Handler 
# classes.  These handlers will be installed during VM startup.
# Note that these classes must be on the system classpath.
# By default we only configure a ConsoleHandler, which will only
# show messages at the INFO and above levels.
# "handlers" :以逗号分隔,指定日志处理器。这些处理器将在VM启动期间安装。
# 注意:这些类必须位于系统的classpath路径下。
# 默认情况下,我们只配置一个ConsoleHandler(控制台),并且只显示INFO等级以上的信息。

handlers= java.util.logging.ConsoleHandler,java.util.logging.FileHandler

# To also add the FileHandler, use the following line instead.
# 如果需要使用FileHandler,则使用下面这行。
#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler

# Default global logging level.
# This specifies which kinds of events are logged across
# all loggers.  For any given facility this global level 
# can be overriden by a facility specific level 
# Note that the ConsoleHandler also has a separate level 
# setting to limit messages printed to the console.
# 默认全局日志级别
# 它指定所有的 logger 记录哪些类型的事件。
# 它可以被任何设备中的指定级别覆盖。
# 注意:ConsoleHandler具有单独的级别设置,用来限制打印到控制台的信息。

.level= INFO

############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
# 处理器特定配置文件
# 描述处理器的特定配置信息
############################################################

# default file output is in user's home directory. 
# 日志文件默认输出到用户主目录。
java.util.logging.FileHandler.pattern = %h/java%u.log

# 指定要写入到任意文件的近似最大量(以字节为单位)。如果该数为 0,则没有限制(默认为无限制)。
java.util.logging.FileHandler.limit = 50000

# 指定有多少输出文件参与循环(默认为 1)。
java.util.logging.FileHandler.count = 1

# 指定要使用的 Formatter(格式化) 类的名称(默认为 java.util.logging.XMLFormatter)。
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter

# Limit the message that are printed on the console to INFO and above.
# 限制打印在控制台上的信息级别(默认为INFO级别以上)。
java.util.logging.ConsoleHandler.level = INFO

# 指定控制台上要使用的 Formatter(格式化) 类的名称。
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

# Example to customize the SimpleFormatter output format 
# to print one-line log message like this:
#     <level>: <log message> [<date/time>]
# 自定义SimpleFormatter输出格式示例

# java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n

############################################################
# Facility specific properties.
# Provides extra control for each logger.
# 设备特定配置
# 为每个 logger 提供额外的配置
############################################################

# For example, set the com.xyz.foo logger to only log SEVERE
# messages:
# 例如:让 com.xyz.foo.level 下的 logger 只记录SEVERE级别及以上

com.xyz.foo.level = SEVERE

按自己情况进行修改后,将 logging.properties 放在 src/main/reources 目录下,即项目 classpath 目录下。

2. 使用 JUL

话不多说,先上代码

package com.cmos.javalog.originalLog;

import java.io.IOException;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

public class JavaLog {
    public static void main(String[] args) {
        try {
            testLog();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public static void testLog() throws IOException {
        /*
         * 为指定子系统查找或创建一个logger
         * name: logger名称
         * resourceBundleName: 子系统名称
         */
        Logger logger = Logger.getLogger("test");
        /*
         * 创建一个FileHandler,并指定属性
         * pattern: 日志文件存储路径
         * limit: 最大存储字节数
         * count: 日志文件数量
         * append: 是否可以追究
         */
        FileHandler fileHandler = new FileHandler("E:/logs/test.log",102400,1,true);
        logger.addHandler(fileHandler);
        // 指定 logger 输出级别
        logger.setLevel(Level.INFO);
        SimpleFormatter simpleFormatter = new SimpleFormatter();
        // 格式化log。
        // 这里放在 logger.addHandler() 之后并没有问题,只要保证在 logger.log() 方法之前即可。
        fileHandler.setFormatter(simpleFormatter);

        logger.log(Level.INFO,"这是一个INFO消息");
        logger.log(Level.WARNING,"这是一个WARNING警告");
        logger.log(Level.SEVERE,"这是一个SEVERE服务器消息");
    }
}

然后,让我们看一下 logger 具体是如何完成这些操作的。

1)首先我们调用 getLogger 方法去获取一个 logger,而 getLogger 方法重载为了两个方法:

// 方法1:
@CallerSensitive
public static Logger getLogger(String name) {
    return demandLogger(name, null, Reflection.getCallerClass());
}
// 方法2:
@CallerSensitive
public static Logger getLogger(String name, String resourceBundleName) {
    // 获取调用者类
    Class<?> callerClass = Reflection.getCallerClass();
    Logger result = demandLogger(name, resourceBundleName, callerClass);
    
    // setupResourceInfo() 可以抛出 MissingResourceException 或 IllegaArgumentException,
    // 我们不得不在这里设置一个 callers 类加载器
    // 以防上面的 demandLogger 发现以前已经创建好的 Logger。
    // 这是又可能发生的,例如,如果我们先调用了 Logger.getLogger(name) ,
    // 随后调用 Logger.getLogger(name,resourceBundleName)
    // 在这种情况下我们无法保证我们存起来的是一个正确的类加载器,所以我们需要在这里设置它。
    result.setupResourceInfo(resourceBundleName, callerClass);
    return result;
}

2)但是殊途同归,这两个重载方法都是去调用了 demandLogger(name, resourceBundleName, callerClass) ,那让我们看一下 Logger 下的 demandLogger 方法的内容:

private static Logger demandLogger(String name, String resourceBundleName, Class<?> caller) {
    // 这里获取的 manager 是单例的,如果其他线程已经初始化了一个 LogManager ,
    // 那我们就直接获取拿来使用
    LogManager manager = LogManager.getLogManager();
    SecurityManager sm = System.getSecurityManager();
    // 这里进行安全检查
    if (sm != null && !SystemLoggerHelper.disableCallerCheck) {
        // 若 getClassLoader() 为空,说明该 classLoader 是 bootstrap classLoader ,
        // 因此调用 demandSystemLogger(name,resourceBundleName)
        if (caller.getClassLoader() == null) {
            return manager.demandSystemLogger(name, resourceBundleName);
        }
    }
    // 上述不成立,就调用 LoggerManager 的 demandLogger(name,resourceBundleName,caller)
    return manager.demandLogger(name, resourceBundleName, caller);
}

3)同样的殊途同归,通过追溯源码发现 demandSystemLogger() 最后还是是调用了 demandLogger() ,因此我们只需要看一下 demandLogger() 是如何实现的即可:

// 如果已经有了给定名称的 logger ,则返回该 logger。
// 否则就在 LoggerManager 的全局命名空间注册一个新的 logger 实例
// 并且这个方法永远不会返回空实例
Logger demandLogger(String name, String resourceBundleName, Class<?> caller) {
    // 这里根据给定 name 去获取 logger
    Logger result = getLogger(name);
    if (result == null) {
        Logger newLogger = new Logger(name, resourceBundleName, caller, this);
        do {
            // 注册一个新 logger 实例
            if (addLogger(newLogger)) {
                return newLogger;
            }
            
            result = getLogger(name);
        } while (result == null);
    }
    return result;
}

4)至此我们已经了解到如何获取的 logger 对象,接下来则是对 logger 的一些配置,用来覆盖上面提到的 logging.properties 中的一些默认配置。

5)接下来则是正式进行日志信息输出:

这里可以看到 logger.log() 方法有很多重载,当然最常用的则是 log(level,msg) 以及 log(level,msg,params)
前者作为静态日志信息输出,后者则作为动态日志信息输出

logger.log()

6)以 log(level,msg,params) 为例,看一下日志信息如何进行输出:

public void log(Level level, String msg, Object params[]) {
    if (!isLoggable(level)) {
        return;
    }
    // 创建一个 LogRecord 对象,并将 level\msg\params 封入 lr 
    LogRecord lr = new LogRecord(level, msg);
    lr.setParameters(params);
    // 调用 doLog(lr)
    doLog(lr);
}

// 为 logging 日志构建的私有化方法,用来向 lr 中填入 logger 的一些信息
private void doLog(LogRecord lr) {
    // 填入 loggerName
    lr.setLoggerName(name);
    // 获取有效绑定对象
    final LoggerBundle lb = getEffectiveLoggerBundle();
    // 获取绑定资源以及资源名称
    final ResourceBundle  bundle = lb.userBundle;
    final String ebname = lb.resourceBundleName;
    if (ebname != null && bundle != null) {
        lr.setResourceBundleName(ebname);
        lr.setResourceBundle(bundle);
    }
    // 调用 log(lr)
    log(lr);
}

// 所有记录日志的操作最终都会使用这个方法
public void log(LogRecord record) {
    if (!isLoggable(record.getLevel())) {
        return;
    }
    Filter theFilter = filter;
    if (theFilter != null && !theFilter.isLoggable(record)) {
        return;
    }

    // 将这个日志记录发送给我们所有的 handler ,然后发送给父类的 handler ,直到树的顶端。
    Logger logger = this;
    while (logger != null) {
        for (Handler handler : logger.getHandlers()) {
            handler.publish(record);
        }
        // 判断是否有父类
        if (!logger.getUseParentHandlers()) {
            break;
        }
        // 获取父类对象
        logger = logger.getParent();
    }
}

7)之后则是我们最开始配置的 FileHandler 以及其他自定义 Handler 或默认 Handler 来进行对日志记录的格式化以及打印、输出到文件等操作。

但是这里有个疑问,我们只是将这条记录交给了 handlers ,但是这些 handler 又是何时进行执行的呢?

8)Handler 的执行

// 在以上流程中唯一使用到 handler 的位置就是 handler.publish(record);
// 因此,我沿着 Handler 的子类向下寻找,发现 Handler 类有一个 StreamHandler 子类
// 而恰巧 StreamHandler 下含有 FileHandler ConsoleHandler 等子类
// 因此我到 StreamHandler 下找到了 publish方法
@Override
    public synchronized void publish(LogRecord record) {
        if (!isLoggable(record)) {
            return;
        }
        String msg;
        try {
            msg = getFormatter().format(record);
        } catch (Exception ex) {
            reportError(null, ex, ErrorManager.FORMAT_FAILURE);
            return;
        }

        try {
            // 如果这是向 OutputStream 中写入的第一条记录,
                // 则在写入日志记录之前,先将 formatter 的 head 写入
            if (!doneHeader) {
                writer.write(getFormatter().getHead(this));
                doneHeader = true;
            }
            // 而后写入 msg
            writer.write(msg);
        } catch (Exception ex) {
            reportError(null, ex, ErrorManager.WRITE_FAILURE);
        }
    }

9)因此 Handler 的执行就是在调用 publish 调用过程中进行的,最后总结一下:

JUL:主要有以下特点
<1> 相同名字的 Logger 全局唯一;
<2> 配置文件默认使用 jre/lib/logging.properties,日志级别默认为 INFO;
<3> 可以通过系统属性 java.util.logging.config.file 指定路径覆盖系统默认文件;
<4> 日志级别由高到低依次为:SEVERE(严重)、WARNING(警告)、INFO(信息)、CONFIG(配置)、FINE(详细)、FINER(较详细)、FINEST(非常详细)。另外还有两个全局开关:OFF「关闭日志记录」和ALL「启用所有消息日志记录」;
<5> logging.properties 文件中,默认日志级别可以通过 .level = ALL 来控制,也可以基于层次命名空间来控制,按照 Logger 名字进行前缀匹配,匹配度最高的优先采用;日志级别只认大写;
<6> 原生 Logger 通过 handler 来完成实际的日志输出,可以通过配置文件指定一个或者多个 hanlder,多个 handler之间使用逗号分隔;handler 上也有一个日志级别,作为该 handler 可以接收的日志最低级别,低于该级别的日志,将不进行实际的输出;handler 上可以绑定日志格式化器,比如 java.util.logging.ConsoleHandler 就是使用的 String.format 来支持的。

注意:一般使用圆点分隔的层次命名空间来命名 Logger;Logger 名称可以是任意的字符串,但是它们一般应该基于被记录组件的包名或类名,如 java.net 或 javax.swing;

至此,我们对 log 就有了一定的认识,了解了 log 的基本工作流程。所以接下来就开始着手分析各个日志框架以及它们如何实现多线程下写入自定义文件。

3. JUL实现多线程写入

1)自定义日志格式

在这里我只需要输出我想输出的数据,不需要其他乱七八糟的内容,因此我需要简单的自定义一下输出格式。

package com.cmos.javalog.jul;

import java.util.logging.Formatter;
import java.util.logging.LogRecord;

/**
 * 继承 Formatter   实现 format 方法
 */
public class MyFormatter extends Formatter {
    @Override
    public String format(LogRecord record) {
        return record.getMessage();
    }
}

2)修改配置文件

## 将 FileHandler.formatter 的值替换为自定义格式
java.util.logging.FileHandler.formatter = com.cmos.javalog.jul.MyFormatter

3)多线程环境下调用 log

public static void main(String[] args) throws IOException {
    // 设置配置文件路径,使用自定义的配置文件
    String path = JavaLog.class.getClassLoader().getResource("logging.properties").getPath();
    System.setProperty("java.util.logging.config.file", path);
    // 多线程下使用同一个 logger 对象
    Logger logger = Logger.getLogger("moreThread2File");
    new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            // System.lineSeparator 系统换行符
            logger.log(Level.INFO, i + " thread-1" + System.lineSeparator());
        }
    }).start();
    new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            logger.log(Level.INFO, i + " thread-2" + System.lineSeparator());
        }
    }).start();
    new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            logger.log(Level.INFO, i + " thread-3" + System.lineSeparator());
        }
    }).start();
}

4)输出结果

日志输出结果
4. 总结

JUL属于较为简单的日志实现,不需要单独引用 jar 包,支持多线程下输出数据到同一文件中,但配置较为麻烦,在平时可使用其基本功能进行测试或简单日志输出,不推荐在对日志有进一步需求时使用。

七、Log4j 和 Log4j2

1. Log4j 单独使用

1) 首先引入 log4j 依赖

<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

2) 然后在 src/main/resources 下构建 log4j.properties 配置文件

### 配置根 Logger ###
### debug 是日志输出级别,stdout\D\E 是日志输出位置的别名 ###
log4j.rootLogger = debug,stdout,D,E

### 输出信息到控制抬 ###
### stdout 为 ConsoleAppender ###
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
### Target 为 System.out 指定控制台输出(out 为输出白色字体,err 为输出红色字体) ###
log4j.appender.stdout.Target = System.out
### layout 为 PatternLayout 灵活指定布局 ###
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
### layout.ConversionPattern 为转换格式,值为格式化信息 ### 
log4j.appender.stdout.layout.ConversionPattern = %m

### 输出DEBUG 级别以上的日志到=E://logs/error.log ###
log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
### File 为 日志文件位置 ###
log4j.appender.D.File = E://logs/log.log
### Append 为 是否可以追加 ###
log4j.appender.D.Append = true
### Threshold 为 输出该级别以上的日志 ###
log4j.appender.D.Threshold = DEBUG 
log4j.appender.D.layout = org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n

### 输出ERROR 级别以上的日志到=E://logs/error.log ###
log4j.appender.E = org.apache.log4j.DailyRollingFileAppender
log4j.appender.E.File =E://logs/error.log 
log4j.appender.E.Append = true
log4j.appender.E.Threshold = ERROR 
log4j.appender.E.layout = org.apache.log4j.PatternLayout
log4j.appender.E.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n

3) 最后在多线程情况下输出

package com.cmos.javalog.log4j;

import org.apache.log4j.Logger;

public class Log4JTest {
    private static Logger logger = Logger.getLogger(Log4JTest.class);
    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                logger.debug(i + " thread-1" + System.lineSeparator());
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                logger.debug(i + " thread-2" + System.lineSeparator());
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                logger.debug(i + " thread-3" + System.lineSeparator());
            }
        }).start();
    }
}

4) 输出结果

输出结果

5) 但在这种情况下,如果我需要自定义文件名称的话,则需要加上以下代码

FileAppender fileAppender = new FileAppender(new PatternLayout("%m"), "E:/logs/newLog.log");
logger.addAppender(fileAppender);

6) 结果:

结果
2. Log4j2 单独使用

1) 引入依赖

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.13.3</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.13.3</version>
</dependency>

2) 然后在 src/main/resources 下构建 log4j2.xml 配置文件
<1> Log4j2 中不再支持像 Log4j 中的 .properties 后缀的文件配置方式,Log4j2 配置文件后缀名只能为".xml",".json"或者".jsn"。
<2> 系统选择配置文件的优先级(从先到后)如下:
a. classpath下的名为log4j2-test.json 或者log4j2-test.jsn的文件.
b. classpath下的名为log4j2-test.xml的文件.
c. classpath下名为log4j2.json 或者log4j2.jsn的文件.
d. classpath下名为log4j2.xml的文件.

<?xml version="1.0" encoding="UTF-8"?>
<!--configuration:根节点  status:定义log4j2本身打印日志的级别  
monitorinterval:指定log4j2自动重新配置的监测间隔时间,单位是s,最小是5s -->
<Configuration status="INFO" monitorinterval="5" strict="true" name="LogConfig">
    <!-- configuration有两个子节点:appenders loggers -->
    <Appenders>
        <!-- Console:定义输出到控制台的Appender  name:Console的属性,指定Appender的名字.
        target:Console的属性,SYSTEM_OUT 或 SYSTEM_ERR,一般只设置默认:SYSTEM_OUT.-->
        <Console name="Console" target="SYSTEM_OUT">
            <!--只接受程序中INFO级别及以上的日志进行处理-->
            <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
            <!--PatternLayout:Console的子节点,输出格式,不设置默认为:%m%n.-->
            <PatternLayout pattern="%m"/>
        </Console>
        <!--RollingFile:定义超过指定大小自动删除旧的创建新的的Appender.  
        name:RollingFile的属性,指定Appender的名字.
        fileName:RollingFile的属性,指定输出日志的目的文件带全路径的文件名.  
        filePattern:RollingFile的属性,指定新建日志文件的名称格式.-->
        <RollingFile name="RollingFileINFO" fileName="./logs/INFO.log"
                     filePattern="logs/$${date:yyyy-MM}/INFO-%d{yyyy-MM-dd}-%i.log.gz">
            <!--Filters:RollingFile的子节点,决定日志事件能否被输出。过滤条件有三个值:
            ACCEPT(接受), DENY(拒绝) or NEUTRAL(中立)-->
            <Filters>
                <!--只接受INFO级别的日志,其余的全部拒绝处理 onMatch:符合  onMismatch:不符合-->
                <ThresholdFilter level="INFO"/>
                <ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL"/>
            </Filters>
            <!--PatternLayout:RollingFile的子节点,输出格式,不设置默认为:%m%n.-->
            <PatternLayout
                    pattern="[%d{yyyy-MM-dd HH:mm:ss}] %-5level %class{36} %L %M - %msg%xEx"/>
            <!--Policies:RollingFile的子节点,指定滚动日志的策略,
            就是什么时候进行新建日志文件输出日志.-->
            <Policies>
                <!--SizeBasedTriggeringPolicy:Policies子节点,基于指定文件大小的滚动策略,
                size属性用来定义每个日志文件的大小.-->
                <SizeBasedTriggeringPolicy size="500 MB"/>
                <!--TimeBasedTriggeringPolicy:Policies子节点,基于时间的滚动策略,
                interval属性用来指定多久滚动一次,默认是1hour。
                modulate=true用来调整时间:比如现在是早上3am,interval是4,
                那么第一次滚动是在4am,接着是8am,12am...而不是7am.-->
                <TimeBasedTriggeringPolicy/>
            </Policies>
        </RollingFile>
        <!--Routing:将日志事件分类,按条件分配给子appender  name:RoutingAppender名称-->
        <Routing name="Routing">
            <!--Routes:包含一个或多个路由声明来标识选择Appenders的标准。 pattern:上下文模板名称-->
            <Routes pattern="$${ctx:log4jFile}">
                <Route>
                    <!--File:用于保存文件。 name:当前Appender的命名  fileName:包含路径的文件名-->
                    <File name="file" fileName="E:/logs/${ctx:log4jFile:-other.log}">
                        <PatternLayout pattern="%m"></PatternLayout>
                    </File>
                </Route>
            </Routes>
        </Routing>

    </Appenders>
    <!-- 然后定义logger,只有定义了logger并引入的appender,appender才会生效 -->
    <!--Loggers节点,常见的有两种:Root和Logger.-->
    <Loggers>
        <!--root:指定项目的根日志,如果没有单独指定Logger,那么就会默认使用该Root日志输出
        level:日志输出级别,共有8个级别,按照从低到高为:
        All < Trace < Debug < Info < Warn < Error < Fatal < OFF.-->
        <Root level="INFO">
            <!--appender-ref:用来指定该日志输出到哪个Appender,通过ref指定.-->
            <Appender-ref ref="Console"/>
            <Appender-ref ref="RollingFileINFO"/>
        </Root>
        <!--logger:用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。
        name:用来指定该Logger所适用的类或者类所在的包全路径,继承自Root节点.  additivity:是否叠加-->
        <Logger name="com.cmos.javalog" level="INFO"></Logger>
        <!--这个name要与java中LogManager.getLogger("log4jFile")相匹配-->
        <Logger name="log4jFile" level="INFO" >
            <AppenderRef ref="Routing"/>
        </Logger>
    </Loggers>
</Configuration>

多线程下测试输出

package com.cmos.javalog.log4j2;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Log4j2Test {
    private static Logger logger = LogManager.getLogger("log4jFile");
    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ThreadContext.put("log4jFile", "log4j2.log");
                logger.warn(i + " thread-1" + System.lineSeparator());
                ThreadContext.clearAll();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ThreadContext.put("log4jFile", "log4j2.log");
                logger.warn(i + " thread-2" + System.lineSeparator());
                ThreadContext.clearAll();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ThreadContext.put("log4jFile", "log4j2.log");
                logger.warn(i + " thread-3" + System.lineSeparator());
                ThreadContext.clearAll();
            }
        }).start();
    }
}

输出结果

Log4j2输出结果

总结:
Log4j2 相比于 Log4j 提升的不止一星半点,首先Log4j2舍弃properties而只使用xml配置文件,大大的提高了可读性。而且 Log4j2 的效率也远超 Log4j 和 Logback,个人感觉是目前最好用的日志实现。

八、Commons-logging

1. Commons-logging + JUL

写在前面
目前 Commons-logging + JUL 在使用时只能通过修改 logging.properties 配置文件进行控制日志信息输出级别及路径。如果大家知道更好的方法,请留言或私信,多谢!

引入依赖
上面说过,Commons-logging 会自动查找 classpath 下是否包含了 Log4j 的类库,若没有,则使用 JUL,因此我们只需要引入 commons-logging.jar 即可,同时需要排除 SpringBoot 启动依赖中的日志依赖。

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>

在 src/main/resources 下创建 commons-logging.properties 与 logging.properties
注: logging.properties 可依旧使用上面的配置

## commons-logging.properties

## 指定 Commons-logging 使用 JUL 实现
org.apache.commons.logging.Log=org.apache.commons.logging.impl.Jdk14Logger 

Commons-logging 支持很多日志实现,针对 JUL 我们需要使用 Jdk14Logger。

-
Commons-logging 支持的日志实现

单线程下测试输出
这里我们先不考虑多线程情况,只做最简单的使用。

package com.cmos.javalog.jcl;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;

public class CommonsLoggingJUL {
    public static void main(String[] args) throws IOException {
        Log log = LogFactory.getLog("CommonsJUL");
        log.trace("This is TRACE COMMONS-JUL");
        log.debug("This is DEBUG COMMONS-JUL");
        log.info("This is INFO COMMONS-JUL");
        log.warn("This is WARN COMMONS-JUL");
        log.error("This is ERROR COMMONS-JUL");
        log.fatal("This is FATAL COMMONS-JUL");
    }
}

日志结果

十一月 16, 2020 3:38:11 下午 com.cmos.javalog.jcl.CommonsLoggingJUL main
信息: This is INFO COMMONS-JUL
十一月 16, 2020 3:38:11 下午 com.cmos.javalog.jcl.CommonsLoggingJUL main
警告: This is WARN COMMONS-JUL
十一月 16, 2020 3:38:11 下午 com.cmos.javalog.jcl.CommonsLoggingJUL main
严重: This is ERROR COMMONS-JUL
十一月 16, 2020 3:38:11 下午 com.cmos.javalog.jcl.CommonsLoggingJUL main
严重: This is FATAL COMMONS-JUL

可以看出,只能输出 INFO 级别及以上的日志信息,而我们如果想修改日志输出级别,目前我已知的可以通过修改 logging.properties 的值来实现,但每次运行前都需要修改。

多线程下测试输出

## logging.properties

handlers= java.util.logging.ConsoleHandler,java.util.logging.FileHandler
.level= INFO
java.util.logging.FileHandler.pattern = E:/logs/CommonsJUL.log
java.util.logging.FileHandler.limit = 102400000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = com.cmos.javalog.jul.MyFormatter
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
java.util.logging.SimpleFormatter.format=%4$s: %5$s
package com.cmos.javalog.jcl;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;

public class CommonsLoggingJUL {
    public static void main(String[] args) throws IOException {
        // 设置JUL配置文件路径,使用自定义的配置文件
        String path = CommonsLoggingJUL.class.getClassLoader()
                             .getResource("logging.properties").getPath();
        System.setProperty("java.util.logging.config.file", path);
        Log log = LogFactory.getLog("CommonsJUL");
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                log.info(i + " thread-1" + System.lineSeparator());
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                log.info(i + " thread-2" + System.lineSeparator());
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                log.info(i + " thread-3" + System.lineSeparator());
            }
        }).start();
    }
}

输出结果

输出结果

同样的,对于写入自定义文件同样需要通过 logging.properties 的配置,若要修改日志文件名,则每次运行前都需要修改。

总结:
使用 JCL + JUL 极其不方便,JCL 并未提供 JUL 中对 FileHandler 等属性的配置方法,因此每次使用都需要修改配置文件,在多线程运行情况下不建议使用。

2. Commons-logging + Log4j2

引入依赖

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.13.3</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.13.3</version>
</dependency>

在 src/main/resources 下创建 commons-logging.properties 以及 log4j2.xml
注:其中 log4j2.xml 继续使用上面的配置。

#################### commons-logging.properties ####################
## 指定 Commons-logging 使用 Log4j 实现
org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JLogger

多线程下测试输出
我们只需要用 Commons-logging 的 Log 来替换 Log4j2 的 Logger 即可。

package com.cmos.javalog.jcl;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class CommonsLoggingLog4j2 {
    private static Log log = LogFactory.getLog("log4jFile");

    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ThreadContext.put("log4jFile", "log4j2.log");
                log.warn(i + " thread-1" + System.lineSeparator());
                ThreadContext.clearAll();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ThreadContext.put("log4jFile", "log4j2.log");
                log.warn(i + " thread-2" + System.lineSeparator());
                ThreadContext.clearAll();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ThreadContext.put("log4jFile", "log4j2.log");
                log.warn(i + " thread-3" + System.lineSeparator());
                ThreadContext.clearAll();
            }
        }).start();
    }
}

输出结果

输出结果

总结:
使用 Commons-logging 其实就是使用它的 log 对象,在这个业务里,说实话没有感觉到比单独使用 Log4j2 来的方便。

九、Slf4j

1. Slf4j + JUL

引入依赖

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-jdk14</artifactId>
    <version>1.7.25</version>
</dependency>

多线程下运行

package com.cmos.javalog.slf4j;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Slf4j2JUL {
    public static void main(String[] args) {
        // 设置配置文件路径,使用自定义的配置文件
        System.setProperty("java.util.logging.config.file", Slf4j2JUL.class.getClassLoader()
                    .getResource("logging.properties").getPath());

        Logger logger = LoggerFactory.getLogger("Slf4j2JUL");
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                logger.info("{}", i + " thread-1" + System.lineSeparator());
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                logger.info("{}", i + " thread-2" + System.lineSeparator());
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                logger.info("{}", i + " thread-3" + System.lineSeparator());
            }
        }).start();
    }
}

输出结果

输出结果

总结:
Slf4j + JUL 与 Commons-logging + JUL 使用几乎相同,优缺点也差不多,毕竟 Slf4j 与 Commons-logging 都只是一个门面,具体的实现都是使用的 JUL。不同之处则是相比于 Commons-logging ,Slf4j 不需要通过配置文件来选择具体实现框架,而是通过添加不同的依赖,在这一点上,二者也是各有千秋,但是 Slf4j 采用的 {} 作为占位符则更节省系统资源。

2. Slf4j + Log4j2

引入依赖

<!-- slf4j-api -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<!-- log4j2 -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.13.3</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.13.3</version>
</dependency>
<!-- slf4j与log4j2的桥梁 -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>2.13.3</version>
</dependency>

在 src/main/resources 下添加 log4j2.xml
注: 这里继续使用上面的 log4j2.xml

多线程下运行测试

package com.cmos.javalog.slf4j;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Slf4j2Log4j2 {

    private static Logger logger = LoggerFactory.getLogger("log4jFile");

    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ThreadContext.put("log4jFile", "Slf4j2Log4j2.log");
                logger.warn("{}", i + " thread-1" + System.lineSeparator());
                ThreadContext.clearAll();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ThreadContext.put("log4jFile", "Slf4j2Log4j2.log");
                logger.warn("{}", i + " thread-2" + System.lineSeparator());
                ThreadContext.clearAll();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ThreadContext.put("log4jFile", "Slf4j2Log4j2.log");
                logger.warn("{}", i + " thread-3" + System.lineSeparator());
                ThreadContext.clearAll();
            }
        }).start();
    }
}

输出结果

输出结果

总结:
这里也明显看的出来,与上面的 Commons-logging + Log4j2 几乎没有任何区别。在使用日志时,绝大多数的差异是由日志具体实现框架来决定的,不同的实现也会有不同的效率与结果。

3. Slf4j + Logback

引入依赖

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.2.3</version>
</dependency>

在 src/main/resources 下添加 logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--configuration 子节点为 appender、logger、root
scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。
           当scan为true时,此属性生效。默认的时间间隔为1分钟。
debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。-->
<configuration scan="true" scanPeriod="5 seconds" debug="false">
    <!--用于区分不同应用程序的记录-->
    <contextName>edu-cloud</contextName>
    <!--日志文件所在目录-->
    <property name="LOG_HOME" value="E:/logs"/>

    <!--控制台-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 
            %logger输出日志的logger名 %msg:日志消息,%n是换行符 -->
            <pattern>
                [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %-5level %logger{36} : %msg
            </pattern>
            <!--解决乱码问题-->
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!--滚动文件-->
    <appender name="infoFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- ThresholdFilter:临界值过滤器,过滤掉 TRACE 和 DEBUG 级别的日志 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_HOME}/log.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory><!--保存最近30天的日志-->
        </rollingPolicy>
        <encoder>
            <charset>UTF-8</charset>
            <pattern>
                [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %-5level %logger{36} : %msg
            </pattern>
        </encoder>
    </appender>

    <!--SiftingAppender:筛选并分配  根据discriminator 值筛选<sift>中的appender-->
    <appender name="logbackFile" class="ch.qos.logback.classic.sift.SiftingAppender">
        <!--discriminator不指定具体class时,默认使用MDCBasedDiscriminator,
        因此才能在下方Java代码中使用MDC进行传值-->
        <discriminator>
            <key>logFileName</key>
            <defaultValue>default</defaultValue>
        </discriminator>
        <sift>
            <appender name="File-${logFileName}" class="ch.qos.logback.core.FileAppender">
                <file>E:/logs/${logFileName}.log</file>
                <append>true</append>
                <encoder charset="UTF-8">
                    <Pattern>%msg</Pattern>
                </encoder>
            </appender>
        </sift>
    </appender>

    <!--这里如果是info,spring、mybatis等框架则不会输出:TRACE < DEBUG < INFO <  WARN < ERROR-->
    <!--root是所有logger的祖先,均继承root,如果某一个自定义的logger没有指定level,就会寻找
    父logger看有没有指定级别,直到找到root。-->
    <root level="debug">
        <appender-ref ref="console"/>
        <appender-ref ref="infoFile"/>

    </root>

    <logger name="com.cmos.javalog" level="INFO">
        <appender-ref ref="logbackFile"/>
    </logger>

    <!--为某个包单独配置logger

    比如定时任务,写代码的包名为:com.seentao.task
    步骤如下:
    1、定义一个appender,取名为task(随意,只要下面logger引用就行了)
    appender的配置按照需要即可

    2、定义一个logger:
    <logger name="com.seentao.task" level="DEBUG" additivity="false">
      <appender-ref ref="task" />
    </logger>
    注意:additivity必须设置为false,这样只会交给task这个appender,
    否则其他appender也会打印com.seentao.task里的log信息。

    3、这样,在com.seentao.task的logger就会是上面定义的logger了。
    private static Logger logger = LoggerFactory.getLogger(Class1.class);
    -->

</configuration>

多线程下运行测试

package com.cmos.javalog.slf4j;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

public class Slf4j2Logback {
    private static Logger logger = LoggerFactory.getLogger(Slf4j2Logback.class);

    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                MDC.put("logFileName","Slf4j2Logback");
                logger.info("{}", i + " thread-1" + System.lineSeparator());
                MDC.remove("logFileName");
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                MDC.put("logFileName","Slf4j2Logback");
                logger.info("{}", i + " thread-2" + System.lineSeparator());
                MDC.remove("logFileName");
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                MDC.put("logFileName","Slf4j2Logback");
                logger.info("{}", i + " thread-3" + System.lineSeparator());
                MDC.remove("logFileName");
            }
        }).start();
    }
}

输出结果

输出结果

总结:
Logback 中使用 SiftingAppender ,与 Log4j2 中的 RoutingAppender功能相同 ,Logback并为之匹配了一个 MDC 类用于传递筛选参数,在这个业务中,相比 Log4j 来说,Logback 更加解耦,但效率比不上 Log4j2。

本文如有不足或有误之处,欢迎大家指正,私信或留言均可。

本文参考:
https://www.jianshu.com/p/bbbdcb30bba8
https://blog.csdn.net/waitgod/article/details/78750184
https://www.cnblogs.com/chenhongliang/p/5312517.html
http://logging.apache.org/log4j/2.x/manual/appenders.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 一、聊聊java中混乱的日志体系 💂‍♀️波妞:先提个问题,你知道哪些日志框架? 💂‍♀️波妞:呃。。。我说说搜我...
    c梦2019阅读 178评论 0 2
  • 对于Java的日志框架,你也许会经常看到这些名词: Log4j、Log4j2 Logback Slf4j JCL ...
    NoahU阅读 3,881评论 0 15
  • 作为Java开发人员,对于日志记录框架一定非常熟悉。而且几乎在所有应用里面,一定会用到各种各样的日志框架用来记录程...
    意识流丶阅读 13,647评论 0 13
  • 在项目开发过程中,我们可以通过 debug 查找问题。而在线上环境我们查找问题只能通过打印日志的方式查找问题。因此...
    Java架构阅读 3,407评论 2 41
  • [TOC] Java 日志框架解析:设计模式、性能 在平常的系统开发中,日志起到了重要的作用,日志写得好对于线上问...
    albon阅读 3,946评论 1 8