logback 用于日志记录,能够将日志输出到控制台、文件、数据库和邮件等,相比其它全部的日志系统,logback 更快而且更小,包含了许多独特而且有用的特性。html
logback 被分红三个不一样的模块:logback-core,logback-classic,logback-access。java
本文将介绍如下内容,因为篇幅较长,可根据须要选择阅读:mysql
如何使用 logback:将日志输出到控制台、文件和数据库,以及使用 JMX 配置 logback;git
logback 配置文件详解;github
logback 的源码分析。sql
JDK:1.8.0_231
maven:3.6.1
IDE:Spring Tool Suite 4.3.2.RELEASE
mysql:5.7.28数据库
Logger
实例,并打印指定等级的日志;项目类型 Maven Project ,打包方式 jar。express
logack 自然的支持 slf4j,不须要像其余日志框架同样引入适配层(如 log4j 需引入 slf4j-log4j12 )。经过后面的源码分析可知,logback 只是将适配相关代码放入了 logback-classic。windows
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- logback+slf4j --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.28</version> <type>jar</type> <scope>compile</scope> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>1.2.3</version> <type>jar</type> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> <type>jar</type> </dependency> <!-- 输出日志到数据库时须要用到 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.17</version> </dependency> <!-- 使用数据源方式输出日志到数据库时须要用到 --> <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.4</version> </dependency> </dependencies>
配置文件放在 resources 下,文件名能够为 logback-test.xml 或 logback.xml,实际项目中能够考虑在测试环境中使用 logback-test.xml ,在生产环境中使用 logback.xml( 固然 logback 还支持使用 groovy 文件或 SPI 机制进行配置,本文暂不涉及)。api
在 logback中,logger 能够当作为咱们输出日志的对象,而这个对象打印日志时必须遵循 appender 中定义的输出格式和输出目的地等。注意,root logger 是一个特殊的 logger。
<configuration> <!-- 控制台输出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <!--定义控制台输出格式--> <encoder charset="utf-8"> <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> </appender> <root level="info"> <appender-ref ref="STDOUT" /> </root> </configuration>
另外,即便咱们没有配置,logback 也会默认产生一个 root logger ,并为它配置一个 ConsoleAppender
。
为了程序的解耦,通常咱们在使用日志时会采用门面模式,即经过 slf4j 或 commons-logging 来获取 Logger
对象。
如下代码中,导入的两个类 Logger
、 LoggerFactory
都定义在 slf4j-api 中,彻底不会涉及到 logback 包的类。这时,若是咱们想切换 log4j 做为日志支持,只要修改 pom.xml 和日志配置文件就行,项目代码并不须要改动。源码分析部分将分析 slf4j 如何实现门面模式。
@Test public void test01() { Logger logger = LoggerFactory.getLogger(LogbackTest.class); logger.debug("输出DEBUG级别日志"); logger.info("输出INFO级别日志"); logger.warn("输出WARN级别日志"); logger.error("输出ERROR级别日志"); }
注意,这里获取的 logger 不是咱们配置的 root logger,而是以 cn.zzs.logback.LogbackTest 命名的 logger,它继承了祖先 root logger 的配置。
运行测试方法,能够看到在控制台打印以下信息:
2020-01-16 09:10:40 [main] INFO ROOT - 输出INFO级别的日志 2020-01-16 09:10:40 [main] WARN ROOT - 输出WARN级别的日志 2020-01-16 09:10:40 [main] ERROR ROOT - 输出ERROR级别的日志
这时咱们会发现,怎么没有 debug 级别的日志?由于咱们配置了日志等级为 info,小于 info 等级的日志不会被打印出来。日志等级以下:
ALL < TRACE < DEBUG < INFO < WARN < ERROR < OFF
本例子将在以上例子基础上修改。测试方法代码不须要修改,只要修改配置文件就能够了。
前面已经讲过,appender 中定义日志的输出格式和输出目的地等,因此,要将日志输出到滚动文件,只要修改appender 就行。logback 提供了RollingFileAppender
来支持打印日志到滚动文件。
如下配置中,设置了文件大小超过100M后会按指定命名格式生成新的日志文件。
<configuration> <!-- 定义变量 --> <property name="LOG_HOME" value="D:/growUp/test/log" /> <property name="APP_NAME" value="logback-demo"/> <!-- 滚动文件输出 --> <appender name="FILE-ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!-- 指定日志文件的名称 --> <file>${LOG_HOME}/${APP_NAME}/error.log</file> <!-- 配置追加写入 --> <append>true</append> <!-- 级别过滤器 --> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> <!-- 滚动策略 --> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <!-- 滚动文件名称 --> <fileNamePattern>${LOG_HOME}/${APP_NAME}/notError-%d{yyyy-MM-dd}-%i.log</fileNamePattern> <!-- 可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。 注意,删除旧文件时, 那些为了归档而建立的目录也会被删除。 --> <MaxHistory>50</MaxHistory> <!-- 当日志文件超过maxFileSize指定的大小时,根据上面提到的%i进行日志文件滚动 --> <maxFileSize>100MB</maxFileSize> <!-- 设置文件总大小 --> <totalSizeCap>20GB</totalSizeCap> </rollingPolicy> <!-- 日志输出格式--> <encoder charset="utf-8"> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> </appender> <root level="info"> <appender-ref ref="FILE" /> </root> </configuration>
运行测试方法,咱们能够在指定目录看到生成的日志文件。
查看日志文件,能够看到只打印了 error 等级的日志:
logback 提供了DBAppender
来支持将日志输出到数据库中。
logback 为咱们提供了三张表用于记录日志, 在使用DBAppender
以前,这三张表必须存在。
这三张表分别为:logging_event, logging_event_property 与 logging_event_exception。logback 自带 SQL 脚原本建立表,这些脚本在 logback-classic/src/main/java/ch/qos/logback/classic/db/script 文件夹下,相关脚本也能够再本项目的 resources/script 找到。
因为本文使用的是 mysql 数据库,执行如下脚本(注意,官方给的 sql 中部分字段设置了NOT NULL 的约束,可能存在插入报错的状况,能够考虑调整):
BEGIN; DROP TABLE IF EXISTS logging_event_property; DROP TABLE IF EXISTS logging_event_exception; DROP TABLE IF EXISTS logging_event; COMMIT; BEGIN; CREATE TABLE logging_event ( timestmp BIGINT NOT NULL, formatted_message TEXT NOT NULL, logger_name VARCHAR(254) NOT NULL, level_string VARCHAR(254) NOT NULL, thread_name VARCHAR(254), reference_flag SMALLINT, arg0 VARCHAR(254), arg1 VARCHAR(254), arg2 VARCHAR(254), arg3 VARCHAR(254), caller_filename VARCHAR(254), caller_class VARCHAR(254) NOT NULL, caller_method VARCHAR(254) NOT NULL, caller_line CHAR(4) NOT NULL, event_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY ); COMMIT; BEGIN; CREATE TABLE logging_event_property ( event_id BIGINT NOT NULL, mapped_key VARCHAR(254) NOT NULL, mapped_value TEXT, PRIMARY KEY(event_id, mapped_key), FOREIGN KEY (event_id) REFERENCES logging_event(event_id) ); COMMIT; BEGIN; CREATE TABLE logging_event_exception ( event_id BIGINT NOT NULL, i SMALLINT NOT NULL, trace_line VARCHAR(254) NOT NULL, PRIMARY KEY(event_id, i), FOREIGN KEY (event_id) REFERENCES logging_event(event_id) ); COMMIT;
能够看到生成了三个表:
logback 支持使用 DataSourceConnectionSource,DriverManagerConnectionSource 与 JNDIConnectionSource 三种方式配置数据源 。本文选择第一种,并使用以 c3p0 做为数据源(第二种方式文中也会给出)。
这里须要说明下,由于实例化 c3p0 的数据源对象ComboPooledDataSource
时,会去自动加载 classpath 下名为 c3p0-config.xml 的配置文件,因此,咱们不须要再去指定 dataSource 节点下的参数,若是是 druid 或 dbcp 等则须要指定。
<configuration> <!--数据库输出--> <appender name="DB" class="ch.qos.logback.classic.db.DBAppender"> <!-- 使用jdbc方式 --> <!-- <connectionSource class="ch.qos.logback.core.db.DriverManagerConnectionSource"> <driverClass>com.mysql.cj.jdbc.Driver</driverClass> <url>jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true</url> <user>root</user> <password>root</password> </connectionSource> --> <!-- 使用数据源方式 --> <connectionSource class="ch.qos.logback.core.db.DataSourceConnectionSource"> <dataSource class="com.mchange.v2.c3p0.ComboPooledDataSource"> </dataSource> </connectionSource> </appender> <root level="info"> <appender-ref ref="DB" /> </root> </configuration>
运行测试方法,能够看到数据库中插入了如下数据:
logback 支持使用 JMX 动态地更新配置。开启 JMX 很是简单,只须要增长 jmxConfigurator 节点就能够了,以下:
<configuration scan="true" scanPeriod="10 seconds" debug="true"> <!-- 定义变量 --> <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/> <!-- 开启JMX支持 --> <jmxConfigurator /> <!-- 控制台输出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <target>system.err</target> <encoder charset="utf-8"> <pattern>${LOG_PATTERN}</pattern> </encoder> </appender> <root level="info"> <appender-ref ref="STDOUT" /> </root> </configuration>
在咱们经过 jconsole 链接到服务器上以后(jconsole 在 JDK 安装目录的 bin 目录下),在 MBeans 面板上,在 "ch.qos.logback.classic.jmx.Configurator" 文件夹下你能够看到几个选项。以下图所示:
咱们能够看到,在属性中,咱们能够查看 logback 已经产生的 logger 和 logback 的内部状态,经过操做,咱们能够:
更多 JMX 相关内容可参考个人另外一篇博客:如何使用JMX来管理程序?
实际项目中,有时咱们须要对打印的内容进行必定处理,以下:
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
这种状况会产生构建消息参数的成本,为了不以上损耗,能够修改以下:
if(logger.isDebugEnabled()) { logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i])); }
当咱们打印的是一个对象时,也能够采用如下方法来优化:
// 不推荐 logger.debug("The new entry is " + entry + "."); // 推荐 logger.debug("The new entry is {}", entry);
前面已经说过, logback 配置文件名能够为 logback-test.xml 、 logback.groovy 或 logback.xml ,除了采用配置文件方式, logback 也支持使用 SPI 机制加载 ch.qos.logback.classic.spi.Configurator 的实现类来进行配置。如下讲解仅针对 xml 格式文件的配置方式展开。
另外,若是想要自定义配置文件的名字,能够经过系统属性指定:
-Dlogback.configurationFile=/path/to/config.xml
若是没有加载到配置,logback 会调用 BasicConfigurator 进行默认的配置。
configuration 是 logback.xml 或 logback-test.xml 文件的根节点。
configuration 主要用于配置某些全局的日志行为,常见的配置参数以下:
属性名 | 描述 |
---|---|
debug | 是否打印 logback 的内部状态,开启有利于排查 logback 的异常。默认 false |
scan | 是否在运行时扫描配置文件是否更新,若是更新时则从新解析并更新配置。若是更改后的配置文件有语法错误,则会回退到以前的配置文件。默认 false |
scanPeriod | 多久扫描一次配置文件是否修改,单位能够是毫秒、秒、分钟或者小时。默认状况下,一分钟扫描一次配置文件。 |
配置方式以下:
<configuration debug="true" scan="true" scanPeriod="60 seconds" > <!-- 控制台输出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <target>system.err</target> <encoder charset="utf-8"> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> </appender> <root level="info"> <appender-ref ref="STDOUT" /> </root> </configuration>
使用以上配置进行测试:
如上图,经过控制台咱们能够查看 logback 加载配置的过程,这时,咱们尝试修改 logback 配置文件的内容:
观察控制台,能够看到配置文件从新加载:
前面提到过,logger 是为咱们打印日志的对象,这个概念很是重要,有助于更好地理解 logger 的继承关系。
在如下代码中,咱们能够在getLogger
方法中传入的是当前类的 Class 对象或全限定类名,本质上获取到的都是一个 logger 对象(若是该 logger 不存在,才会建立)。
@Test public void test01() { Logger logger1 = LoggerFactory.getLogger(LogbackTest.class); Logger logger2 = LoggerFactory.getLogger("cn.zzs.logback.LogbackTest"); System.err.println(logger == logger2);// true }
这里补充一个问题,该 logger 对象以 cn.zzs.logback.LogbackTest 命名,和咱们配置文件中定义的 root logger 并非同一个,可是为何这个 logger 对象却拥有 root logger 的行为?
这要得益于 logger 的继承关系,以下图:
若是咱们未指定当前 logger 的日志等级,logback 会将其日志等级设置为最近父级的日志等级。另外,默认状况下,当前 logger 也会继承最近父级持有的 appender。
下面测试下以上特性,将配置文件进行以下修改:
<?xml version="1.0" encoding="UTF-8"?> <configuration scan="true" scanPeriod="10 seconds" debug="true"> <!-- 定义变量 --> <property scope="system" name="LOG_HOME" value="D:/growUp/test/logs" /> <property scope="system" name="APP_NAME" value="logback-demo"/> <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/> <!-- 控制台输出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <target>system.err</target> <encoder charset="utf-8"> <pattern>${LOG_PATTERN}</pattern> </encoder> </appender> <timestamp key="bySecond" datePattern="yyyy-MM-dd'T'HH-mm-ss" /> <!-- 文件输出 --> <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <append>true</append> <file>${LOG_HOME}/${APP_NAME}/file-${bySecond}.log</file> <immediateFlush>true</immediateFlush> <!-- 是否启用安全写入 --> <prudent>false</prudent> <encoder> <pattern>${LOG_PATTERN}</pattern> </encoder> </appender> <logger name="cn.zzs" level="error"> <appender-ref ref="FILE" /> </logger> <root level="info"> <appender-ref ref="STDOUT" /> </root> </configuration>
这里自定义了一个 logger,日志等级是 error,appender 为文件输出。运行测试方法:
能够看到,名为 cn.zzs.logback.LogbackTest 的 logger 继承了名为 cn.zzs 的 logger 的日志等级和 appender,以及继承了 root logger 的 appender。
实际项目中,若是不但愿继承父级的 appender,能够配置 additivity="false" ,以下:
<logger name="cn.zzs" additivity="false"> <appender-ref ref="FILE" /> </logger>
注意,由于如下配置都是创建在 logger 的继承关系上,因此这部份内容必须很好地理解。
appender 用于定义日志的输出目的地和输出格式,被 logger 所持有。logback 为咱们提供了如下几种经常使用的appender:
类名 | 描述 |
---|---|
ConsoleAppender | 将日志经过 System.out 或者 System.err 来进行输出,即输出到控制台。 |
FileAppender | 将日志输出到文件中。 |
RollingFileAppender | 继承自 FileAppender,也是将日志输出到文件,但文件具备轮转功能。 |
DBAppender | 将日志输出到数据库 |
SocketAppender | 将日志以明文方式输出到远程机器 |
SSLSocketAppender | 将日志以加密方式输出到远程机器 |
SMTPAppender | 将日志输出到邮件 |
本文仅会讲解前四种,后四种可参考官方文档。
ConsoleAppender 支持将日志经过 System.out 或者 System.err 输出,即输出到控制台,经常使用属性以下:
属性名 | 类型 | 描述 |
---|---|---|
encoder | Encoder | 后面单独讲 |
target | String | System.out 或 System.err。默认为 System.out |
immediateFlush | boolean | 是否当即刷新。默认为 true。 |
withJansi | boolean | 是否激活 Jansi 在 windows 使用 ANSI 彩色代码,默认值为 false。 在windows电脑上我尝试开启这个属性并引入 jansi 包,但总是报错,暂时没有解决方案。 |
具体配置以下:
<!-- 控制台输出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <target>system.err</target> <encoder charset="utf-8"> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> </appender>
FileAppender 支持将日志输出到文件中,经常使用属性以下:
属性名 | 类型 | 描述 |
---|---|---|
append | boolean | 是否追加写入。默认为 true |
encoder | Encoder | 后面单独讲 |
immediateFlush | boolean | 是否当即刷新。默认为 true。 |
file | String | 要写入文件的路径。若是文件不存在,则新建。 |
prudent | boolean | 是否采用安全方式写入,即便在不一样的 JVM 或者不一样的主机上运行 FileAppender 实例。默认的值为 false。 |
具体配置以下:
<!-- 定义变量 --> <property scope="system" name="LOG_HOME" value="D:/growUp/test/logs" /> <property scope="system" name="APP_NAME" value="logback-demo"/> <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/> <timestamp key="bySecond" datePattern="yyyy-MM-dd'T'HH-mm-ss" /> <!-- 文件输出 --> <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <file>${LOG_HOME}/${APP_NAME}/file-${bySecond}.log</file> <encoder> <pattern>${LOG_PATTERN}</pattern> </encoder> </appender>
RollingFileAppender 继承自 FileAppender,也是将日志输出到文件,但文件具备轮转功能。
RollingFileAppender 的属性以下所示:
属性名 | 类型 | 描述 |
---|---|---|
file | String | 要写入文件的路径。若是文件不存在,则新建。 |
append | boolean | 是否追加写入。默认为 true。 |
immediateFlush | boolean | 是否当即刷新。默认为true。 |
encoder | Encoder | 后面单独将 |
rollingPolicy | RollingPolicy | 定义文件如何轮转。 |
triggeringPolicy | TriggeringPolicy | 定义何时发生轮转行为。若是 rollingPolicy 使用的类已经实现了 triggeringPolicy 接口,则不须要再配置 triggeringPolicy,例如 SizeAndTimeBasedRollingPolicy。 |
prudent | boolean | 是否采用安全方式写入,即便在不一样的 JVM 或者不一样的主机上运行 FileAppender 实例。默认的值为 false。 |
具体配置以下:
<!-- 定义变量 --> <property scope="system" name="LOG_HOME" value="D:/growUp/test/logs" /> <property scope="system" name="APP_NAME" value="logback-demo"/> <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/> <!-- 轮转文件输出 --> <appender name="FILE-ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!-- 轮转策略,它根据时间和文件大小来制定轮转策略 --> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <!-- 按天轮转 --> <fileNamePattern>${LOG_HOME}/${APP_NAME}/log-%d{yyyy-MM-dd}-%i.log</fileNamePattern> <!-- 保存 30 天的历史记录,最大大小为 30GB --> <MaxHistory>30</MaxHistory> <totalSizeCap>30GB</totalSizeCap> <!-- 当日志文件超过100MB的大小时,根据上面提到的%i进行日志文件轮转 --> <maxFileSize>100MB</maxFileSize> </rollingPolicy> <!-- 日志输出格式--> <encoder charset="utf-8"> <pattern>${LOG_PATTERN}</pattern> </encoder> </appender>
参见使用例子。
encoder 负责将日志事件按照配置的格式转换为字节数组,经常使用属性以下:
属性名 | 类型 | 描述 |
---|---|---|
pattern | String | 日志打印格式。 |
outputPatternAsHeader | boolean | 是否将 pattern 字符串插入到日志文件顶部。默认false。 |
针对 pattern 属性,这里补充下它的经常使用转换字符:
转换字符 | 描述 |
---|---|
c{length} lo{length} logger{length} |
输出 logger 的名字。能够经过 length 缩短其长度。 可是,logger 名字最右边永远都会存在。 例如,当咱们设置 logger{0}时,cn.zzs.logback.LogbackTest 中的 LogbackTest 永远不会被删除 |
C{length} class{length} |
输出发出日志请求的类的全限定名称。 能够经过 length 缩短其长度。 |
d{pattern} date{pattern} d{pattern, timezone} date{pattern, timezone} |
输出日志事件的日期。 能够经过 pattern 设置日期格式,timezone 设置时区。 |
m / msg / message | 输出与日志事件相关联的,由应用程序提供的日志信息。 |
M / method | 输出发出日志请求的方法名。 |
p / le / level | 输出日志事件的级别。 |
t / thread | 输出生成日志事件的线程名。 |
n | 输出平台所依赖的行分割字符。 |
F / file | 输出发出日志请求的 Java 源文件名。 |
caller{depth} caller{depthStart..depthEnd} caller{depth, evaluator-1, ... evaluator-n} caller{depthStart..depthEnd, evaluator-1, ... evaluator-n} |
输出生成日志的调用者所在的位置信息。 |
L / line | 输出发出日志请求所在的行号。 |
property{key} | 输出属性 key 所对应的值。 |
注意,在拼接 pattren 时,应该考虑使用“有意义的”转换字符,避免产生没必要要的性能开销。具体配置以下:
<!-- 控制台输出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder charset="utf-8"> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> <outputPatternAsHeader>true</outputPatternAsHeader> </encoder> </appender>
其中, 转换说明符 %-5level 表示日志事件的级别的字符应该向左对齐,保持五个字符的宽度。
appender 除了定义日志的输出目的地和输出格式,其实也能够对日志事件进行过滤输出,例如,仅输出包含指定字符的日志。而这个功能需配置 filter。
LevelFilter 基于级别来过滤日志事件。修改配置文件以下:
<configuration scan="true" scanPeriod="10 seconds" debug="true"> <!-- 定义变量 --> <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/> <!-- 控制台输出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <target>system.err</target> <encoder charset="utf-8"> <pattern>${LOG_PATTERN}</pattern> </encoder> <!-- 设置过滤器 --> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <root level="info"> <appender-ref ref="STDOUT" /> </root> </configuration>
运行测试方法,可见,虽然 root logger 的日志等级是 info,但最终只会打印 error 的日志:
ThresholdFilter 基于给定的临界值来过滤事件。若是事件的级别等于或高于给定的临界,则过滤经过,不然会被拦截。配置以下:
<configuration scan="true" scanPeriod="10 seconds" debug="true"> <!-- 定义变量 --> <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/> <!-- 控制台输出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <target>system.err</target> <encoder charset="utf-8"> <pattern>${LOG_PATTERN}</pattern> </encoder> <!-- 设置过滤器 --> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>ERROR</level> </filter> </appender> <root level="info"> <appender-ref ref="STDOUT" /> </root> </configuration>
运行测试方法,可见,虽然 root logger 的日志等级是 info,但最终只会打印 error 的日志:
EvaluatorFilter 基于给定的标准来过滤事件。 它采用 Groovy 表达式做为评估的标准。配置以下:
<configuration scan="true" scanPeriod="10 seconds" debug="true"> <!-- 定义变量 --> <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/> <!-- 控制台输出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <target>system.err</target> <encoder charset="utf-8"> <pattern>${LOG_PATTERN}</pattern> </encoder> <!-- 设置过滤器 --> <filter class="ch.qos.logback.core.filter.EvaluatorFilter"> <evaluator class="ch.qos.logback.classic.boolex.GEventEvaluator"> <expression> e.level.toInt() >= ERROR.toInt() && !(e.mdc?.get("req.userAgent") =~ /Googlebot|msnbot|Yahoo/ ) </expression> </evaluator> <OnMismatch>DENY</OnMismatch> <OnMatch>NEUTRAL</OnMatch> </filter> </appender> <root level="info"> <appender-ref ref="STDOUT" /> </root> </configuration>
上面的过滤器引用自官网,规则为:让级别在 ERROR 及以上的日志事件在控制台显示,除非是因为来自 Google,MSN,Yahoo 的网络爬虫致使的错误。
注意,使用 GEventEvaluator 必须引入 groovy 的 jar 包:
<!-- groovy --> <dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy</artifactId> <version>3.0.0-rc-3</version> </dependency>
运行测试方法,输出以下结果:
EvaluatorFilter 除了支持 Groovy 表达式,还支持使用 java 代码来做为过滤标准,修改配置文件以下:
<configuration scan="true" scanPeriod="10 seconds" debug="true"> <!-- 定义变量 --> <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/> <!-- 控制台输出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <target>system.err</target> <encoder charset="utf-8"> <pattern>${LOG_PATTERN}</pattern> </encoder> <!-- 设置过滤器 --> <filter class="ch.qos.logback.core.filter.EvaluatorFilter"> <evaluator> <!-- defaults to type ch.qos.logback.classic.boolex.JaninoEventEvaluator --> <expression>return message.contains("ERROR");</expression> </evaluator> <OnMismatch>DENY</OnMismatch> <OnMatch>NEUTRAL</OnMatch> </filter> </appender> <root level="info"> <appender-ref ref="STDOUT" /> </root> </configuration>
注意,使用 JaninoEventEvaluator 必须导入 janino 包,以下:
<!-- janino --> <dependency> <groupId>org.codehaus.janino</groupId> <artifactId>janino</artifactId> <version>3.1.0</version> </dependency>
运行测试方法,输出以下结果:
logback 很是庞大、复杂,若是要将 logback 全部模块分析完,估计要花至关长的时间,因此,本文仍是和之前同样,仅针对核心代码进行分析,当分析的方法存在多个实现时,也只会挑选其中一个进行讲解。文中没有涉及到的部分,感兴趣的能够自行研究。
接下来经过解决如下几个问题来逐步分析 logback 的源码:
slf4j 使用的是门面模式,无论使用什么日志实现,项目代码都只会用到 slf4j-api 中的接口,而不会使用到具体的日志实现的代码。slf4j 究竟是如何实现门面模式的?接下来进行源码分析:
在咱们的应用中,通常会经过如下方式获取 Logger 对象,咱们就从这个方法开始分析吧:
Logger logger = LoggerFactory.getLogger(LogbackTest.class);
进入到 LoggerFactory.getLogger(Class<?> clazz)
方法,以下。在调用这个方法时,咱们通常会以当前类的 Class 对象做为入参。固然,logback 也容许你使用其余类的 Class 对象做为入参,可是,这样作可能不利于对 logger 的管理。经过设置系统属性-Dslf4j.detectLoggerNameMismatch=true
,当实际开发中出现该类问题,会在控制台打印提醒信息。
public static Logger getLogger(Class<?> clazz) { // 获取Logger对象,后面继续展开 Logger logger = getLogger(clazz.getName()); // 若是系统属性-Dslf4j.detectLoggerNameMismatch=true,则会检查传入的logger name是否是CallingClass的全限定类名,若是不匹配,会在控制台打印提醒 if (DETECT_LOGGER_NAME_MISMATCH) { Class<?> autoComputedCallingClass = Util.getCallingClass(); if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) { Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(), autoComputedCallingClass.getName())); Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation"); } } return logger; }
进入到LoggerFactory.getLogger(String name)
方法,以下。在这个方法中,不一样的日志实现会返回不一样的ILoggerFactory实现类:
public static Logger getLogger(String name) { // 获取工厂对象,后面继续展开 ILoggerFactory iLoggerFactory = getILoggerFactory(); // 利用工厂对象获取Logger对象 return iLoggerFactory.getLogger(name); }
进入到getILoggerFactory()
方法,以下。INITIALIZATION_STATE
表明了初始化状态,该方法会根据初始化状态的不一样而返回不一样的结果。
static final SubstituteLoggerFactory SUBST_FACTORY = new SubstituteLoggerFactory(); static final NOPLoggerFactory NOP_FALLBACK_FACTORY = new NOPLoggerFactory(); public static ILoggerFactory getILoggerFactory() { // 若是未初始化 if (INITIALIZATION_STATE == UNINITIALIZED) { synchronized (LoggerFactory.class) { if (INITIALIZATION_STATE == UNINITIALIZED) { // 修改状态为正在初始化 INITIALIZATION_STATE = ONGOING_INITIALIZATION; // 执行初始化 performInitialization(); } } } switch (INITIALIZATION_STATE) { // 若是StaticLoggerBinder类存在,则经过StaticLoggerBinder获取ILoggerFactory的实现类 case SUCCESSFUL_INITIALIZATION: return StaticLoggerBinder.getSingleton().getLoggerFactory(); // 若是StaticLoggerBinder类不存在,则返回NOPLoggerFactory对象 // 经过NOPLoggerFactory获取到的NOPLogger没什么用,它的方法几乎都是空实现 case NOP_FALLBACK_INITIALIZATION: return NOP_FALLBACK_FACTORY; // 若是初始化失败,则抛出异常 case FAILED_INITIALIZATION: throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG); // 若是正在初始化,则SubstituteLoggerFactory对象,这个对象不做扩展 case ONGOING_INITIALIZATION: return SUBST_FACTORY; } throw new IllegalStateException("Unreachable code"); }
以上方法须要重点关注 StaticLoggerBinder
这个类,它并不在 slf4j-api 中,而是在 logback-classic 中,以下图所示。其实分析到这里应该能够理解:slf4j 经过 StaticLoggerBinder 类与具体日志实现进行关联,从而实现门面模式。
接下来再简单看下LoggerFactory.performInitialization()
,以下。这里会执行初始化,所谓的初始化就是查找 StaticLoggerBinder 这个类是否是存在,若是存在会将该类绑定到当前应用,同时,根据不一样状况修改INITIALIZATION_STATE
。代码比较多,我归纳下执行的步骤:
private final static void performInitialization() { // 查找StaticLoggerBinder这个类是否是存在,若是存在会将该类绑定到当前应用 bind(); // 若是检测存在 if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) { // 判断StaticLoggerBinder与当前使用的slf4j是否适配 versionSanityCheck(); } } private final static void bind() { try { // 使用类加载器在classpath下查找StaticLoggerBinder类。若是存在多个StaticLoggerBinder类,这时会在控制台提醒并列出全部路径(例如同时引入了logback和slf4j-log4j12 的包,就会出现两个StaticLoggerBinder类) Set<URL> staticLoggerBinderPathSet = null; if (!isAndroid()) { staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet(); reportMultipleBindingAmbiguity(staticLoggerBinderPathSet); } // 这一步只是简单调用方法,可是很是重要。 // 能够检测StaticLoggerBinder类和它的getSingleton方法是否存在,若是不存在,分别会抛出 NoClassDefFoundError错误和NoSuchMethodError错误 // 注意,当存在多个StaticLoggerBinder时,应用不会中止,由JVM随机选择一个。 StaticLoggerBinder.getSingleton(); // 修改状态为初始化成功 INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION; // 若是存在多个StaticLoggerBinder,会在控制台提醒用户实际选择的是哪个 reportActualBinding(staticLoggerBinderPathSet); // 对SubstituteLoggerFactory的操做,不做扩展 fixSubstituteLoggers(); replayEvents(); SUBST_FACTORY.clear(); } catch (NoClassDefFoundError ncde) { // 当StaticLoggerBinder不存在时,会将状态修改成NOP_FALLBACK_INITIALIZATION,并抛出信息 String msg = ncde.getMessage(); if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) { INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION; Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\"."); Util.report("Defaulting to no-operation (NOP) logger implementation"); Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details."); } else { failedBinding(ncde); throw ncde; } } catch (java.lang.NoSuchMethodError nsme) { // 当StaticLoggerBinder.getSingleton()方法不存在时,会将状态修改成初始化失败,并抛出信息 String msg = nsme.getMessage(); if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) { INITIALIZATION_STATE = FAILED_INITIALIZATION; Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding."); Util.report("Your binding is version 1.5.5 or earlier."); Util.report("Upgrade your binding to version 1.6.x."); } throw nsme; } catch (Exception e) { failedBinding(e); throw new IllegalStateException("Unexpected initialization failure", e); } }
这里再补充一个问题,slf4j-api 中不包含 StaticLoggerBinder 类,为何能编译经过呢?其实咱们项目中用到的 slf4j-api 是已经编译好的 class 文件,因此不须要再次编译。可是,编译前 slf4j-api 中是包含 StaticLoggerBinder.java 的,且编译后也存在 StaticLoggerBinder.class ,只是这个文件被手动删除了。
前面说过,logback 支持采用 xml、grovy 和 SPI 的方式配置文件,本文只分析 xml 文件配置的方式。
logback 依赖于 Joran(一个成熟的,灵活的而且强大的配置框架 ),本质上是采用 SAX 方式解析 XML。由于 SAX 不是本文的重点内容,因此这里不会去讲解相关的原理,可是,这部分的分析须要具有 SAX 的基础,能够参考个人另外一篇博客: 源码详解系列(三) ------ dom4j的使用和分析(重点对比和DOM、SAX的区别)
logback 加载配置的代码仍是比较繁琐,且代码量较大,这里就不一个个方法地分析了,而是采用类图的方式来说解。下面是 logback 加载配置的大体图解:
这里再补充下图中几个类的做用:
类名 | 描述 |
---|---|
SaxEventRecorder | SaxEvent 记录器。继承了 DefaultHandler,因此在解析 xml 时会触发对应的方法, 这些方法将触发的参数封装到 saxEven 中并放入 saxEventList 中 |
SaxEvent | SAX 事件体。用于封装 xml 事件的参数。 |
Action | 执行的配置动做。 |
ElementSelector | 节点模式匹配器。 |
RuleStore | 用于存放模式匹配器-动做的键值对。 |
结合上图,我简单归纳下整个执行过程:
如今回到 StaticLoggerBinder.getLoggerFactory()
方法,以下。这个方法返回的 ILoggerFactory 其实就是 LoggerContext。
private LoggerContext defaultLoggerContext = new LoggerContext(); public ILoggerFactory getLoggerFactory() { // 若是初始化未完成,直接返回defaultLoggerContext if (!initialized) { return defaultLoggerContext; } if (contextSelectorBinder.getContextSelector() == null) { throw new IllegalStateException("contextSelector cannot be null. See also " + NULL_CS_URL); } // 若是是DefaultContextSelector,返回的仍是defaultLoggerContext // 若是是ContextJNDISelector,则可能为不一样线程提供不一样的LoggerContext 对象 // 主要取决因而否设置系统属性-Dlogback.ContextSelector=JNDI return contextSelectorBinder.getContextSelector().getLoggerContext(); }
下面简单看下 LoggerContext 的 UML 图。它不只做为获取 logger 的工厂,还绑定了一些全局的 Object、property 和 LifeCycle。
这里先看下 Logger 的 UML 图,以下。在 Logger 对象中,持有了父级 logger、子级 logger 和 appender 的引用。
进入LoggerContext.getLogger(String)
方法,以下。这个方法逻辑简单,可是设计很是巧妙,能够好好琢磨下。我归纳下主要的步骤:
public final Logger getLogger(final String name) { if (name == null) { throw new IllegalArgumentException("name argument cannot be null"); } // 若是获取的是root logger,直接返回 if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) { return root; } int i = 0; Logger logger = root; // 在loggerCache中缓存着已经建立的logger,若是存在,直接返回 Logger childLogger = (Logger) loggerCache.get(name); if (childLogger != null) { return childLogger; } // 若是还找不到,就须要建立 // 注意,要获取以cn.zzs.logback.LogbackTest为名的logger,名为cn、cn.zzs、cn.zzs.logback的logger不存在的话也会被建立 String childName; while (true) { // 从起始位置i开始,获取“.”的位置 int h = LoggerNameUtil.getSeparatorIndexOf(name, i); // 截取logger的名字 if (h == -1) { childName = name; } else { childName = name.substring(0, h); } // 修改起始位置,以获取下一个“.”的位置 i = h + 1; synchronized (logger) { // 判断当前logger是否存在以childName命名的子级 childLogger = logger.getChildByName(childName); if (childLogger == null) { // 经过当前logger来建立以childName命名的子级 childLogger = logger.createChildByName(childName); // 放入缓存 loggerCache.put(childName, childLogger); // logger总数量+1 incSize(); } } // 当前logger修改成子级logger logger = childLogger; // 若是当前logger是最后一个,则跳出循环 if (h == -1) { return childLogger; } } }
进入Logger.createChildByName(String)
方法,以下。
Logger createChildByName(final String childName) { // 判断要建立的logger在名字上是否是与当前logger为父子,若是不是会抛出异常 int i_index = LoggerNameUtil.getSeparatorIndexOf(childName, this.name.length() + 1); if (i_index != -1) { throw new IllegalArgumentException("For logger [" + this.name + "] child name [" + childName + " passed as parameter, may not include '.' after index" + (this.name.length() + 1)); } // 建立子logger集合 if (childrenList == null) { childrenList = new CopyOnWriteArrayList<Logger>(); } Logger childLogger; // 建立新的logger childLogger = new Logger(childName, this, this.loggerContext); // 将logger放入集合中 childrenList.add(childLogger); // 设置有效日志等级 childLogger.effectiveLevelInt = this.effectiveLevelInt; return childLogger; }
logback 在类的设计上很是值得学习, 使得许多代码逻辑也很是简单易懂。
这里以Logger.debug(String)
为例,以下。这里须要注意 TurboFilter 和 Filter 的区别,前者是全局的,每次发起日志记录请求都会被调用,且在日志事件建立前调用,然后者是附加的,做用范围较小。由于实际项目中 TurboFilter 使用较少,这里不作扩展,感兴趣可参考这里。
public static final String FQCN = ch.qos.logback.classic.Logger.class.getName(); public void debug(String msg) { filterAndLog_0_Or3Plus(FQCN, null, Level.DEBUG, msg, null, null); } private void filterAndLog_0_Or3Plus(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params, final Throwable t) { // 使用TurboFilter过滤当前日志,判断是否经过 final FilterReply decision = loggerContext.getTurboFilterChainDecision_0_3OrMore(marker, this, level, msg, params, t); // 返回NEUTRAL表示没有TurboFilter,即无需过滤 if (decision == FilterReply.NEUTRAL) { // 若是须要打印日志的等级小于有效日志等级,则直接返回 if (effectiveLevelInt > level.levelInt) { return; } } else if (decision == FilterReply.DENY) { // 若是不经过,则不打印日志,直接返回 return; } // 建立LoggingEvent buildLoggingEventAndAppend(localFQCN, marker, level, msg, params, t); }
进入Logger.buildLoggingEventAndAppend(String, Marker, Level, String, Object[], Throwable)
,以下。 logback 中,日志记录请求会被构形成日志事件 LoggingEvent,传递给对应的 appender 处理。
private void buildLoggingEventAndAppend(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params, final Throwable t) { // 构造日志事件LoggingEvent LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params); // 设置标记 le.setMarker(marker); // 通知LoggingEvent给当前logger持有的和继承的appender callAppenders(le); }
进入到Logger.callAppenders(ILoggingEvent)
,以下。
public void callAppenders(ILoggingEvent event) { int writes = 0; // 通知LoggingEvent给当前logger的持有的和继承的appender处理日志事件 for (Logger l = this; l != null; l = l.parent) { writes += l.appendLoopOnAppenders(event); // 若是设置了logger的additivity=false,则不会继续查找父级的appender // 若是没有设置,则会一直查找到root logger if (!l.additive) { break; } } // 当前logger未设置appender,在控制台打印提醒 if (writes == 0) { loggerContext.noAppenderDefinedWarning(this); } } private int appendLoopOnAppenders(ILoggingEvent event) { if (aai != null) { // 调用AppenderAttachableImpl的方法处理日志事件 return aai.appendLoopOnAppenders(event); } else { // 若是当前logger没有appender,会返回0 return 0; } }
在继续分析前,先看下 Appender 的 UML 图(注意,Appender 还有不少实现类,这里只列出了经常使用的几种)。Appender 持有 Filter 和 Encoder 到引用,能够分别对日志进行过滤和格式转换。
本文仅涉及到 ConsoleAppender 的源码分析。
继续进入到AppenderAttachableImpl.appendLoopOnAppenders(E)
,以下。这里会遍历当前 logger 持有的 appender,并调用它们的 doAppend 方法。
public int appendLoopOnAppenders(E e) { int size = 0; // 得到当前logger的全部appender final Appender<E>[] appenderArray = appenderList.asTypedArray(); final int len = appenderArray.length; for (int i = 0; i < len; i++) { // 调用appender的方法 appenderArray[i].doAppend(e); size++; } // 这个size为appender的数量 return size; }
为了简化分析,本文仅分析打印日志到控制台的过程,因此进入到UnsynchronizedAppenderBase.doAppend(E)
方法,以下。
public void doAppend(E eventObject) { // 避免doAppend方法被重复调用?? // TODO 这一步不是很理解,同一个线程还能同时调用两次这个方法? if (Boolean.TRUE.equals(guard.get())) { return; } try { guard.set(Boolean.TRUE); // 过滤当前日志事件是否容许打印 if (getFilterChainDecision(eventObject) == FilterReply.DENY) { return; } // 调用实现类的方法 this.append(eventObject); } catch (Exception e) { if (exceptionCount++ < ALLOWED_REPEATS) { addError("Appender [" + name + "] failed to append.", e); } } finally { guard.set(Boolean.FALSE); } }
进入到OutputStreamAppender.append(E)
,以下。
protected void append(E eventObject) { // 若是appender未启动,则直接返回,不处理日志事件 if (!isStarted()) { return; } subAppend(eventObject); } protected void subAppend(E event) { // 这里又判断一次?? if (!isStarted()) { return; } try { // 这一步不是很懂 TODO if (event instanceof DeferredProcessingAware) { ((DeferredProcessingAware) event).prepareForDeferredProcessing(); } // 调用encoder的方法将日志事件转化为字节数组 byte[] byteArray = this.encoder.encode(event); // 打印日志 writeBytes(byteArray); } catch (IOException ioe) { this.started = false; addStatus(new ErrorStatus("IO failure in appender", this, ioe)); } }
看下LayoutWrappingEncoder.encode(E)
,以下。
public byte[] encode(E event) { // 根据配置格式处理日志事件 String txt = layout.doLayout(event); // 将字符转化为字节数组并返回 return convertToBytes(txt); }
后面会调用PatternLayout.doLayout(ILoggingEvent)
将日志的消息进行处理,这部份内容我就不继续扩展了,感兴趣能够自行研究。
以上是 logback 的源码基本分析完成,后续有空再做补充。
相关源码请移步:https://github.com/ZhangZiSheng001/logback-demo
本文为原创文章,转载请附上原文出处连接:https://www.cnblogs.com/ZhangZiSheng001/p/12246122.html