QZ-BOOKQZ-BOOK
首页
  • Java日志
  • 2024
  • 2025
关于我
首页
  • Java日志
  • 2024
  • 2025
关于我
  • Java日志

Java日志

1基础概念

1.1日志门面

  • JCL 一套日志的 API ,在不改变代码的情况下,接入不同的第三方日志框架,切换不同的具体实现,[常用 log4j 作为实现]
  • SLF4j

1.2日志框架

  • JUL java util logging
  • Log4j
  • Log4j2
  • Logback

日志门面是日志规范接口,日志框架是对日志门面的一种实现,类似JDBC。门面就是标准[specification]

2 JUL

2.1JUL 是什么

JUL 全称 Java Util Logging,它是Java的原生的日志框架,使用时不需要另外引入第三方类库,相对其他的框架使用方便。适用于小型项目应用中。

2.2 JUL 组件

Logger:被成为记录器,应用程序通过获取logger对象,用其 API 来发布日志信息。logger通常被认为是访问日志系统的入口程序。

Handler:处理器,每个Logger都会关联一个或者一组Handler,Logger会将日志交给关联的Handler去处理,有Handler负责将日志做几路。Handler具体实现了是指的输出位置,比如可以输出到控制台或者文件中,等等。

Filter:过滤器,根据需要定制哪些信息回被记录,哪些心事会被略过。

Formatter:格式化组件,让负责对日志中的数据和信息进行转换和格式化,所以他决定了日志最终的输出形式。

Level:日志输出级别,每条日志消息都有一个关联的级别,设置输出级别,设置debug级别信息,用来展现最终所呈现的日志信息,那么info级别的信息就会被忽略。根据不同的需求,取设置不同的级别。

入门案例


import java.util.logging.Level;  
import java.util.logging.Logger;  
/**  
 * @author chuan  
 * @Classname JULTest  
 * @Description  
 * @Version 1.0.0  
 * @Date 2024/11/11 23:21  
 */public class JULTest {  
    @Test  
    public void testLogging() {  
        Logger logger = java.util.logging.Logger.getLogger(JULTest.class.getName());  
        // 直接指定日志级别输出日志  
        logger.info("This is a test log message.");  
        // 打印日志,设置级别,输出日志  
        logger.log(Level.INFO, "This is a test log message with level.");  
    }  
}

2.3 日志级别

public static final Level OFF = new Level("OFF",Integer.MAX_VALUE, defaultBundle);  
public static final Level SEVERE = new Level("SEVERE",1000, defaultBundle);  
public static final Level WARNING = new Level("WARNING", 900, defaultBundle);  
public static final Level INFO = new Level("INFO", 800, defaultBundle);  
public static final Level CONFIG = new Level("CONFIG", 700, defaultBundle);    
public static final Level FINE = new Level("FINE", 500, defaultBundle);
public static final Level FINER = new Level("FINER", 400, defaultBundle);  
public static final Level FINEST = new Level("FINEST", 300, defaultBundle);  
public static final Level ALL = new Level("ALL", Integer.MIN_VALUE, defaultBundle);
日志级别数值备注
OFFint 最大值
SEVERE1000错误
WARNING900警告
INFO800默认日志级别
CONFIG700
FINE500
FINER400
FINEST300
ALLint 最小值
日志级别数值的意义,在于设置日志级别时,假设指定默认级别为 CONFIG 那么比 CONFIG 日志级别数值低的日志级别的日志,日志系统不处理[不打印],一半系统默认日志级别是 INFO,这意味着比 INFO 级别数值低的日志,是不会打印的。

设置日志级别的方法

logger.setLevel(Levle.CONFIG);

上面的方法虽然设置了日志级别,但并不意味着 CONFIG 这个级别的日志能够打印,需要实现相应的 Handler 才能实现默认以下级别日志的打印。


@Test  
public void testLogging2() {  
    Logger logger = java.util.logging.Logger.getLogger(JULTest.class.getName());  
    // 设置日志级别为ALL 这里的设置并不能生效,在getLogger会读取配置文件中的设置,如果没有配置文件,则使用默认的配置 INFO    logger.setLevel(Level.ALL);  
    // 输出日志,设置级别,输出日志  
    logger.log(Level.SEVERE, "This is a test log message with level SEVERE.");  
    logger.log(Level.WARNING, "This is a test log message with level WARNING.");  
    logger.log(Level.INFO, "This is a test log message with level INFO.");  
    logger.log(Level.CONFIG, "This is a test log message with level CONFIG.");  
    logger.log(Level.FINE, "This is a test log message with level FINE.");  
    logger.log(Level.FINER, "This is a test log message with level FINER.");  
    logger.log(Level.FINEST, "This is a test log message with level FINEST.");  
    logger.log(Level.ALL, "This is a test log message with level ALL.");  
}

在代码中Logger.getLogger(T.class),通过阅读源码,代码会读取一个配置文件,如果在配置文件中设置了日志级别,代码会以设置的级别为主,否则设置默认的日志级别。默认的的日志处理器Handler,[会继承级别 / 未验证],所以即使设置了 logger的日志级别,由于Handler不处理相应级别的日志,会导致设置不生效,[日志级别向下设置不生效 / 向上未知]。

自定义日志处理器,并打印所有级别的日志

@Test  
public void testLogging3() {  
    Logger logger = java.util.logging.Logger.getLogger(JULTest.class.getName());  
    // 不使用父类处理器 [不使用父类处理器,则需要自己设置日志处理器]  
    logger.setUseParentHandlers(false);  
    // 自定义日志处理器[控制台输出]  
    ConsoleHandler customHandler = new ConsoleHandler();  
    // 创建日志格式化器  
    SimpleFormatter formatter = new SimpleFormatter();  
    // 设置日志格式化器  
    customHandler.setFormatter(formatter);  
    // 设置日志处理器级别  
    customHandler.setLevel(Level.ALL);  
    // 设置日志级别   [日志处理器和日志级别都需要设置,否则不会生效]  
    logger.setLevel(Level.ALL);  
    // 添加日志处理器  
    logger.addHandler(customHandler);  
    // 输出日志,设置级别,输出日志  
    logger.log(Level.SEVERE, "This is a test log message with level SEVERE.");  
    logger.log(Level.WARNING, "This is a test log message with level WARNING.");  
    logger.log(Level.INFO, "This is a test log message with level INFO.");  
    logger.log(Level.CONFIG, "This is a test log message with level CONFIG.");  
    logger.log(Level.FINE, "This is a test log message with level FINE.");  
    logger.log(Level.FINER, "This is a test log message with level FINER.");  
    logger.log(Level.FINEST, "This is a test log message with level FINEST.");  
    logger.log(Level.ALL, "This is a test log message with level ALL.");  
}
  
/**  
 * 自定义日志处理器 [文件输出] FileHandler 日志格式化器 SimpleFormatter  
 */@Test  
public void testLogging4() throws IOException {  
    Logger logger = java.util.logging.Logger.getLogger(JULTest.class.getName());  
    // 不使用父类处理器 [不使用父类处理器,则需要自己设置日志处理器]  
    logger.setUseParentHandlers(false);  
    // 确保 log 文件夹存在  
    Files.createDirectories(Paths.get("log"));  
    // 自定义日志处理器 [文件输出]  
    FileHandler customHandler = new FileHandler("log/JULTest.log",true);  
    // 创建日志格式化器  
    SimpleFormatter formatter = new SimpleFormatter();  
    // 设置日志格式化器  
    customHandler.setFormatter(formatter);  
    // 设置日志处理器级别  
    customHandler.setLevel(Level.ALL);  
    // 设置日志级别   [日志处理器和日志级别都需要设置,否则不会生效]  
    logger.setLevel(Level.ALL);  
    // 添加日志处理器  
    logger.addHandler(customHandler);  
    // 输出日志,设置级别,输出日志  
    logger.log(Level.SEVERE, "This is a test log message with level SEVERE.");  
    logger.log(Level.WARNING, "This is a test log message with level WARNING.");  
    logger.log(Level.INFO, "This is a test log message with level INFO.");  
    logger.log(Level.CONFIG, "This is a test log message with level CONFIG.");  
    logger.log(Level.FINE, "This is a test log message with level FINE.");  
    logger.log(Level.FINER, "This is a test log message with level FINER.");  
    logger.log(Level.FINEST, "This is a test log message with level FINEST.");  
    logger.log(Level.ALL, "This is a test log message with level ALL.");  
}  
  
/**  
 * 自定义日志处理器 [文件输出 + 控制台输出] FileHandler 日志格式化器 SimpleFormatter  
 * 两个 Handler 共用一个 Formatter 也可以为不同的Handler 设置不同的Formatter 以差异化输出不同的日志格式   
 * 不同的 Handler 也可以设置不同的日志级别 
 */  
@Test  
public void testLogging5() throws IOException {  
    Logger logger = java.util.logging.Logger.getLogger(JULTest.class.getName());  
    // 不使用父类处理器 [不使用父类处理器,则需要自己设置日志处理器]  
    logger.setUseParentHandlers(false);  
    // 确保 log 文件夹存在  
    Files.createDirectories(Paths.get("log"));  
    // 自定义日志处理器 [文件输出]  
    FileHandler customHandler = new FileHandler("log/JULTest.log",true);  
    // 创建日志格式化器  
    SimpleFormatter formatter = new SimpleFormatter();  
    // 设置日志格式化器  
    customHandler.setFormatter(formatter);  
    // 设置日志处理器级别  
    customHandler.setLevel(Level.ALL);  
    // 设置日志级别   [日志处理器和日志级别都需要设置,否则不会生效]  
    logger.setLevel(Level.ALL);  
    // 添加日志处理器  
    logger.addHandler(customHandler);  
  
    // 添加控制台输出  
    ConsoleHandler consoleHandler = new ConsoleHandler();  
    consoleHandler.setFormatter(formatter);  
    consoleHandler.setLevel(Level.ALL);  
    logger.addHandler(consoleHandler);  
  
    // 输出日志,设置级别,输出日志  
    logger.log(Level.SEVERE, "This is a test log message with level SEVERE.");  
    logger.log(Level.WARNING, "This is a test log message with level WARNING.");  
    logger.log(Level.INFO, "This is a test log message with level INFO.");  
    logger.log(Level.CONFIG, "This is a test log message with level CONFIG.");  
    logger.log(Level.FINE, "This is a test log message with level FINE.");  
    logger.log(Level.FINER, "This is a test log message with level FINER.");  
    logger.log(Level.FINEST, "This is a test log message with level FINEST.");  
    logger.log(Level.ALL, "This is a test log message with level ALL.");  
}


阶段性总结: 用户使用 Logger 来进行日志的记录,Logger 可以持有多个处理器 Handler [日志的记录使用的是 Logger ,日志的输出使用的 Handler ]

2.4 Logger 层级

Logger之间的父子关系 JUL 中的Logger之间的“父子” 关系,非普遍的 [继承] 关系,是通过树形结构存储的上下级关系


@Test  
public void testLogging6() throws IOException {  
    // Logger 的层次结构  
    // 根 Logger 名称为 ""    // 子 Logger 名称为 父 Logger 名称 + "." + 子 Logger 名称  
    // 例如:cn.qzbook.test 父 Logger 名称为 cn.qzbook 子 Logger 名称为 test    
    Logger logger0 = java.util.logging.Logger.getLogger("cn");  
    Logger logger1 = java.util.logging.Logger.getLogger("cn.qzbook");  
    Logger logger2 = java.util.logging.Logger.getLogger("cn.qzbook.test");  
    // 通过引用关系判断 Logger 层次结构  
    System.out.println(logger0 == logger1.getParent());  
    System.out.println(logger1 == logger2.getParent());  
}

在 Logger 的上级的设置,同时能够作用于下级 Logger 对于 上面代码 在logger0 做的设置,会作用于 logger1 和 logger2

@Test  
public void testLogging7() throws IOException {  
    // 日志配置文件  
    Logger logger0 = java.util.logging.Logger.getLogger("cn");  
    Logger logger1 = java.util.logging.Logger.getLogger("cn.qzbook");  
    Logger logger2 = java.util.logging.Logger.getLogger("cn.qzbook.test");  
    // [上层的 Logger]不使用父类[再上层的 Logger]处理器  
    logger0.setUseParentHandlers(false);  
    // 修改上层的 Logger 日志级别为 ALL 输出到控制台  
    logger0.setLevel(Level.ALL);  
    ConsoleHandler handler = new ConsoleHandler();  
    handler.setLevel(Level.ALL);  
    handler.setFormatter(new SimpleFormatter());  
    logger0.addHandler(handler);  
    // 使用下层的 Logger 输出日志 上层的 Logger 设置的日志级别和日志处理器都会生效  
    logger2.log(Level.SEVERE, "This is a test log message with level SEVERE.");  
    logger2.log(Level.WARNING, "This is a test log message with level WARNING.");  
    logger2.log(Level.INFO, "This is a test log message with level INFO.");  
    logger2.log(Level.CONFIG, "This is a test log message with level CONFIG.");  
    logger2.log(Level.FINE, "This is a test log message with level FINE.");  
    logger2.log(Level.FINER, "This is a test log message with level FINER.");  
    logger2.log(Level.FINEST, "This is a test log message with level FINEST.");  
    logger2.log(Level.ALL, "This is a test log message with level ALL.");  
    // 根据上面的日志 配置 直接在 root Logger 上设置日志级别和处理器,则全局生效  
}

阶段性总结: JUL 在初始化 Logger 时,会创建一个顶层的 Logger [RootLogger] 作为所有 Logger 最顶级父 Logger

// JUL 获取java Logger 部分源码
// RootLogger 是 LogManger 的内部类
owner.rootLogger = owner.new RootLogger();

RootLogger 对象作为输转结构的根节点存在,自定义的 Logger 父子关系也是通过路径[包路径] 来关联的。

LoggerContent 日志结构上下文,内部不使用 Map 存储 Logger,Key 为路径,value 为 具体的 Logger 对象。

2.5 配置文件

以上所有的配置相关的操作,都是以java硬变化的形式进行的,还可以通过配置文件的方式来配置 Logger 对象,跟方便解耦

如果没有自定义配置文件,则会使用系统默认的配置文件来初始化 Logger,系统默认配置部分源码

owner.readPrimordialConfiguation();

读取的默认配置文件路径 java.home --> 找到 jre 文件夹 --> lib --> logging.properties

配置文件-配置项 在配置文件中配置的是根节点,也就是 RootLogger ,在获取 RootLogger对象时进行设置,默认情况下


# 日志处理器 多个处理器使用 逗号 [,] 分割
handlers=java.util.logging.ConsoleHandler,java.util.logging.FileHandler  
 
# 根节点 日志级别  
# 默认情况下,这是全局的日志级别,如果不手动设置其他级别,则默认输出INFO或者比INFO更高的级别  
.level= ALL  
 
# 文件日志处理器的 属性配置  
# 输出日志文件的路径  %h 代表主机名[用户目录], %u 代表用户名  
#java.util.logging.FileHandler.pattern=%h/java%u.log  
# 输出日志文件的路径 直接写文件夹名称  相对路径 当前项目的根目录  
java.util.logging.FileHandler.pattern=log/java%u.log  
# 输出日志文件的显示 50000字节  
java.util.logging.FileHandler.limit=50000  
# 输出日志文件的数量  
java.util.logging.FileHandler.count=1  
# 输出日志的格式  
# 默认是以 xml 的方式进行输出  
#java.util.logging.FileHandler.formatter=java.util.logging.XMLFormatter  
# 以文本方式输出日志  
java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
# 文件日志默认是覆盖模式新增 设置为 - 追加模式  
java.util.logging.FileHandler.append=true
 
# 控制台日志处理器 属性配置  
# 控制台输出默认的级别  
java.util.logging.ConsoleHandler.level=ALL  
# 控制台默认输出的格式  
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter  
# 包名.属性=值 即可设置指定包下的日志配置  
# 指定在某个包路径下 指定的日志级别  
#cn.qzbook.level = SEVERE

2.6 使用总结

  1. 初始化 LogManager
  2. LogManager 加在 logging.properties 配置文件
  3. 从单例 LogManager 中获取 LogManager [单例工厂]
  4. 设置日志级别 Level ,在打印的过程中使用到了日志记录的LogRecord类
  5. Fileter 作为过滤器,提供了日志级别之外,更细粒度的控制器
  6. Handler 日志处理器,决定日志的输出位置,例如:控制台、文件...
  7. Formatter 是用来格式化输出的

3 Log4J

3.1 Log4J 简介

Log4J 是 Apache 是一个开源项目,通过使用 Log4j,我们可以控制日志信息的输送目的地是控制台、文件、GUI 组件,甚至是套接口服务器、NT 的事件记录器、UNIX Syslog 守护进程等;也可以控制每一条日志的输出格式,通过定义每一条日志信息的级别,能够更加细致的控制日志的生成过程。

3.2 Log4J 组件

组件概述

Loggers [日志记录器] : 控制日志的输出以及输出级别

Appenders [输出控制器] : 指定日志的输出方式,控制台,文件 ...

Layout [日志格式化器] : 控制日志信息的输出格式

3.2.1 Loggers

日志记录器,负责手机处理日志记录,实例的命名就是类的全限定名,如 cn.qzbook.Test,Logger 的名字大小写敏感,其命名有继承机制,下级包名会继承上级包名。Log4J 有一个特殊的 logger 是 “root”,他是所有的logger的根,也就意味着其他所有的logger都会直接或间接的继承自root。root logger 可以用logger.getRootLogger()方法获取。

日志根节点的名称,为空字符串 "",下一级为以及包名,依次按包名层级结构向下分级。

日志级别: DEBUG、INFO、WARN、ERROR ... 级别分别是从大到小的。

  • ALL 最低等级,用于打开所有级别的日志记录
  • TRACE 程序推进下的追踪信息,这个追踪信息的日志级别非常低,一般情况下是不会使用的
  • DEBUG 指出细粒度信息时间对调试应用程序是非常有帮助的,主要是配合开发,在开发过程中打印一些重要的运行信息
  • INFO 消息的粗粒度级别运行信息
  • WARN 表示警告,程序运行过程中会出现的有可能会发生隐形的错误,注意,有些信息不是错误,但是这个级别输出的母的就是给程序员提示
  • ERROR 系统错误信息,发生的错误不影响系统运行,印版情况下,如果不想输出太多的日志,则使用更改级别即可
  • FATAL 表示严重错误,这类型错误发生程序将停止运行
  • OFF 最高等级的级别,用户关闭了所有的日志记录

3.2.2 Appenders

常用的 appenders

  • ConsoleAppender
  • 将日志输出到控制台
  • FileAppender
  • 将日志输出到文件中
  • DailyRollingFileAppender
  • 将日志输出到一个日志文件,并且每天输出到一个新的文件 [按照设定的时间来分别创建文件]
  • RollingFileAppender
  • 将日志信息输出到日志文件,并且指定文件的大小,当文件内容大小超出时,自动修改文件名,并创建的新的文件
  • JDBCAppender
  • 将日志信息输出到数据库

3.2.3 Layout

格式化日志输出,Log4J可以在Appenders后面附件Layouts来完善这个功能。Layouts提供了四种日志输出样式,如根据HTML样式、自由制定演示、包含日志级别与信息的样式,包含日志时间、现成、类别等信息的样式。

常用的 Layout

  • HTMLLayout
  • 格式化日志输出为 HTML
  • SimpleLayout
  • 简单的日志输出格式化,打印日志格式如默认INFO级别的消息
  • PatternLayout
  • 最强大的格式组件,可以根据自定义格式输出日志,如果没有指定转换格式,就是用默认的转换格式。

启动流程

BasicConfigurator.configure();

// 1. 创建了根节点对象
Logger root = Logger.getRootLogger();
// 2. 根节点添加了 ConsoleAppender 对象 输出日志到工作台 使用默认的输出格式
root.addAppender(new ConsoleAppender(new PatternLayout("%r [%t] %p %c %x - %m%n")));

3.3 基本使用

引入依赖

<dependency>  
  <groupId>log4j</groupId>  
  <artifactId>log4j</artifactId>  
  <version>1.2.17</version>  
</dependency>
// log4j 包路径
// import org.apache.log4j.BasicConfigurator;  
// import org.apache.log4j.Logger;

@Test  
public void test() {  
    // 加载初始化配置 如果不初始化 会报警告信息  
    BasicConfigurator.configure();  
    Logger logger = Logger.getLogger(AppLog4JTest.class);  
    logger.info("This is an info message.");  
}


3.4 配置文件

使用配置文件防止硬编码在程序中不方便维护,使用配置文件的方式可以解耦,方便维护

Log4J 提供了多种配置文件的配置方式,在 LogManager 中可以看到

/** @deprecated */  
public static final String DEFAULT_CONFIGURATION_FILE = "log4j.properties";  
static final String DEFAULT_XML_CONFIGURATION_FILE = "log4j.xml";  
/** @deprecated */  
public static final String DEFAULT_CONFIGURATION_KEY = "log4j.configuration";  
/** @deprecated */  
public static final String CONFIGURATOR_CLASS_KEY = "log4j.configuratorClass";  
/** @deprecated */  
public static final String DEFAULT_INIT_OVERRIDE_KEY = "log4j.defaultInitOverride";  
private static Object guard = null;  
private static RepositorySelector repositorySelector;

常用的配置方式,使用 log4j.properties 配置文件,它在 LogManager 初始化的时候通过静态代码块加载。 配置文件默认加载源码:

static {  
    Hierarchy h = new Hierarchy(new RootLogger(Level.DEBUG));  
    repositorySelector = new DefaultRepositorySelector(h);  
    String override = OptionConverter.getSystemProperty("log4j.defaultInitOverride", (String)null);  
    if (override != null && !"false".equalsIgnoreCase(override)) {  
        LogLog.debug("Default initialization of overridden by log4j.defaultInitOverrideproperty.");  
    } else {  
        String configurationOptionStr = OptionConverter.getSystemProperty("log4j.configuration", (String)null);  
        String configuratorClassName = OptionConverter.getSystemProperty("log4j.configuratorClass", (String)null);  
        URL url = null;  
        if (configurationOptionStr == null) {  
            url = Loader.getResource("log4j.xml");  
            if (url == null) {  
                url = Loader.getResource("log4j.properties");  
            }  
        } else {  
            try {  
                url = new URL(configurationOptionStr);  
            } catch (MalformedURLException var7) {  
                url = Loader.getResource(configurationOptionStr);  
            }  
        }  
        if (url != null) {  
            LogLog.debug("Using URL [" + url + "] for automatic log4j configuration.");  
            try {  
                OptionConverter.selectAndConfigure(url, configuratorClassName, getLoggerRepository());  
            } catch (NoClassDefFoundError var6) {  
                NoClassDefFoundError e = var6;  
                LogLog.warn("Error during default initialization", e);  
            }  
        } else {  
            LogLog.debug("Could not find resource: [" + configurationOptionStr + "].");  
        }  
    }  
}

配置文件在maven项目中resource目录下,可以使用多种格式配置,以下示例基本都是以 log4j.properties 文件形式配置

以下数据是通过 OptionConverter.selectAndConfigure(url, configuratorClassName, getLoggerRepository()); 代码加载配置文件转换为配置 PropertyConfigurator 对象,其中的常量就是在配置文件中配置的配置项

static final String CATEGORY_PREFIX = "log4j.category.";  
static final String LOGGER_PREFIX = "log4j.logger.";  
static final String FACTORY_PREFIX = "log4j.factory";  
static final String ADDITIVITY_PREFIX = "log4j.additivity.";  
static final String ROOT_CATEGORY_PREFIX = "log4j.rootCategory";  
static final String ROOT_LOGGER_PREFIX = "log4j.rootLogger";  
static final String APPENDER_PREFIX = "log4j.appender.";  
static final String RENDERER_PREFIX = "log4j.renderer.";  
static final String THRESHOLD_PREFIX = "log4j.threshold";  
private static final String THROWABLE_RENDERER_PREFIX = "log4j.throwableRenderer";  
private static final String LOGGER_REF = "logger-ref";  
private static final String ROOT_REF = "root-ref";  
private static final String APPENDER_REF_TAG = "appender-ref";  
public static final String LOGGER_FACTORY_KEY = "log4j.loggerFactory";  
private static final String RESET_KEY = "log4j.reset";  
private static final String INTERNAL_ROOT_NAME = "root";

配置方式:

log4j.appender.自定义名称=org.apache.log4j.ConsoleAppender[log4J提供的Appenders] log4j.logger.自定义名称=[log4J提供的logger]

不同的前缀配置不同的组件,前缀后面跟自定义名称为组件命名

简单配置

# 配置 rootLogger 输出级别为 DEBUG,输出到控制台  
# 配置项:第一个参数为日志级别, 后续参数为 Appender 名称[线面配置的 log4j.appender.自定义名称]  
log4j.rootLogger=DEBUG, console  
# 配置 appender 输出方式 输出到控制台  
log4j.appender.console=org.apache.log4j.ConsoleAppender  
# 配置输出格式  
log4j.appender.console.layout=org.apache.log4j.SimpleLayout
@Test  
public void test3() {  
    // 使用配置文件  
    Logger logger = Logger.getLogger(AppLog4JTest.class);  
    logger.fatal("This is a fatal message.");  
    logger.error("This is an error message.");  
    logger.warn("This is a warning message.");  
    logger.info("This is an info message.");  
    logger.debug("This is a debug message.");  
    logger.trace("This is a trace message.");  
}  

// LogLog 未生效 在idea启动中添加参数 VM options: -Dlog4j.debug
@Test  
public void test4() {  
    // 开启内部调试模式 打印 logger 的内部日志信息  
    LogLog.setInternalDebugging(true);  
    // 确保日志级别为 DEBUG 或更低  
    Logger.getRootLogger().setLevel(Level.DEBUG);  
    // 检查标准错误流是否正常工作  
    System.err.println("This should appear in the standard error stream.");  
    Logger logger = Logger.getLogger(AppLog4JTest.class);  
    logger.fatal("This is a fatal message.");  
    logger.error("This is an error message.");  
    logger.warn("This is a warning message.");  
    logger.info("This is an info message.");  
    logger.debug("This is a debug message.");  
    logger.trace("This is a trace message.");  
}


自定义 日志格式输出,log4j layout配置,其中 PatternLayout 是日常使用最多的方式,支持自定义输出格式,根据配置定义个性化输出。 关于 PatternLayout 在源码中的位置

setConversionPattern(String conversionPattern)

PatternLayout 配置项

  • 这种格式化输出,采用类似于 C 语言的 printf 函数的打印格式来格式化日志信息,具体的占位符以及其含义
  • %m 输出代码中指定的日志信息
  • %p 输出优先级,及 DEBUG、INFO 等
  • %n 换行符
  • %r 输出自应用启动到输出该 Log 信息的毫秒数
  • %c 输出打印语句所属类的全名
  • %t 输出产生该日志的线程全名
  • %d 输出服务器当前时间,默认为 ISO8601,也可以指定格式,如:%d{yyyy年MM月dd日 HH:mm:ss}
  • %1 输出日志时间发生的位置,包括类名、线程、及在代码中的行数。如:Test.main(Test.java:10)
  • %F 输出日志消息产生式所在的文件名称
  • %L 输出代码中的行号
  • %% 输出一个 “%” 字符
  • %5c 输出 category 名称,最小宽度是5,category<<5,默认的情况下右对齐

配置文件配置-示例 [控制台输出]

# 配置 rootLogger 输出级别为 DEBUG,输出到控制台  
# 配置项:第一个参数为日志级别, 后续参数为 Appender 名称[下面配置的 log4j.appender.自定义名称]  
log4j.rootLogger=DEBUG, aa  
# 配置 自定义名称的 appender 输出方式 输出到控制台     同一个自定义名称的 Appenders 各个组件配置为一组
log4j.appender.aa=org.apache.log4j.ConsoleAppender  
# 配置输出格式 HTML 格式输出 简单输出格式  
#log4j.appender.aa.layout=org.apache.log4j.HTMLLayout  
#log4j.appender.aa.layout=org.apache.log4j.SimpleLayout  
# 自定义输出格式化器
log4j.appender.aa.layout=org.apache.log4j.PatternLayout  
# 自定义输出格式 [conversionPattern 是 PatternLayout类的一个属性]
log4j.appender.aa.layout.conversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} %p %c{1}:%L %t - %m%n

配置文件配置-示例 [控制台输出+文件输出]

# 配置 rootLogger 输出级别为 DEBUG,输出到控制台  
# 配置项:第一个参数为日志级别, 后续参数为 Appender 名称[下面配置的 log4j.appender.自定义名称]  
log4j.rootLogger=DEBUG, aa,outFile  
# 配置 appender 输出方式 输出到控制台  
log4j.appender.aa=org.apache.log4j.ConsoleAppender  
# 配置输出格式 HTML 格式输出 简单输出格式  
#log4j.appender.aa.layout=org.apache.log4j.HTMLLayout  
#log4j.appender.aa.layout=org.apache.log4j.SimpleLayout  
log4j.appender.aa.layout=org.apache.log4j.PatternLayout  
log4j.appender.aa.layout.conversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} %p %c{1}:%L %t - %m%n  
  
# 配置 appender 输出方式 输出到文件 [FileAppender 中的属性 在下面的配置中添加配置]
log4j.appender.outFile=org.apache.log4j.FileAppender  
# 如: File 是 FileAppender 的一个属性,表示指定文件位置,下面属性同理
log4j.appender.outFile.File=log/log4j.log  
log4j.appender.outFile.MaxFileSize=10MB  
log4j.appender.outFile.layout=org.apache.log4j.PatternLayout  
log4j.appender.outFile.layout.conversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} %p %c{1}:%L %t - %m%n

输出中文设置 查看FileAppender的父类,有一个 encoding 字符集编码属性,配置为 UTF-8,可正确的输出中文

log4j.appender.outFile.layout.encoding=UTF-8

文件输出Appender类:所有的配置,都是类的属性,为属性设置不同的值,达到个性化输出的目的,可配置的项是当前类的属性和父类的属性都可以配置

  • FileAppender
  • 类的属性按需要配置
  • RollingFileAppender
  • 文件拆分 [ maxFileSize 文件最大字节 / maxBackupIndex 最大文件数量] 文件数量超过指定数量,从最早的日志数据开始覆盖
  • DailyRollingFileAppender
  • 按照时间来拆分 [具体配置项查看类属性]
  • ExternallyRolledFileAppender
  • 外部程序控制拆分,比如:日志文件达到一定大小后,调用外部程序,由外部程序来拆分日志文件,再将日志数据写入新的文件中
# 配置 rootLogger 输出级别为 DEBUG,输出到控制台 ,输出到文件 ,输出到文件[可拆分] 如果下面配置了自定义的Appenders 但在 log4j.rootLogger 中没有配置,是不会生效的
# 配置项:第一个参数为日志级别, 后续参数为 Appender 名称[下面配置的 log4j.appender.自定义名称]  
log4j.rootLogger=DEBUG, aa,outFile,outFileRolling  
# 配置 appender 输出方式 输出到控制台  
log4j.appender.aa=org.apache.log4j.ConsoleAppender  
# 配置输出格式 HTML 格式输出 简单输出格式  
#log4j.appender.aa.layout=org.apache.log4j.HTMLLayout  
#log4j.appender.aa.layout=org.apache.log4j.SimpleLayout  
log4j.appender.aa.layout=org.apache.log4j.PatternLayout  
log4j.appender.aa.layout.conversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} %p %c{1}:%L %t - %m%n  
  
# 配置 appender 输出方式 输出到文件  
log4j.appender.outFile=org.apache.log4j.FileAppender  
log4j.appender.outFile.encoding=UTF-8  
log4j.appender.outFile.Append=true  
log4j.appender.outFile.File=log/log4j.log  
log4j.appender.outFile.layout=org.apache.log4j.PatternLayout  
log4j.appender.outFile.layout.conversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} %p %c{1}:%L %t - %m%n  
  
# 配置 appender 输出方式 输出到文件  
log4j.appender.outFileRolling=org.apache.log4j.RollingFileAppender  
log4j.appender.outFileRolling.encoding=UTF-8  
log4j.appender.outFileRolling.Append=true  
log4j.appender.outFileRolling.File=log/log4j1.log  
log4j.appender.outFileRolling.layout=org.apache.log4j.PatternLayout  
log4j.appender.outFileRolling.layout.conversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} %p %c{1}:%L %t - %m%n

3.5 持久化日志[JDBC]

自定义数据库字段,按照 PatternLayout 配置项 取值,最终输出到数据库

引入 JDBC 依赖

<dependency>  
  <groupId>com.mysql</groupId>  
  <artifactId>mysql-connector-j</artifactId>  
  <version>8.3.0</version>  
</dependency>

配置文件

log4j.rootLogger=DEBUG,db
  
  
# 配置 输出到数据库  
log4j.appender.db=org.apache.log4j.jdbc.JDBCAppender  
log4j.appender.db.driver=com.mysql.cj.jdbc.Driver  
log4j.appender.db.URL=jdbc:mysql://localhost:3306/log4j  
log4j.appender.db.user=root  
log4j.appender.db.password=123456  
log4j.appender.db.sql=INSERT INTO log4j_log (log_time, log_level, log_class, log_method, log_line, log_message) VALUES (NOW(), '%p', '%c{1}', '%M', '%L', '%m')
log4j.appender.db.layout=org.apache.log4j.PatternLayout  
log4j.appender.db.layout.conversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} %p %c{1}:%L %t - %m%n

解释

  1. 日志消息格式化: log4j.appender.db.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} %p %c{1}:%L %t - %m%n 定义了日志消息的格式。 例如,一条日志消息可能会被格式化为 2023-10-01 12:34:56.789 DEBUG MyClass:42 main - This is a log message。

  2. SQL 语句中的占位符: log4j.appender.db.sql=INSERT INTO LOGS (TIMESTAMP, LEVEL, LOGGER, THREAD, MESSAGE) VALUES ('%d{yyyy-MM-dd HH:mm:ss.SSS}', '%p', '%c{1}', '%t', '%m') 定义了将日志消息插入数据库的 SQL 语句。

占位符 %d{yyyy-MM-dd HH:mm:ss.SSS}、%p、%c{1}、%t 和 %m 会被 PatternLayout 格式化后的日志消息内容替换。 例如,上述格式化的日志消息会被插入到数据库中,对应的 SQL 语句可能是:

     INSERT INTO LOGS (TIMESTAMP, LEVEL, LOGGER, THREAD, MESSAGE) 
     VALUES ('2023-10-01 12:34:56.789', 'DEBUG', 'MyClass', 'main', 'This is a log message')

总结

  • log4j.appender.db.layout 和 log4j.appender.db.layout.conversionPattern 用于定义日志消息的格式。
  • log4j.appender.db.sql 用于定义将日志消息插入数据库的 SQL 语句。
  • conversionPattern 中的占位符会被 sql 语句中的相应占位符替换,从而将格式化后的日志消息插入到数据库中。

3.6 自定义 Logger

前面所有的配置都是基于 rootLogger 来进行配置的,log4j 可以通过自定Logger的形式来细化log记录的差异化配置。自定义的 Logger 也可以被其他的 Logger 所继承。无论是 rootLogger 还是自定的 Logger 它们的关系依然是树形结构的关系,上下级由包名的的层级关系来组成,也就是说,所有 Logger 的根节点最终还是 rootLogger, 所以全局配置还是配置在rootLogger上,自定义的配置,可以按照业务模块[包结构]做差异化的配置.

自定义 Logger 在配置文件中的前缀为 [log4j.logger.]+包名,包名到哪一级,当前自定义Logger作用到哪一级。它的参数配置和[log4j.rootLogger]是一致的,第一个参数为日志级别, 后续参数为 定义的 Appender 名称,可以与 rootLogger 引用相同的 Appender。

# 自定义 logger 输出级别为 DEBUG,输出到文件  
log4j.logger.cn.qzbook=DEBUG, outFile

# 配置 appender 输出方式 输出到文件  
log4j.appender.outFile=org.apache.log4j.FileAppender  
log4j.appender.outFile.encoding=UTF-8  
log4j.appender.outFile.Append=true  
log4j.appender.outFile.File=log/log4j.log  
log4j.appender.outFile.layout=org.apache.log4j.PatternLayout  
log4j.appender.outFile.layout.conversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} %p %c{1}:%L %t - %m%n

此包cn.qzbook级别的日志,采用 outFile 的 appender 将日志文件输出到文件,其他包级别的日志,默认走 rootLogger 的配置。当前自定义的 cn.qzbook 的 logger 自身的配置生效,且还会集成rootLogger,两者的配置都会生效,简单来说,下一级的日志配置本身生效的同时,还会在继承上级日志配置。

4 JCL

全称为 Jakarta commons logging,是 Apache 提供的一个通用日志 API。

用户可以自由选择第三方的日志组件作为具体的实现,像 Log4j,或者 Jdk自带的jul,common-logging 会通过动态查找的机制,在程序运行时自动找出真真使用的日志库。

当然,common-logging 内部是有一个 Simple Logger 的简单实现,但实现较为简单,所以使用 common-logging 的时候,通常配置 log4j 以及其他日志框架来使用。

使用它的好处就是,代码依赖的是 common-logging 而非具体的日志框架,避免了和具体的日志框架 API 直接耦合,在有必要时,可以更改日志实现的第三方库。

JCL 有两个基本的抽象类:

  • Log:日志记录器
  • LogFactory:日志工厂[负责创建 Log 实例]

4.1 JCL 入门案例

引入依赖

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

hello word 示例

package cn.qzbook;  
  
import org.apache.commons.logging.Log;  
import org.apache.commons.logging.LogFactory;  
import org.junit.Test;  

public class AppTest{  
  
    @Test  
    public void test1(){  
        Log log = LogFactory.getLog(AppTest.class);  
  
        log.info("Hello, world!");  
    }  
}

控制台打印信息

十一月 19, 2024 11:00:42 下午 cn.qzbook.AppTest test1
信息: Hello, world!

在hello World 示例中,JCL 使用的是 JDK 自带的 JUL 日志框架,因为项目中没有导入任何第三方日志框架

JCL 使用原则:如果有项目中导入了 Log4j框架,JCL 优先使用 Log4j 作为日志实现,在项目没有使用任何第三方日志框架的时候,JCL 默认使用 jdk 自带的 JUL 日志框架。

继续上面的案例,在导入 log4j 日志框架之后,控制台输出

log4j:WARN No appenders could be found for logger (cn.qzbook.AppTest).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.

从警告信息中可以看出,JCL 使用了 log4j 日志框架,由于没有项目中没有找到 log4j 的配置文件,发出警告了 ⚠️

4.1.2 LogFactory

LogFactory 是加在日志框架具体实现的构造工厂,但它是一个接口,具体的实现类 LogFactoryImpl

5 SLF4J

SLF4J 日志门面。

门面模式 (facade pattern),也称为外观模式,核心概念:外部与一个子系统的通信西部通过一个统一的外观对象进行,是的子系统更易于使用。

外观模式主要是体现了 Java 中的一种好的封装性。更简单的说,就是对外提供的接口要尽可能的简单。

简单日志门面(simple Logging Facade For Java) SLF4J 主要是为了给 Java 日志访问提供一套标准、规范的 API 框架,其主要意义在于提供接口,具体的视线可以交由其他的日志框架,例如 Log4j 和 Logback等。当然 slf4j 自己也提供了功能较为简单的实现,但是一般很少用到。对于一般的 java 项目而言,日志框架会选择 slf4j-api 作为门面,配上具体的实现框架 (log4j、logback等),中间使用桥接器完成桥接。 对于开发者而言,SLF4J 最重要的两个功能就是对于日志框架的绑定以及日志框架的桥接。

5.1 SLF4J 适配器

通常,项目依赖的某些组件中依赖于 SLF4J 的日志 API。可能还假设这些组件在将来会切换到 SLF4J。为了处理这种情况,SLFJ 附带了几个桥接模块,这些模块会将对 Log4j,JCL 和 java.util.logging API 的调用重定向行为,就好像对 SLF4J API 进行的操作一样。

SLF4J 官网介绍图

![[concrete-bindings.png]] SJF4J 日志门面,共有三种情况对日志实现进行绑定

  • 在没有绑定任何日志实现的基础之上,日志门面本身无法提供具体功能,此时,日志功能相当于没有,是上图中第一种情况。
  • 值得注意的是,下面的几个示例,是导入了 slf4j-simple 这个简单实现来提供的具体功能。导入了 slf4j-simple 依赖 SLF4J 自动绑定,并且提供日志功能,是上图中的第五中情况。
  • logback 和 simple (报错 nop)
  • 都是 slf4j 门面时间线后面提供的日志实现,所以 API 完全遵循 slf4j 进行设计,需要导入相关的日志实现依赖,即可与 slf4j 无缝衔接,值得一提的是 nop 虽然也划分到视线中了,但它只是一个默认的抽象实现,即没有任何具体的功能(相当于项目有日志依赖,但没有日志功能)。
  • log4j 和 JUL
  • 都是 slf4j 门面出现之前的日志实现,所以 API 不遵循 slf4j 进行设计
  • 通过适配桥接的技术,完成与日志门面的衔接

5.2 入门示例

导包: SLF4J 的包分两部分,slf4j-api 是接口定义(门面),slf4j-simple SLF4J 的一个简单实现。

<!--  slf4j API  -->  
<dependency>  
    <groupId>org.slf4j</groupId>  
    <artifactId>slf4j-api</artifactId>  
</dependency>  
<!--  slf4j simple implementation  -->  
<dependency>  
    <groupId>org.slf4j</groupId>  
    <artifactId>slf4j-simple</artifactId>  
</dependency>

5.2.1 示例一:简单打印 info 日志

package cn.qzbook;  
  
import org.junit.Test;  
import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;  
// 注意 logger 的日志包 org.slf4j.*
public class AppTest {  
    /**  
     * 入门示例  
     */  
    @Test  
    public void testLog1() {  
        Logger logger = LoggerFactory.getLogger(AppTest.class);  
        logger.info("This is a test log message.");  
    }  
}

注意: 在导入 slf4j-api 后没有导入任何一个具体的日志实现框架,项目运行会抛出异常

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

5.2.12 示例二:占位符拼接日志信息

@Test  
public void testLog2() {  
    String name = "qzbook";  
    int age = 25;  
    double salary = 5000.0;  
    Logger logger = LoggerFactory.getLogger(AppTest.class);  
    // 参数站位符{}  
    logger.info("{} is {} years old and earns {}.",name,age,salary);  
}

5.2.3 示例三:打印异常堆栈信息

@Test  
public void testLog3() {  
    Logger logger = LoggerFactory.getLogger(AppTest.class);  
    // 异常栈信息  
    try {  
        int i = 1 / 0;  
    } catch (Exception e) {  
        // e 是错误对象,可以打印堆栈信息 不需要字符串占位符{}  
        // 堆栈信息会自动输出到日志文件中 方便追踪错误  
        // logger 会自动输出异常信息和堆栈信息 不需要调用e.printStackTrace()  
        // 此处调用的是重载方法,参数类型不同 void error(String var1, Throwable var2);        
        logger.error("Exception occurred.", e);  
    }  
}

5.3 日志级别

SLF4J 的日志级别:

  • trace 日志追踪信息
  • debug 日志详细信息
  • info 日志的关键信息 默认打印级别
  • warn 日志的警告信息
  • error 日志的错误信息

5.4 集成实现

5.4.1 集成Logback

导入依赖

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

注意在下面示例中: logback-classic 在导入的时候,并没有去掉上面示例的 slf4j-simple 导入完整导入如下:

<!--  slf4j API  -->  
<dependency>  
  <groupId>org.slf4j</groupId>  
  <artifactId>slf4j-api</artifactId>  
  <version>1.7.25</version>  
</dependency>   
<!--  slf4j simple implementation  -->  
<dependency>  
  <groupId>org.slf4j</groupId>  
  <artifactId>slf4j-simple</artifactId>  
  <version>1.7.25</version>  
</dependency>     
<!--  logback implementation  -->
<dependency>  
  <groupId>ch.qos.logback</groupId>  
  <artifactId>logback-classic</artifactId>  
  <version>1.2.3</version>  
</dependency>

通过调换导入 slf4j-simple 和 logback-classic 的顺序,slf4j 会加载先导入的实现框架。 在实际应用中,不需要引入多个日志实现框架,只需要选择其中一个。这里反应出 SLF4J 的一个便利之处,在更换不同实现框架时,代码不需要做任何变更,这就是日志门面的优势,低耦合,降低学习成本,只需要学习 SLF4J 的 API ,而不用每一个日志框架都学习(虽然还是需要了解一下,理论上是可以的)。

/**  
 * 日志实现 导入顺序测试  
 *  1. 在 maven 中导入顺序 slf4j-simple -> logback-classic  
 *     在一下示例中 打印日志的实现框架 是 sl4j-simple 实现的  
 *   2. 在 pom.xml 中导入顺序 logback-classic -> slf4j-simple  
 *     在一下示例中 打印日志的实现框架 是 logback-classic 实现的  
 *  日志实现框架的选择,需要根据实际情况选择,如果项目中已经有其他日志实现框架,可以选择不使用 slf4j-simple 实现。  
 *  一旦导入了多个日志实现框架,正常情况下,会按照导入顺序进行初始化,但是如果存在冲突,可能会导致日志输出异常。  
 */  
@Test  
public void testLog4() {  
    Logger logger = LoggerFactory.getLogger(AppTest.class);  
    // 异常栈信息  
    try {  
        int i = 1 / 0;  
    } catch (Exception e) {  
        logger.error("Exception occurred.", e);  
    }  
}

5.4.2 集成 Log4J

Log4j 日志框架出现在 slf4j 之前,所以 log4j 不适配 slf4j 的标准,如果需要在项目中使用 slf4j 并且以 log4j 作为实现,需要引入一个额外的依赖,添加一个适配器的依赖,通过桥接器让 slf4j 兼容 log4j。


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

<!--  slf4j to log4j bridge 桥接器  -->  
<dependency>  
  <groupId>org.slf4j</groupId>  
  <artifactId>slf4j-log4j12</artifactId>  
  <version>1.7.25</version>  
</dependency>

<!--  log4j 依赖  -->  
<dependency>  
  <groupId>log4j</groupId>  
  <artifactId>log4j</artifactId>  
  <version>1.2.17</version>  
</dependency>

示例代码:

import org.junit.Test;  
import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;

@Test  
public void testLog4() {  
    Logger logger = LoggerFactory.getLogger(AppTest.class);  
    // 异常栈信息  
    try {  
        int i = 1 / 0;  
    } catch (Exception e) {  
        logger.error("Exception occurred.", e);  
    }  
}

控制台输出: 控制台打印警告信息找不到 appenders 的配置(找不到 log4j 的配置文件),这说明 slf4j 已经成功的桥接了 log4j 作为实现框架,到此为止,代码中导入的包,仍然是 slft4j 的,也就是说 logger 仍然是 slft4j 的类对象。

log4j:WARN No appenders could be found for logger (cn.qzbook.AppTest).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.

5.4.3 集成 JUL

导入依赖

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

<!--  slf4j jdk14 bridge JCL 桥接器 -->  
<dependency>  
    <groupId>org.slf4j</groupId>  
    <artifactId>slf4j-jdk14</artifactId>  
</dependency>

代码示例

import org.junit.Test;  
import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;

@Test  
public void testLog4() {  
    Logger logger = LoggerFactory.getLogger(AppTest.class);  
    // 异常栈信息  
    try {  
        int i = 1 / 0;  
    } catch (Exception e) {  
        logger.error("Exception occurred.", e);  
    }  
}

5.5 集成异常

当项目中使用了 SLF4J ,且导入了多个日志实现框架如:logback log4j ... 控制台会输出警告信息

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/Users/chuan/local_directory/repository/org/slf4j/slf4j-simple/1.7.25/slf4j-simple-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/Users/chuan/local_directory/repository/org/slf4j/slf4j-jdk14/1.7.25/slf4j-jdk14-1.7.25.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.SimpleLoggerFactory]

控制台输出:可以看出日志格式为 JUL 的默认格式,即表示 slf4j 桥接 JCL 作为日志实现框架成功,并且代码示例和上面的 log4j 的示例没有却别,这意味着切换日志的底层实现框架,不需要做任何代码层面上的修改。

十一月 23, 2024 11:59:04 下午 cn.qzbook.AppTest testLog4
严重: Exception occurred.
java.lang.ArithmeticException: / by zero
at cn.qzbook.AppTest.testLog4(AppTest.java:62)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
.... 省略

5.6 日志重构

当项目中已经使用了 log4j 的日志框架,这是项目需要切换日志框架,将 log4j 切换到 logback 由于代码中已经使用了 log4j 的代码,修改

slf4j官网提供了 Bridging legacy APIs 桥接旧版 API ,可以在不改变原始代码的情况下切换到 slf4j 的日志框架

![[log4j-slf4j.png]]

示例:从 log4j 切换到 logback

使用 log4j 的情况: 添加依赖

<dependency>  
    <groupId>log4j</groupId>  
    <artifactId>log4j</artifactId>    
<version>1.2.17</version>
</dependency>

代码示例:此时日志对象的依赖包为 log4j org.apache.log4j.Logger

package cn.qzbook;  

import org.apache.log4j.Logger;  
import org.junit.Test;  
  
public class AppTest{  
    @Test  
    public void test1() {  
        // 使用配置文件  
        Logger logger = Logger.getLogger(AppTest.class);  
        logger.fatal("This is a fatal message.");  
        logger.error("This is an error message.");  
        logger.warn("This is a warning message.");  
        logger.info("This is an info message.");  
        logger.debug("This is a debug message.");  
        logger.trace("This is a trace message.");  
    }  
}

配置文件如下:

log4j.rootLogger=DEBUG, aa  
log4j.logger.cn.qzbook=DEBUG, outFile  
log4j.appender.aa=org.apache.log4j.ConsoleAppender  
log4j.appender.aa.layout=org.apache.log4j.PatternLayout  
log4j.appender.aa.layout.conversionPattern=%d{yyyy-MM-dd HH:mm:ss} %p %c{1}:%L %t - %m%n

控制台打印结果如下:

2024-11-25 23:05:36 FATAL AppTest:11 main - This is a fatal message.
2024-11-25 23:05:36 ERROR AppTest:12 main - This is an error message.
2024-11-25 23:05:36 WARN AppTest:13 main - This is a warning message.
2024-11-25 23:05:36 INFO AppTest:14 main - This is an info message.
2024-11-25 23:05:36 DEBUG AppTest:15 main - This is a debug message.

切换到 logback 日志输出

注释或删除 log4j 的依赖,并导入 slf4j 和 logback 的依赖

  
<!--        <dependency>-->  
<!--            <groupId>log4j</groupId>-->  
<!--            <artifactId>log4j</artifactId>--> 
<!--            <version>1.2.17</version>--> 
<!--        </dependency>-->  
  
<dependency>  
<groupId>org.slf4j</groupId>  
<artifactId>slf4j-api</artifactId>  
<version>1.7.25</version>
</dependency>  
<dependency>            
<groupId>org.slf4j</groupId>  
<artifactId>log4j-over-slf4j</artifactId> 
<version>1.7.25</version> 
</dependency>  
<dependency>            
<groupId>ch.qos.logback</groupId>  
<artifactId>logback-classic</artifactId>  
<version>1.2.3</version>
</dependency>

代码不变,控制台打印

23:08:45.282 [main] ERROR cn.qzbook.AppTest - This is a fatal message.
23:08:45.290 [main] ERROR cn.qzbook.AppTest - This is an error message.
23:08:45.290 [main] WARN cn.qzbook.AppTest - This is a warning message.
23:08:45.290 [main] INFO cn.qzbook.AppTest - This is an info message.
23:08:45.290 [main] DEBUG cn.qzbook.AppTest - This is a debug message.

此时,打印日志风格已经变为了 logback 的默认的风格,这表示日志框架已经无缝切换到了 logback,并且全程只修改了 maven 依赖,并没有对代码做任何调整。

需要注意的是 log4j 打印的日志因为自定义配置的关系,与 logback 的默认风格有点类似,但差异还是存在的。

实现无缝切换日志框架的核心是,桥接器 log4j-over-slf4j 依赖。

注意:log4j-over-slf4j 和 slf4j-log4j12 不能同时导入,会导致异常。 slf4j-log4j12 让 log4j 适配 slf4j 的 API,在代码中使用 slf4j 的 API。 log4j-over-slf4j 与上面功能类似,也是让 让 log4j 适配 slf4j ,但是在代码中仍然可以使用 log4j 的 API,适合项目框架重构。

6 Logback

Logback 是由 log4j 创始人设计的又一个开源日志组件。

Logback 当前分为三个模块: logback-core , logback-classic 和 logback-access

logback-access 访问模块与 servlet 容器集成提供通过 htpp 来访问日志的功能。

6.1 Logback 组件

6.1.1 Logger

日志记录器,主要用于存放日志对象,也可以定义日志类型,级别。

6.1.2 Appender

用于指定日志输出目的地,目的地可以使控制台、文件、数据库等等。

6.1.3 Layout

负责把事件转换成字符串,格式化的日志信息输出。在 Logback 中 Layout 对象被封装在 encoder 中。也就是说未来使用的 encoder 其实就是 Layout。

6.1.4 日志级别

日志级别 trace < debug < info < warn < error

6.2 Logback 配置文件

Logback 提供了 3 中配置文件

  • Logback.groovy
  • Logback-test.xml
  • Logback.xml 如果都不配置,则采用默认配置

6.3 日志输出格式

%-10level  级别,案例为设置 10 个字符,左对齐
%d{yyyy-MM-dd HH:mm:ss.SSS} 日期
%c 当前类的全限定名
%M 当前执行日志的方法
%L 行号
%thread 线程名称
%m 或者 %msg  信息
%n 换行

6.4 示例[XML 配置文件]

使用 SLF4J 日志门面的方式来使用 logback,代码示例

package cn.qzbook;  
  
import org.junit.Test;  
import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;  
  
public class AppTest {  
    @Test  
    public void testApp1() {  
        // 日志级别 trace < debug < info < warn < error        
        Logger logger = LoggerFactory.getLogger(AppTest.class);  
        logger.trace("This is a trace message");  
        logger.debug("This is a debug message");  
        logger.info("This is an info message");  
        logger.warn("This is a warn message");  
        logger.error("This is an error message");  
    }

xml 配置文件命名为 logback.xml 放在 maven 项目的 resource 目录即可,Logback 默认会读取这个位置的以 logback.xml 命名的配置文件。

<?xml version="1.0" encoding="UTF-8" ?>  
<configuration>  
<!--    <property name="LOG_HOME" value="${user.home}/logs"/>-->  
<!--    property 可以定义变量 name="变量名" value="变量值",在 logback.xml 中引用 -->  
  
    <!-- 日志输出路径-->  
    <property name="LOG_HOME" value="logs"/>  
    <!-- 日志 输出文本格式 -->  
    <property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %logger{36} - %msg%n"/>  
<!--    <property name="pattern" value="%msg%n"/>-->  
  
    <!--  输出日志到控制台  -->  
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">  
        <!--   日志 输出格式     -->  
        <target>System.err</target>  
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">  
            <!--   引用pattern变量  -->  
            <pattern>${pattern}</pattern>  
        </encoder>  
    </appender>  
  
    <!--  输出日志到文件  -->  
    <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">  
        <!--   日志 输出路径     -->  
        <file>${LOG_HOME}/app.log</file>  
        <!--   日志 输出格式     -->  
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">  
            <!--   引用pattern变量  -->  
            <pattern>${pattern}</pattern>  
        </encoder>  
        <!--   日志 输出滚动策略     -->  
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">  
            <!--   日志 输出文件名  -->  
            <fileNamePattern>${LOG_HOME}/app.%d{yyyy-MM-dd}.log</fileNamePattern>  
            <!--   日志 保留天数     -->  
            <maxHistory>30</maxHistory>  
        </rollingPolicy>  
        <!--   日志 输出文件大小     -->  
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">  
            <!--   日志 输出文件大小 50MB     -->            <maxFileSize>50MB</maxFileSize>  
        </triggeringPolicy>  
    </appender>  
  
    <!--  注意 logger 的配置 要在 appender 之后 否则不会生效 [在当前案例中是这样] -->  
    <logger name="cn.qzbook">  
        <level value="ALL"/>  
        <appender-ref ref="console"/>  
        <appender-ref ref="file"/>  
    </logger>  
  
    <!--  自定义的 logger 顺序在 root logger 之前  -->  
  
    <!--  root logger 的顺序在配置文件的最后,所以它将接收所有日志事件    -->  
    <!--  root logger 日志级别为INFO,输出到控制台  -->  
    <root level="ALL">  
        <appender-ref ref="console"/>  
    </root>  
  
</configuration>

Appender 的可配置项,可以查看具体的类的属性名称,以 RollingFileAppender 为例,以下为部分源码:

public class RollingFileAppender<E> extends FileAppender<E> {  
   File currentlyActiveFile;  
   TriggeringPolicy<E> triggeringPolicy;  
   RollingPolicy rollingPolicy;
// ... 省略其他源码
}

可以看到 rollingPolicy 和 triggeringPolicy 两个属性,对应 xml 文件中的标签 和 这两个标签,同理,这两个标签下可配置项也是其类中的属性,层层嵌套。

6.5 过滤器

参见示例配置文件

<?xml version="1.0" encoding="UTF-8" ?>  
<configuration>  
  
    <!-- 日志 输出文本格式 -->  
    <property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %logger{36} - %msg%n"/>  
  
    <!--  输出日志到控制台  -->  
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">  
        <!--   日志 输出格式     -->  
        <target>System.err</target>  
        <!--   日志级别过滤器   -->  
        <filter class="ch.qos.logback.classic.filter.LevelFilter">  
            <!--   日志级别过滤   -->  
            <level>TRACE</level>  
            <!--   日志级别匹配 若匹配则输出         -->  
            <onMatch>ACCEPT</onMatch>  
            <!--   日志级别不匹配 若不匹配则不输出   -->  
            <onMismatch>DENY</onMismatch>  
        </filter>  
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">  
            <!--   引用pattern变量  -->  
            <pattern>${pattern}</pattern>  
        </encoder>  
    </appender>  
    <root level="ALL">  
        <appender-ref ref="console"/>  
    </root>  
</configuration>

6.6 异步日志

java代码示例

@Test  
public void testApp2() {  
    // 日志级别 trace < debug < info < warn < error    Logger logger = LoggerFactory.getLogger(AppTest.class);  
  
    for (int i = 0; i < 1000000; i++) {  
        logger.trace("This is a trace message");  
        logger.debug("This is a debug message");  
        logger.info("This is an info message");  
        logger.warn("This is a warn message");  
        logger.error("This is an error message");  
    }  
  
    System.out.println("------------ This is a normal message 1");  
    System.out.println("------------ This is a normal message 2");  
    System.out.println("------------ This is a normal message 3");  
    System.out.println("------------ This is a normal message 4");  
}

xml 配置文件

<?xml version="1.0" encoding="UTF-8" ?>  
<configuration>  
  
    <!-- 日志 输出文本格式 -->  
    <property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %logger{36} - %msg%n"/>  
  
    <!--  输出日志到控制台  -->  
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">  
        <!--   日志 输出格式     -->  
        <target>System.err</target>  
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">  
            <!--   引用pattern变量  -->  
            <pattern>${pattern}</pattern>  
        </encoder>  
    </appender>  
  
    <root level="ALL">  
        <!--  引用async 输出器  -->  
        <appender-ref ref="console"/>  
    </root>  
</configuration>

正常情况下日志打印完成才会指定到 System.out.println 语句,代码是按照顺序同步执行的。引申出一个问题,日志的打影响到了业务代码的执行,如果日志的记录数据量较大,会直接影响业务代码的执行速度,导致服务迟滞,这就本末倒置了。 logback 提供了异步执行日志的方式来打印日志,日志的打印不影响业务流程的执行。

使用异步,新增一个Appender,实现类为 AsyncAppender ,在 AsyncAppender 标签中可以直接引用其他的 Appender 配置,在 root 或者自定 logger 配置中将引用 AsyncAppender logger 即可实现异步打印日志。 配置如下:

<?xml version="1.0" encoding="UTF-8" ?>  
<configuration>  
  
    <!-- 日志 输出文本格式 -->  
    <property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %logger{36} - %msg%n"/>  
  
    <!--  输出日志到控制台  -->  
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">  
        <!--   日志 输出格式     -->  
        <target>System.err</target>  
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">  
            <!--   引用pattern变量  -->  
            <pattern>${pattern}</pattern>  
        </encoder>  
    </appender>  
  
    <!--  日志 异步  -->  
    <appender name="async" class="ch.qos.logback.classic.AsyncAppender">  
        <!--  引用console 输出器  -->  
        <appender-ref ref="console"/>  
    </appender>  
  
    <root level="ALL">  
        <!--  引用async 输出器  -->  
        <appender-ref ref="async"/>  
    </root>  
</configuration>

注意: AsyncAppender 引用的 Appender 不能使用过滤器,使用过滤器,看控制台的数据顺序,还是顺序[同步]执行的,异步没有生效。

6.7 自定义 Logger

配置实例

<?xml version="1.0" encoding="UTF-8" ?>  
<configuration>  
  
    <!-- 日志 输出文本格式 -->  
    <property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %logger{36} - %msg%n"/>  
  
    <!--  输出日志到控制台  -->  
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">  
        <!--   日志 输出格式     -->  
        <target>System.err</target>  
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">  
            <!--   引用pattern变量  -->  
            <pattern>${pattern}</pattern>  
        </encoder>  
    </appender>  
  
    <root level="ERROR">  
        <appender-ref ref="console"/>  
    </root>  
  
    <!--  自定义 logger additivity="false" 关闭父类的日志输出[不继承父类的日志配置]   -->  
    <logger name="cn.qzbook" level="DEBUG" additivity="false">  
        <!--  引用console appender      -->  
        <appender-ref ref="console"/>  
    </logger>  
</configuration>

6.7 补充

异步日志:可配置属性,配置了一个阈值,但队列的剩余容量小于这个阈值的时候,当前日志级别的 trace、debug、info这个三个级别的日志将被丢弃。设置为 0 ,说明永不丢弃 trace、debug、info 这三个级别的日志。

<discardingThreshold>0</discardingThreshold>

配置队列的深度,这个值会影响记录日志的性能,默认值是 256

<queueSize>256</queueSize>

一般使用默认配置即可,配置为其他值可能会影响性能,了解功能即可

关于不同日志的实现,配置文件也是不通的,例如 log4j 经常使用的是 properties 属性文件,logback 使用的是 xml 配置文件,将项目日志记录框架从 log4j 切换到 logback 的时候,可以借助 logback 官网工具,将 log4j 的 properties 配置文件,转换为 logback 的 xml 格式配置文件 网址:https://logback.qos.ch/translator/

转换工具有限翻译,log4j 与 logback 都支持的配置才会被翻译

7 Log4j2

性能提升: log4j2 包含基于 LAMX Disruptor 库的下一代异步记录器。在多线程场景中,异步记录器的吞吐比 log4j 1.x 和 Logback 高18倍,延迟低。

自动重新加载配置 与 Logback 一样,Log4j2 可以再修改时自动重新加载器配置。与 Logback 不同,会在重新配置发生时不会丢失日志事件。

高级过滤 与 Logback 一样,Log4j2 支持基于 Log 事件上下文数据,标记,正则表达式和其他组件进行过滤。 此外,过滤器还可以与记录器关联。与 Logback 不同,Log4j2 可以在任何这些情况下使用通用的 Filter 类。

插件架构 Log4j2 使用插件模式配置组件。因此,无需编写代码来创建和配置 appender ,Layout , Pattern Converter 等。在配置了的情况下, Log4j2 会自动识别插件并使用它们。

无垃圾机制 在稳态日志记录期间 Log4j2 在独立应用程序中是无垃圾的,在 web 引用程序中是低垃圾。这减少了垃圾收集器的压力,并且可以提供更好的响应性能。

7.1 入门示例

导入依赖

<!-- log4j2 的日志门面 -->
<dependency>  
  <groupId>org.apache.logging.log4j</groupId>  
  <artifactId>log4j-api</artifactId>  
  <version>2.12.1</version>  
</dependency>  
<!-- log4j2 的日志实现 -->  
<dependency>  
  <groupId>org.apache.logging.log4j</groupId>  
  <artifactId>log4j-core</artifactId>  
  <version>2.12.1</version>  
</dependency>

代码

package cn.qzbook;  
  
import org.apache.logging.log4j.LogManager;  
import org.apache.logging.log4j.Logger;  
import org.junit.Test;  
  
public class AppTest {  
    @Test  
    public void testApp() {  
        
        Logger logger = LogManager.getLogger(AppTest.class);  
        logger.fatal("fatal message");  
        logger.error("error message");  
        logger.warn("warn message");  
        logger.info("info message");  
        logger.debug("debug message");  
        logger.trace("trace message");  
  
        /**  
         * ERROR StatusLogger No Log4j 2 configuration file found. Using default configuration (logging only errors to the console), or user programmatically provided configurations. Set system property 'log4j2.debug' to show Log4j 2 internal initialization logging. See https://logging.apache.org/log4j/2.x/manual/configuration.html for instructions on how to configure Log4j 2         * 23:02:04.085 [main] FATAL cn.qzbook.AppTest - fatal message         * 23:02:04.087 [main] ERROR cn.qzbook.AppTest - error message         *         *   
* log4j2 默认日志级别为 ERROR,因此以上日志级别为 ERROR 及以上级别的日志才会被输出。  
         *   
         *   
         * ERROR StatusLogger No Log4j 2 configuration file found. Using default configuration  
         * 表示没有找到 log4j2 的配置文件,使用默认配置(只输出错误日志到控制台),或者用户通过编程方式提供的配置。  
         */  
    }  
  
}

控制台输出

ERROR StatusLogger No Log4j 2 configuration file found. Using default configuration (logging only errors to the console), or user programmatically provided configurations. Set system property 'log4j2.debug' to show Log4j 2 internal initialization logging. See https://logging.apache.org/log4j/2.x/manual/configuration.html for instructions on how to configure Log4j 2
23:02:04.085 [main] FATAL cn.qzbook.AppTest - fatal message
23:02:04.087 [main] ERROR cn.qzbook.AppTest - error message

log4j2 与 log4j 的日志级别一致: fatal > error > warn > info > debug > trace
log4j2 默认日志级别为 ERROR,因此以上日志级别为 ERROR 及以上级别的日志才会被输出。

控制台错误信息:ERROR StatusLogger No Log4j 2 configuration file found. Using default configuration 表示没有找到 log4j2 的配置文件,使用默认配置(只输出错误日志到控制台),或者用户通过编程方式提供的配置。

7.2 使用配置文件

log4j2 默认加载类路径(resource)下的 log4j2.xml 文件中的配置。

<?xml version="1.0" encoding="UTF-8" ?>  
<Configuration>  
    <!--  定义 appender 输出到控制台  -->  
    <Appenders>  
        <Console name="Console" target="SYSTEM_OUT">  
            <!-- 日志输出格式 -->  
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>  
        </Console>  
    </Appenders>  
    <!-- 定义日志器 -->  
    <Loggers>  
        <!-- 定义日志级别 -->  
        <Root level="info">  
            <!-- 定义日志输出到控制台 -->  
            <AppenderRef ref="Console"/>  
        </Root>  
    </Loggers>  
</Configuration>

控制台输出

23:11:53.391 [main] FATAL cn.qzbook.AppTest - fatal message
23:11:53.393 [main] ERROR cn.qzbook.AppTest - error message
23:11:53.393 [main] WARN  cn.qzbook.AppTest - warn message
23:11:53.393 [main] INFO  cn.qzbook.AppTest - info message

根标签,所有日志相关信息,都在根标签中进行配置. 根标签中,可添加属性 status="debug" 日志框架根深的日志输出级别 monitorInterval="数值" 自动加载配置文件的间隔时间,单位秒(自动重新加载)

<Configuration status="debug" monitorInterval="1">  
</Configuration>

7.3 使用 SLF4J门面

教程 URL : https://www.bilibili.com/video/BV1Mb4y1Z74W?spm_id_from=333.788.player.switch&vd_source=6c6e9b25d282f47b0d3022737f870fce&p=99