vlambda博客
学习文章列表

Java项目主流日志解决方案介绍【Log4j2+Slf4j+Lombok】

        在大型系统中,日志是一个很重要的部分,线上问题的排查很大程度上依赖日志。记录日志的过程,大体上可以分成三个步骤:

  • 在程序中对原始日志信息进行采集

  • 对采集下来的日志信息进行格式化

  • 将格式化好的日志信息写入目的地

    Log4j2 的架构也自然是按照这个来的,Log4j2 中有三个重要的组件分别实现了这些功能,分别是 Logger 、Layout 和Appender,日志数据流向图如下:


Log4j2的使用

Log4j2 使用很简单,以 Maven 为例,只需要在 pom.xml 中添加:


<dependencies> <!-- log4j2的使用--> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.12.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.12.1</version> </dependency></dependencies>

然后就可以在 Java 代码中直接使用:


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

import java.text.SimpleDateFormat;import java.util.Timer;import java.util.TimerTask;

public class Log4j2Test {
private static final Logger logger = LogManager.getLogger(Log4j2Test.class);

public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); logger.error("Hello World --by Liu, Test01:测试日志1 "); new Timer("timer").schedule(new TimerTask() { @Override public void run() { logger.error("Hello World --by Liu, Test01:测试日志1 ,系统时间 "+ sdf.format(System.currentTimeMillis()) ); } },1000,1000);
}
}

配置详解:

  • 通过 ConfigurationFactory 使用编程的方式进行配置

  • 通过配置文件配置,支持 XML 、JSON 、YAML 和 properties 等文件类型

Configuration

Confirguration 是配置文件根元素,每个配置文件有且仅有一个。如果不使用配置文件使用默认配置,以下配置与默认配置等价:

<?xml version="1.0" encoding="UTF-8"?><Configuration status="WARN"> <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="error"> <AppenderRef ref="Console"/> </Root> </Loggers></Configuration>

每一个配置都至少包含一个 AppenderLoggerstatus 属性用于配置 Log4j2 内部的的日志级别。

Log4j2 可以自动检测配置文件的变化,monitorInterval 属性可以配置自动检测配置文件的时间,这个属性最小的值为 5,代表 5 秒检测一次。


在加入依赖完成后,我们开始Lo4j2进行配置,这里重点关注log4j2的配置文件的读取位置

    说明:log4j2默认读取配置文件的位置如下

        (1)非Maven项目,src目录下的log4j2.xml

        (2)Maven项目,resouces目录下的log4j2.xml


本文中log4j2的配置文件内容具体如下:

<?xml version="1.0" encoding="UTF-8"?><Configuration status="debug" name="defaultLogConfig" packages=""> <properties> <property name="LOG_HOME">D:\export\Logs\log.demo</property> <property name="LOG_LEVEL">INFO</property> <property name="patternlayout"> [%t] %d{yyyy-MM-dd HH:mm:ss SSS} %-5level %logger{1} %L %M - %msg%xEx%n </property> <!-- 入库模块WORKER日志 --> <property name="jdx-cis-inbound">${LOG_HOME}/jdx-cis-inbound.log</property>
</properties>
<Appenders> <Console name="Console" target="SYSTEM_OUT" follow="true"> <PatternLayout pattern="${patternlayout}"/> </Console> <!-- name: 输出端的名字 fileName: 指定当前日志文件的位置和文件名称 filePattern: 指定当发生自动封存日志时,文件的转移和重命名规则 这个filePatten结合下面的TimeBasedTriggeringPolicy一起使用,可以实现控制日志按天生成文件. 自动封存日志的策略可以设置时间策略和文件大小策略(见下面的Policies配置) 时间策略: 文件名_%d{yyyy-MM-dd}_%i.log 这里%d表示自动封存日志的单位是天 如果下面的TimeBasedTriggeringPolicy的interval设为1, 表示每天自动封存日志一次;那么就是一天生成一个文件。 文件大小策略: 如果你设置了SizeBasedTriggeringPolicy的size的话, 超过了这个size就会再生成一个文件,这里的%i用来区分的 --> <!-- 入库模块日志--> <RollingFile name="InboundRollingFile" fileName="${jdx-cis-inbound}" filePattern="${LOG_HOME}/jdx-cis-inbound-%d{yyyy-MM-dd}-%i.log"> <PatternLayout pattern="${patternlayout}"/> <Policies> <!-- 根据上图:filePattern的设置,每隔一天生成一个日志文件。--> <TimeBasedTriggeringPolicy/> <!--如果果今天的文件大小到了设定的size,则会新生成一个文件,上面的%i就表示今天的第几个文件--> <SizeBasedTriggeringPolicy size="20 MB"/> </Policies> <!-- DefaultRolloverStrategy属性如不设置, 则默认为最多同一文件夹下7个文件,这里设置了50 --> <DefaultRolloverStrategy max="50"/> </RollingFile> </Appenders>

<Loggers> <!-- 这里引用上面定义的输出端,千万不要漏了。 --> <!-- 入库模块日志,此处猪样,配置不同的代码包输出的对应的路径下 --> <AsyncLogger name="com.yuewei.log4j.ws" level="${LOG_LEVEL}" additivity="true" > <AppenderRef ref="InboundRollingFile"/> </AsyncLogger>
<Root level="INFO"> <AppenderRef ref="Console"/> </Root> </Loggers>
</Configuration>

Logger

Logger 元素用于配置日志记录器。Logger 有两种形式,LoggerRoot,两个区别在于 Root 没有 name 属性并且不支持 additivity 属性。

假如我们要获取一个特定类的某个级别的日志。调高日志的级别无法完成这样的需求。一种可行的方式是在 Loggers 中添加一个 Logger,配置方式如下:

<Loggers> <Logger name="cn.rayjun.Log4j2HelloWorld" level="trace"> <AppenderRef ref="Console"/> </Logger> <Root level="error"> <AppenderRef ref="Console"/> </Root> </Loggers>

这样配置,除了 cn.rayjun.Log4j2HelloWorld 类会打印出 trace 级别的日志,其他就只会打印出 error 级别的日志,打印结果如下:

19:59:06.957 [main] TRACE cn.rayjun.Log4j2HelloWorld - Enter19:59:06.957 [main] TRACE cn.rayjun.Log4j2HelloWorld - Enter19:59:06.959 [main] ERROR cn.rayjun.Log4j2HelloWorld - Hello world19:59:06.959 [main] ERROR cn.rayjun.Log4j2HelloWorld - Hello world


但是上面打印的日志有个问题,信息都被打印了两遍,为了解决这个问题,需要添加 additivity 参数并且置为 false,如下配置:

<Loggers> <Logger name="cn.rayjun.Log4j2HelloWorld" level="trace" additivity="false"> <AppenderRef ref="Console"/> </Logger> <Root level="error"> <AppenderRef ref="Console"/> </Root></Loggers>

默认情况下 Logger 都是同步的,但是也有异步的实现,Root 的异步版本是 AsyncRoot,Logger 的异步版本是 AsyncLogger,异步 Logger 依赖外部队列 LMAX Disruptor。

使用异步 Logger 需要加上外部依赖:


 <!-- 使用异步日志增加如下的配置--><dependency> <groupId>com.lmax</groupId> <artifactId>disruptor</artifactId> <version>3.4.2</version></dependency>

使用异步 Logger:


<AsyncRoot level="error"> <AppenderRef ref="Console"/></AsyncRoot> <AsyncLogger name="com.yuewei.log4j.ws" level="${LOG_LEVEL}" additivity="true" > <AppenderRef ref="InboundRollingFile"/>  </AsyncLogger>

Logger 在具体使用中需要声明一个 Logger 对象,官方推荐将 Logger 声明为一个静态变量,可以提高日志记录的性能:

    private static final Logger logger = LogManager.getLogger(Log4j2Test.class);

Appender 配置:

Appender 负责将日志分发到相应的目的地。也就是说 Appender 决定日志以何种方式展示,上面使用到的就是 ConsoleAppender,这个 Appender 会将日志直接打印到控制台。同时还支持将日志输出到文件数据库消息队列 。

FileAppender 基本配置如下:

<File name="MyFile" fileName="logs/app.log"> <PatternLayout> <Pattern>%d %p %c{1.} [%t] %m%n</Pattern> </PatternLayout></File>

如果想要把日志记录到数据库中,那就使用JDBCAppender, 基本配置如下:

<JDBC name="databaseAppender" tableName="dbo.application_log"> <DataSource jndiName="java:/comp/env/jdbc/LoggingDataSource" /> <Column name="eventDate" isEventTimestamp="true" /> <Column name="level" pattern="%level" /> <Column name="logger" pattern="%logger" /> <Column name="message" pattern="%message" /> <Column name="exception" pattern="%ex{full}" /></JDBC>

默认情况下 Appender 都是同步的,就是说日志产生的时候就会进行处理。但是有时候会从程序性能的角度进行考虑,生成的日志不会立即进行刷盘或者进行传输,而是在一个合适的时间集中进行处理,配置方式如下,异步 Appender 使用 ArrayBlockingQueue 作为队列,与异步 Logger 不同,异步 Appender 不需要外部依赖,但是官方推荐使用异步 Logger 而不是异步 Appender:

<Appenders> <File name="MyFile" fileName="logs/app.log"> <PatternLayout> <Pattern>%d %p %c{1.} [%t] %m%n</Pattern> </PatternLayout> </File> <Async name="Async"> <AppenderRef ref="MyFile"/> </Async></Appenders>

把日志保存为文件是一个常用的操作,保存在文件中的日志以追加方式写入,但是单个文件不可能无限增大,也不可能手工来分割日志文件,所以需要通过自动的方式来分割日志。这就需要使用 RollingFileAppender,通过设定日志分割的条件。分割的条件可以从两个方面进行设定,以时间频率或者日志文件的大小来触发日志的分割。

<RollingFile name="InboundRollingFile" fileName="${jdx-cis-inbound}" filePattern="${LOG_HOME}/jdx-cis-inbound-%d{yyyy-MM-dd}-%i.log"> <PatternLayout pattern="${patternlayout}"/> <Policies> <!-- 根据上图:filePattern的设置,每隔一天生成一个日志文件。--> <TimeBasedTriggeringPolicy/> <!--如果果今天的文件大小到了设定的size,则会新生成一个文件,上面的%i就表示今天的第几个文件--> <SizeBasedTriggeringPolicy size="20 MB"/> </Policies> <!-- DefaultRolloverStrategy属性如不设置, 则默认为最多同一文件夹下7个文件,这里设置了50 --> <DefaultRolloverStrategy max="50"/> </RollingFile>


TimeBasedTriggeringPolicy 会与 filePattern 的配置相匹配,如果 filePattern 是 {yyyy-MM-dd HH-mm} ,最小的时间粒度是分钟,那么就会每隔一分钟生成一个文件,如果改成 {yy-MM-dd HH},最小时间粒度是小时,那么就会每隔一个小时生成一个文件。SizeBasedTriggeringPolicy 表示设定日志的大小,上面的配置是指日志大小到 20M 后开始生成新的日志文件。

RollingRandomAccessFileAppenderRollingFileAppender 在功能上基本一致,但是底层的实现有所区别,RollingFileAppender 底层是 BufferedOutputStream,RollingRandomAccessFileAppender 底层是使用 ByteBuffer + RandomAccessFile ,性能上有了很大的提升。


Layouts:

Appender 会使用 Layout 来对日志进行格式化。Lo4j1 和 Logback 中的 Layout 会把日志转成 String,而在 Log4j2 中使用的则是 byte 数组,这是从性能的角度进行的优化。

Layout 配置支持多种方式。用的最多的方式就是 PatternLayout,就是通过正则表达式来格式化日志,应用的也最多,基本配置如下:


<PatternLayout> <Pattern>%d %p %c{1.} [%t] %m%n</Pattern></PatternLayout>

或者采用本文案例中的方式。

  • %d 表示时间,默认情况下表示打印完整时间戳 2012-11-02 14:34:02,123,可以调整 %d 后面的参数来调整输出的时间格式

  • %p 表示输出日志的等级,可以使用 %highlight{%p} 来高亮显示日志级别

  • %c 用来输出类名,默认输出的是完整的包名和类名,%c{1.} 输出包名的首字母和完整类名

  • %t 表示线程名称

  • %m 表示日志内容,%M 表示方法名称

  • %n 表示换行符

  • %L 表示打印日志的代码行数。


Logger 上如果加上了 includeLocation 后,日志性能会下降的很厉害,如果日志的位置信息不是必要的,就不需要加上


配合 Slf4j 使用:

Simple Logging Facade for Java(SLF4J)用作各种日志框架(例如 java.util.logging,logback,log4j)的简单外观或抽象,允许最终用户在部署时插入所需的日志框架。

slf4j 就是众多日志接口的集合,他不负责具体的日志实现,只在编译时负责寻找合适的日志系统进行绑定

Log4j2 和logbook是具体的日志框架的实现。

使用 Slf4j 非常简单,只需要在项目中加入以下依赖:


<!-- slfg4j的使用--> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.12.1</version> </dependency>

然后就可以在代码中使用了:

import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class Slf4jDemo {
public static void main(String[] args) { Logger logger = LoggerFactory.getLogger(Slf4jDemo.class); logger.info("Slf4j log info"); }}


配合lombok的使用:

在pom文件中引入lombok的依赖:

 <!--lombok的使用--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.16</version> </dependency>


代码示例如下:

import lombok.extern.slf4j.Slf4j;import java.text.SimpleDateFormat;import java.util.Timer;import java.util.TimerTask;
@Slf4j public class Test01 {
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//在类中引入注解@Slf4j后就可以使用,使用时固定的记录器是log log.error("Hello World --by Liu, Test01:测试日志1 ");

new Timer("timer").schedule(new TimerTask() { @Override public void run() { log.error("Hello World --by Liu, Test01:测试日志1 ,系统时间 "+ sdf.format(System.currentTimeMillis()) ); log.error("参数1:{},参数2:{}","lombok时间",sdf.format(System.currentTimeMillis())); } },1000,1000); }


小结:

输出日志时,建议采用Sl4j作为日志的门面,方便后续切换日志框架。



参考资料:https://juejin.cn/post/6844903990619013134https://logging.apache.org/log4j/2.x/manual/index.html