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() {public void run() {logger.error("Hello World --by Liu, Test01:测试日志1 ,系统时间 "+ sdf.format(System.currentTimeMillis()) );}},1000,1000);}}
配置详解:
通过
ConfigurationFactory使用编程的方式进行配置通过配置文件配置,支持
XML、JSON、YAML和properties等文件类型
Configuration
Confirguration 是配置文件根元素,每个配置文件有且仅有一个。如果不使用配置文件使用默认配置,以下配置与默认配置等价:
<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>
每一个配置都至少包含一个 Appender 和 Logger。status 属性用于配置 Log4j2 内部的的日志级别。
Log4j2 可以自动检测配置文件的变化,monitorInterval 属性可以配置自动检测配置文件的时间,这个属性最小的值为 5,代表 5 秒检测一次。
在加入依赖完成后,我们开始Lo4j2进行配置,这里重点关注log4j2的配置文件的读取位置
说明:log4j2默认读取配置文件的位置如下
(1)非Maven项目,src目录下的log4j2.xml
(2)Maven项目,resouces目录下的log4j2.xml
本文中log4j2的配置文件内容具体如下:
<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 有两种形式,Logger 和 Root,两个区别在于 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 后开始生成新的日志文件。
RollingRandomAccessFileAppender 与 RollingFileAppender 在功能上基本一致,但是底层的实现有所区别,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;@Slf4jpublic class Test01 {public static void main(String[] args) {SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//在类中引入注解@Slf4j后就可以使用,使用时固定的记录器是loglog.error("Hello World --by Liu, Test01:测试日志1 ");new Timer("timer").schedule(new TimerTask() {@Overridepublic 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
