背景
日志框架一直没进行过细致的梳理,对其理解仅留在基本使用的程度;周五自测小功能时暴露了技术漏洞😦花费了不必要的时间,因此趁周末补习一下顺便做个总结。
本文主题内容是介绍Java中常用的日志框架解决方案,包括log4j+jcl以及logback+slf4j, 并介绍通过桥接和适配技术实现日志框架的兼容。文中会对日志框架进行拆分,分别介绍facade层和impl层以及其组合关系。
1.介绍
日志在软件的生命周期中起到重要作用,开发、测试、维护各个阶段都离不开日志。日志可以帮助developer快速追踪、定位问题,还可以用于性能监控以及后期的大数据分析。日志框架的作用自然是方便程序员记录日志。
1.1 日志发展过程
上图所示是日志框架的发展流程图,其中log4j和jcl以及log4j2来源于apache, slf4j和logback来源于qos公司,jul来自jdk; 其中log4j2在国内不常用,因此下文不进行介绍(可作为log4j的替代品)。
1.2 日志框架图
日志框架中大量使用了结构型设计模式,有外观、代理、桥接等。jcl和slf4j作为门面规范了接口, 同时对客户端隐藏了内部实现。
jcl通过代理在实现层有不同的实现方案,如log4j或jul;而slf4j和logback是qos公司一起推出的,因此slf4j的实现层默认是logback。
说明: 以下使用facade层指代jcl和slf4j,使用impl层指代log4j和jul和logback.
下图所示是Java的日志框架图,包含了facade层和impl层及其组合方案:
图中的实线表示可以通过组合形成日志解决方案, 包括:jcl+juc
, jcl+log4j
, slf4j+logback
; 虚线表示可以借助对应的依赖包实现jul或log4j与slf4j的兼容.
2.facade层
章节1.2中图片可以表示为下图所示:
facade层用于定义接口规范,使得开发人员可以使用统一的API进行操作,而不用分别学习log4j、jul、logback以及新的日志框架对应的API等;其中:facede层包括jcl和slf4j, impl层有log4j、jul、logback, 可以通过组合、桥接、适配实现6种组合方案。
2.1 jcl
jcl是Apache为java日志框架推出的门面API,全称为"Jakarta Commons Logging",因为jcl属于Apache且代码位于org.apache.commons.logging包路径下,所以也可以称为"Apache Commons Logging".
pom依赖:
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
使用方式:
// 依赖apache.commons.logging包
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
// 获取日志对象,注意是Log类型
private static final Log LOG = LogFactory.getLog(所在类类名.class);
// 打印日志API
LOG.trace("字符串类型");
LOG.debug("test debug");
LOG.info("test info");
LOG.warn("test warn");
LOG.error("test error");
LOG.fatal("test fatal");
}
}
jcl提供了6种日志级别,从低到高分别为:trace, debug, info, warn, error, fatal.
需要注意:Apache在1.2版本后已经停止了维护(2014),因此市面占用量较低。需要注意, 如果未给jcl指定impl层, 会默认使用jul.
2.2 slf4j
slf4j是qos为java日志框架推出的门面API,全称为“simple logging facade for java”, 是目前比较流行的日志框架.
pom依赖:
<dependency>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
<version>1.7.25</version>
</dependency>
使用方式:
// 依赖org.slf4j包
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// 获取日志对象,注意是Logger类型
private Logger LOGGER = LoggerFactory.getLogger(slf4jLogbackTest.class);
// 打印日志API
LOGGER.trace("test trace");
LOGGER.debug("test debug");
LOGGER.info("test info");
LOGGER.warn("test warn");
LOGGER.error("test error");
slf4j提供5种日志级别,从低到高分别为:trace, debug, info, warn, error. 相比jcl少了fatal级别。
需要注意: 如果项目仅依赖slf4j而未指定impl层,启动时会因加载不到StaticLoggerBinder类而报错(该类在logback-classic中定义):
3.实现层
日志框架中真正起作用的是实现层,包括: juc、log4j、logback.
3.1 juc
juc是jdk提供的日志框架,因此不需要引入任何依赖;
package com.seong.test;
import org.junit.Test;
import java.util.logging.Logger;
public class JucTest {
private static final Logger LOGGER = Logger.getLogger(JucTest.class.getName());
@Test
public void test() {
LOGGER.finest("log finest");
LOGGER.finer("log finer");
LOGGER.fine("log fine");
LOGGER.config("log config");
LOGGER.info("log info");
LOGGER.warning("log warning");
LOGGER.severe("log severe");
}
}
结果如下所示:
日志分层打印策略使得info级别以下日志未被打印(可在${JAVA_HOME}/jre/lib/logging.properties
中修改);另外由于jul通过借助Sytem.err进行打印,因此显示红色字体。
3.2 log4j
引入log4j依赖包:
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
其中:版本1.2.17已是最新版本, 在2012后已停止维护.
添加配置文件log4j.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration>
<appender name="log4jConsole" class="org.apache.log4j.ConsoleAppender">
...
</appender>
...
<root>
<priority value ="info"/>
<appender-ref ref="log4jConsole"/>
...
</root>
</log4j:configuration>
未配置log4j.xml时, 打印日志会报错:
Java代码:
package com.seong;
import org.apache.log4j.Logger;
import org.junit.Test;
public class Log4jSingleTest {
Logger LOGGER = Logger.getLogger(Log4jSingleTest.class);
@Test
public void test() {
LOGGER.trace("test trace");
LOGGER.debug("test debug");
LOGGER.info("test info");
LOGGER.warn("test warn");
LOGGER.error("test error");
LOGGER.fatal("test fatal");
}
}
注意:这里单独使用log4j, 使用的Logger为org.apache.log4j包中的类.
3.3 logback
Logback包括以下3个部分:
[1] logback-core (基础核心, 被其他模块应用)
[2] logback-classic (完整实现SLF4J API接口)
[3] logback-access (提供通过Http访问日志的功能)
本章节中使用logback提供的日志记录能力, 而不需要提供http访问日志的能力, 因此不涉及logback-acess依赖;
引入依赖包:
<dependency>
<artifactId>logback-classic</artifactId>
<groupId>ch.qos.logback</groupId>
<version>1.2.3</version>
</dependency>
这里引入的ogback-classic
依赖同时包含了ogback-core
和slf4j-api
:
可以看出logback包中已经依赖了slf4j, 即logback默认设置为与slf4j组合使用.
4.组合方案
4.1 log4j 与 jcl
引入依赖包:
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration>
<appender name="log4jConsole" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="[%d{dd HH:mm:ss,SSS\} %-5p] [%t] %c{2\} - %m%n" />
</layout>
<filter class="org.apache.log4j.varia.LevelRangeFilter">
<param name="levelMin" value="debug" />
<param name="levelMax" value="warn" />
<param name="AcceptOnMatch" value="true" />
</filter>
</appender>
<appender name="log4jFile" class="org.apache.log4j.RollingFileAppender">
<param name="File" value="/Users/seong/Documents/work/code/blog/logdemo/log/temp/log4jFile.log" />
<!-- 设置是否在重新启动服务时,在原有日志的基础添加新日志 -->
<param name="Append" value="true" />
<param name="MaxBackupIndex" value="10" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="[%d{dd HH:mm:ss,SSS\} %-5p] [%t] %c{2\} - %m%n" />
</layout>
</appender>
<appender name="log4jDaily" class="org.apache.log4j.DailyRollingFileAppender">
<param name="File" value="/Users/seong/Documents/work/code/blog/logdemo/log/temp/log4jDaily.log" />
<param name="DatePattern" value="'.'yyyy-MM-dd'.log'" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern"
value="[%d{MMdd HH:mm:ss SSS\} %-5p] [%t] %c{3\} - %m%n" />
</layout>
</appender>
<root>
<priority value ="info"/>
<appender-ref ref="log4jConsole"/>
<appender-ref ref="log4jFile"/>
<appender-ref ref="log4jDaily"/>
</root>
</log4j:configuration>
Java代码:
package com.seong;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Test;
public class JucLog4jTest {
Log log = LogFactory.getLog(JucLog4jTest.class);
@Test
public void test() {
log.trace("test trace");
log.debug("test debug");
log.info("test info");
log.warn("test warn");
log.error("test error");
log.fatal("test fatal");
}
}
4.2 logback和slf4j
引入依赖包:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
添加logback.xml配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<configuration scanPeriod="2 seconds" debug="true">
<!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
<property name="LOG_HOME" value="/Users/seong/Documents/work/code/blog/logdemo/log/temp" />
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<appender name="logbackFile" class="ch.qos.logback.core.FileAppender">
<file>${LOG_HOME}/logbackFile.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<appender name="logbackRollFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/logbackRollFile.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_HOME}/logbackRollFile-%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>1kb</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
</appender>
<logger name="com.seong.test.LogbackTest" level="warn" />
<!-- 日志输出级别 -->
<root level="DEBUG">
<appender-ref ref="STDOUT" />
<appender-ref ref="logbackRollFile" />
<appender-ref ref="logbackFile" />
</root>
</configuration>
注意: 没有配置文件时, 使用logback默认的日志风格.
Java代码:
package com.seong;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class slf4jLogbackTest {
private Logger LOGGER = LoggerFactory.getLogger(slf4jLogbackTest.class);
@Test
public void test() {
LOGGER.trace("test trace");
LOGGER.debug("test debug");
LOGGER.info("test info");
LOGGER.warn("test warn");
LOGGER.error("test error");
}
}
5.适配模式
slf4j除了可以作为logback的门面, 还可以作为jcl, jul, log4j的门面; 此时需要借助适配包实现,如下图所示:
以下以slf4j+log4j
为例进行介绍.
pom依赖:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
slf4j-log4j12
中包含了log4j
和slf4j-api
, 因此不用再引入其他依赖, 如下图所示:
此时, 按章节4中配置文件配置好log4j后, 客户端即可以使用slf4j的API记录日志.
6.桥接模式
当依赖的第三方jar包使用不同的日志框架时, 需要通过桥接来实现兼容, 否则需要独立维护两套配置; 桥接的实现需要借助对应的依赖,如下图所示:
案例介绍:
项目使用主流的slf4j+logback
作为日志框架, 依赖了一个使用log4j为日志框架的第三方jar包。
为保障程序正常运行, 项目需要维护两套日志规范, 即为logback和log4j各维护一份配置文件。 基于减少重复工作并提高项目的可维护性的角度, 希望logback和log4j共用一份配置文件, 即将log4j的日志打印任务桥接给logback.
第三方maven坐标:
<groupId>org.example</groupId>
<artifactId>jcl-log4j</artifactId>
<version>1.0-SNAPSHOT</version>
第三方jar包的pom依赖:
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
项目引用第三方:
<dependency>
<groupId>org.example</groupId>
<artifactId>jcl-log4j</artifactId>
<version>1.2</version>
<exclusions>
<exclusion>
<artifactId>log4j</artifactId>
<groupId>log4j</groupId>
</exclusion>
</exclusions>
</dependency>
分析:
log4j-over-slf4j
依赖中定义了与log4j-log4j
完全相同的包、类、方法, 用于顶替log4j, 同时将来自门面的日志打印请求转发到logback上实现桥接; 因此, 使用时需要排除第三方包中的log4j依赖.