日志可以帮助开发者理解和调试程序,日志数据可以很好地还原代码执行场景,便于问题的定位和分析。
本文将讨论 Java 中流行的日志框架 Log4j 、Log4j2 、Logback等。
日志框架引入的必要性
在编写程序时,使用System.out.println
输出似乎是最简单、最容易的选择。无需将额外的库添加到类路径中,也无需进行其他配置。
但是使用System.out和System.err有一些局限性:
- 文件日志:系统输出日志无法直接存储到文件,需要额外的工具支持。
- 日志文件管理:无法控制日志文件的大小,导致文件不断增长,难以管理。
- 日志级别:内置的日志级别有限,只有标准输出和错误输出。
- 日志集成:将日志输出到日志分析系统(如Kafka Stream、Elasticsearch)存在困难。
- 日志定制:自定义日志信息(如方法名、时间、线程号等)需要大量手动工作。
- 异常栈跟踪:使用printStackTrace输出异常信息时,会产生大量冗余信息,消耗内存。
日志框架引入步骤
在项目内部启用日志记录遵循三个常见步骤:
- 添加依赖:在项目的pom.xml文件中添加所需的日志框架依赖。
- 配置:根据所选的日志框架进行配置,如Log4j2的log4j2.xml或Logback的logback.xml。
- 编写日志代码:在Java代码中使用相应的日志API进行日志记录。
日志框架介绍
Log4J
Log4j是早期的Java日志框架,提供了基本的日志记录功能。随着时间的推移,Log4j已经逐渐被Log4j2所取代。
Log4j依赖库
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
log4j 最新版已经是2012年发布的,如果项目中还在使用应该尽快升级到 log4j2了。
Log4j配置
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd" >
<log4j:configuration debug="false">
<!--Console appender-->
<appender name="stdout" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern"
value="%d{yyyy-MM-dd HH:mm:ss} %p %m%n" />
</layout>
</appender>
<root>
<level value="INFO" />
<appender-ref ref="stdout" />
</root>
</log4j:configuration>
代码使用
import org.apache.log4j.Logger;
public class Log4jExample {
private static Logger logger = Logger.getLogger(Log4jExample.class);
public static void main(String[] args) {
logger.debug("Debug log 日志");
logger.info("Info log 日志");
logger.error("Error log 日志");
}
}
2024-03-16 21:32:54 ERROR Error log 日志
Log4j2
Log4j2是Log4j的升级版本,引入了许多新特性,如异步日志记录支持。Log4j2的性能也比Log4j有显著提升。
Log4j2依赖库
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.23.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.23.1</version>
</dependency>
最新版本可以在mvn中央仓库查看https://mvnrepository.com
Log4j2配置
配置Log4j 2是基于主配置log4j2.xml
文件,首先要配置的是Appender。
这些决定了日志消息将被路由到的位置,目的地可以是控制台Console、文件File、套接字Socket等。
<Configuration status="warn" name="mapull" packages="">
<Appenders>
<Console name="STDOUT" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %p %m%n"/>
</Console>
</Appenders>
</Configuration>
你可以为每个Appender设置一个名称;例如,使用名称CONSOLE而不是STDOUT。
请注意PatternLayout元素,这决定了消息的格式。
上面示例中,样式是根据pattern参数设置的,其中:
- %d 确定日期格式
- %p 日志级别输出
- %m 记录消息的输出
- %n 添加新行符号
有关样式的更多信息可以在官方Log4j 2 Layouts页面上找到。
最后,要启用一个(或多个)Appender,只需要将其添加到
<Loggers>
<Root level="error">
<AppenderRef ref="STDOUT"/>
</Root>
</Loggers>
记录到文件
有时,我们需要将日志记录到文件,因此我们将 File Appender添加到配置中:
<Appenders>
<File name="MyFile" fileName="logs/app.log" append="true">
<PatternLayout>
<Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
</PatternLayout>
</File>
</Appenders>
- fileName 表示日志文件的文件名
- append 参数的默认值为 true,这意味着默认情况下,File Appender将追加到现有文件而不是覆盖原始内容。
启用File Appender,您需要将其添加到
<Root level="INFO">
<AppenderRef ref="STDOUT" />
<AppenderRef ref="MyFile"/>
</Root>
完整的配置文件 log4j2.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" name="mapull" packages="">
<Appenders>
<File name="MyFile" fileName="logs/app.log" append="true">
<PatternLayout>
<Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
</PatternLayout>
</File>
<Console name="STDOUT" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %p %m%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="STDOUT" />
<AppenderRef ref="MyFile"/>
</Root>
</Loggers>
</Configuration>
异步日志
如果想让 Log4j2 异步,需要将 LMAX Disruptor 库添加到pom.xml中。 LMAX Disruptor 是一个无锁线程间通信库。
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>4.0.0</version>
</dependency>
使用 LMAX Disruptor以后,需要在配置中使用<asyncRoot>
而不是<Root>
:
<AsyncRoot level="error">
<AppenderRef ref="STDOUT" />
<AppenderRef ref="MyFile"/>
</AsyncRoot>
编写日志代码
以下是一个简单的示例,演示了如何使用 Log4j 进行日志记录:
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
public class Log4jExample {
private static Logger logger = LogManager.getLogger(Log4jExample.class);
public static void main(String[] args) {
logger.debug("Debug log 日志");
logger.info("Info log 日志");
logger.error("Error log 日志");
}
}
运行后,应用程序会将以下消息记录到控制台和名为 app.log 的文件中:
2024-03-16 22:02:13 ERROR Error log 日志
如果将根日志级别提升为ERROR:
<Root level="ERROR">
<AppenderRef ref="STDOUT" />
<AppenderRef ref="MyFile"/>
</Root>
输出将有所变化:
将日志基本调高,将导致低等级的日志信息丢失掉。
包级别配置
如果我们需要某个特定包中的日志输出有特殊要求,可以在log4j2.xml的<Root>
之前添加以下部分:
<Logger name="com.mapull.log.demo" level="debug">
<AppenderRef ref="stdout"/>
</Logger>
它将启用com.mapull.log.demo
包的日志记录,输出将如下所示:
2024-03-16 22:15:28 INFO Info log 日志
2024-03-16 22:15:28 ERROR Error log 日志
Logback
Logback 是 Log4j 的改进版本,由开发 Log4j 的同一开发人员开发。
Logback 还具有比 Log4j 更多的功能,其中许多功能也被引入到 Log4j 2 中。
Logback依赖库
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.3</version>
</dependency>
此依赖项将间接引入另外两个依赖项:logback-core和slf4j-api。
Logback配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds">
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<contextName>logback</contextName>
<property name="log.path" value="logs"></property>
<property name="Console_Pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%logger{50}] - %msg%n"/>
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<Pattern>${Console_Pattern}</Pattern>
<!-- 设置字符集 -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 时间滚动输出 level为 INFO 日志 -->
<appender name="RollingFileInfo" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/info.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%logger{50}] - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 每天日志归档路径以及格式 -->
<fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录info级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 时间滚动输出 level为 WARN 日志 -->
<appender name="RollingFileWarn" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/warn.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%logger{50}] - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录warn级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>warn</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 时间滚动输出 level为 ERROR 日志 -->
<appender name="RollingFileError" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/error.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%logger{50}] - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录ERROR级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!--additivity:是否继承root节点,默认是true继承。默认情况下子Logger会继承父Logger的appender,
也就是说子Logger会在父Logger的appender里输出。
若是additivity设为false,则子Logger只会在自己的appender里输出,而不会在父Logger的appender里输出。-->
<logger name="org.springframework" level="INFO" additivity="false">
<appender-ref ref="Console"/>
<appender-ref ref="RollingFileInfo"/>
</logger>
<logger name="org.mybatis" level="INFO"></logger>
<Logger name="org.apache.catalina" level="info"/>
<Logger name="org.apache.tomcat.util" level="info"/>
<!-- 从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF-->
<root level="ALL">
<appender-ref ref="Console"/>
<appender-ref ref="RollingFileWarn"/>
<appender-ref ref="RollingFileError"/>
</root>
<!--生产环境:输出到文件-->
<springProfile name="pro">
<root level="INFO">
<appender-ref ref="Console"/>
<appender-ref ref="RollingFileInfo"/>
<appender-ref ref="RollingFileWarn"/>
<appender-ref ref="RollingFileError"/>
</root>
</springProfile>
</configuration>
Logback使用SLF4J作为接口,因此需要导入SLF4J的Logger和LoggerFactory。
SLF4J
SLF4J 为大多数 Java 日志框架提供了通用接口和抽象。它充当门面并提供标准化 API 用于访问日志记录框架的底层功能。
Logback 的功能就使用到了 SLF4J 的原生 API。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Log4jExample {
private static Logger logger = LoggerFactory.getLogger(Log4jExample.class);
public static void main(String[] args) {
logger.debug("Debug log 日志");
logger.info("Info log 日志");
logger.error("Error log 日志");
}
}
总结
日志框架对于Java应用程序的开发和维护至关重要。它们提供了灵活的日志级别控制、方便的日志文件管理、易于集成的日志分析功能以及丰富的日志定制选项。Log4j、Log4j2和Logback是Java社区中最流行的三个日志框架,它们各有特点和优势。开发者可以根据项目需求和个人偏好选择合适的日志框架。通过使用日志框架,可以显著提高代码的可读性和可维护性,同时也便于问题的排查和解决。