06.java日志之logback源码和xml解析
本篇文章中涉及到的所有代码都已经上传到gitee中: gitee.com/sss123a/log…
示例中包含了logback中几乎所有的特性和组件:
记重点
如何找到xml配置文件,具体逻辑参考DefaultJoranConfigurator#findURLOfDefaultConfigurationFile()
:
- 读取系统属性
logback.configurationFile
; - 当没有找到时,继续试着查找
logback-test.xml
文件; - 当没有找到时,继续试着查找
logback.xml
文件; - 如果仍然没有找到,则使用默认配置(打印到控制台)
logback基本认识
主要三个模块组成
- logback-core:其它模块的基础设施,其它模块基于它构建
- logback-classic:被认为是
Log4J
的一个改进版,并且它实现了SLF4J
简单日志门面 - logback-access:主要作为一个与
Servlet
容器交互的模块,比如说tomcat
或者jetty
,提供一些与HTTP访问相关的功能
logback启动流程
核心入口:
ch.qos.logback.classic.spi.LogbackServiceProvider#initialize();
下面具体看看initialize()
方法内部逻辑:
private LoggerContext defaultLoggerContext;
private IMarkerFactory markerFactory;
private MDCAdapter mdcAdapter;
@Override
public void initialize() {
// LoggerContext本质上是一个org.slf4j.ILoggerFactory
defaultLoggerContext = new LoggerContext();
defaultLoggerContext.setName("default");
// 找配置文件去初始化LoggerContext
initializeLoggerContext();
// 发布事件给LoggerContextListener
defaultLoggerContext.start();
markerFactory = new BasicMarkerFactory();
mdcAdapter = new LogbackMDCAdapter();
}
private void initializeLoggerContext() {
new ContextInitializer(defaultLoggerContext).autoConfig();
}
基于ContextInitializer
类初始化LoggerContext
public void autoConfig(ClassLoader classLoader) throws JoranException {
String versionStr = EnvUtil.logbackVersion();
if (versionStr == null) {
versionStr = CoreConstants.NA;
}
// logback内部日志
loggerContext.getStatusManager().add(new InfoStatus("This is logback-classic version " + versionStr, loggerContext));
// 读取系统属性logback.statusListenerClass尝试注册StatusListener
StatusListenerConfigHelper.installIfAsked(loggerContext);
// 通过SPI机制去加载classpath中META-INF/services/ch.qos.logback.classic.spi.Configurator文件内容
List<Configurator> configuratorList = ClassicEnvUtil.loadFromServiceLoader(Configurator.class, classLoader);
sortByPriority(configuratorList);
for (Configurator c : configuratorList) {
c.setContext(loggerContext);
Configurator.ExecutionStatus status = c.configure(loggerContext);
if (status == Configurator.ExecutionStatus.DO_NOT_INVOKE_NEXT_IF_ANY) {
return;
}
}
// at this stage invoke basicConfigurator
fallbackOnToBasicConfigurator();
}
以上代码执行主要分三步:
- 读取系统属性
logback.statusListenerClass
尝试注册StatusListener
,用来输出logback内部日志status - 通过SPI机制去加载classpath中
META-INF/services/ch.qos.logback.classic.spi.Configurator
文件内容,默认返回ch.qos.logback.classic.util.DefaultJoranConfigurator
,见下图:
- 找到xml配置文件,具体逻辑参考
DefaultJoranConfigurator#findURLOfDefaultConfigurationFile()
,具体见下:
- 读取系统属性
logback.configurationFile
; - 当没有找到时,继续试着查找
logback-test.xml
文件; - 当没有找到时,继续试着查找
logback.xml
文件; - 如果仍然没有找到,则使用默认配置(打印到控制台)
- 利用
DefaultJoranConfigurator
解析xml配置文件,初始化logback上下文,具体逻辑见下:
public final void doConfigure(final InputSource inputSource) throws JoranException {
context.fireConfigurationEvent(newConfigurationStartedEvent(this));
long threshold = System.currentTimeMillis();
SaxEventRecorder recorder = populateSaxEventRecorder(inputSource);
List<SaxEvent> saxEvents = recorder.getSaxEventList();
if (saxEvents.isEmpty()) {
addWarn("Empty sax event list");
return;
}
Model top = buildModelFromSaxEventList(recorder.getSaxEventList());
if (top == null) {
addError(ErrorCodes.EMPTY_MODEL_STACK);
return;
}
sanityCheck(top);
processModel(top);
// no exceptions at this level
StatusUtil statusUtil = new StatusUtil(context);
if (statusUtil.noXMLParsingErrorsOccurred(threshold)) {
addInfo("Registering current configuration as safe fallback point");
registerSafeConfiguration(top);
}
context.fireConfigurationEvent(newConfigurationEndedEvent(this));
}
以上代码执行主要分三步:
- 发布一个
ConfigurationEvent
事件,表示准备开始解析xml配置文件了
context.fireConfigurationEvent(newConfigurationStartedEvent(this));
- 构建一个SaxEventRecorder解析xml,将xml文件解析成一个个的
ch.qos.logback.core.joran.event.SaxEvent
(这步不需要深究,直接忽略)
SaxEventRecorder recorder = populateSaxEventRecorder(inputSource);
List<SaxEvent> saxEvents = recorder.getSaxEventList();
- 将这些SaxEvent转成一个个的ch.qos.logback.core.model.Model,转换规则见1ch.qos.logback.classic.joran.JoranConfigurator#addElementSelectorAndActionAssociations(RuleStore)`:
Model top = buildModelFromSaxEventList(recorder.getSaxEventList());
调用addElementSelectorAndActionAssociations(rs);
这行代码的时候会添加转换规则,如下:
- 基于以下规则,找到各个Model对应的ModelHandlerBase处理器,然后回调其
ch.qos.logback.core.model.processor.ModelHandlerBase#handle()
和postHandle()
方法
processModel(top);
public void processModel(Model model) {
buildModelInterpretationContext();
DefaultProcessor defaultProcessor = new DefaultProcessor(context, this.modelInterpretationContext);
// 添加model跟handler的映射
addModelHandlerAssociations(defaultProcessor);
synchronized (context.getConfigurationLock()) {
// 开始调用handler去处理model了
defaultProcessor.process(model);
}
}
- 发布一个
ConfigurationEvent
事件,表示xml配置文件解析完成了
context.fireConfigurationEvent(newConfigurationEndedEvent(this));
logback mdc功能
如何使用mdc(具体参考PatternLayout
、MDCConverter
):
- %mdc:读取mdc中所有键值对,以格式k1=v1, k2=v2,...展示
- %mdc{username:-xxx}:读取mdc中username对应的value,默认值为xxx
- %mdc{age}:读取mdc中age对应的value
示例代码如下:
package com.matio.logback.mdc;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
public class MDCTest {
public static void main(String[] args) {
// 读取resources下logback-mdc.xml作为logback配置文件
System.setProperty("logback.configurationFile", "logback-mdc.xml");
Logger logger = LoggerFactory.getLogger("logger123");
logger.info("mdc默认");
org.slf4j.MDC.put("username", "matio");
MDC.put("age", "20");
ch.qos.logback.classic.util.LogbackMDCAdapter mdcAdapter = (ch.qos.logback.classic.util.LogbackMDCAdapter) MDC.getMDCAdapter();
try {
logger.info("test mdc-1");
} finally {
MDC.remove("username");
}
MDC.put("username", "zcf");
try {
logger.info("test mdc-2");
} finally {
MDC.remove("username");
}
}
}
logback-mdc.xml
文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-5level %d{yyyy-MM-dd HH:mm:ss.SSS} %mdc{username:-xxx} %mdc{age} [%logger{50}] %file %-4line %method - %msg%n</pattern>
</encoder>
</appender>
<root>
<appender-ref ref="STDOUT"/>
</root>
</configuration>
打印结果如下:
INFO 2024-03-29 10:26:07.374 xxx [logger123] MDCTest.java 13 main - mdc默认
INFO 2024-03-29 10:26:07.406 matio 20 [logger123] MDCTest.java 22 main - test mdc-1
INFO 2024-03-29 10:26:07.406 zcf 20 [logger123] MDCTest.java 30 main - test mdc-2
logback xml标签
前置知识点
- 属性的
scope
有三种:local、context、system,默认为local
可以在本地范围、上下文范围或系统范围中定义用于插入的属性。本地作用域是默认的。虽然可以从操作系统环境中读取变量,但不可能写入操作系统环境。
local:具有局部作用域的属性从其在配置文件中的定义开始一直存在,直到所述配置文件的解释/执行结束。因此,每次解析和执行配置文件时,都会重新定义本地范围内的变量。
context:将具有上下文范围的属性插入到上下文中,并与上下文一样长或直到其被清除。一旦定义,上下文范围中的属性就是上下文的一部分。因此,它在所有日志事件中都可用,包括那些通过序列化发送到远程主机的事件。
system:会被保存到JVM的系统属性中,我们可以在我们的程序中使用System.getProperty()
读取。
读取变量的顺序:首先在local中查找属性,其次在context中查找,第三在system中查找,最后在操作系统环境中查找。
- 变量的默认值、嵌套变量
在某些情况下,如果变量未声明或其值为 null,则可能希望变量具有默认值。与Bash shell中一样,可以使用“:-”运算符指定默认值 。例如"${aName:-golden}"
,假设名为aName的变量未定义,将被解释为“golden”。
一个变量的默认值可以引用另一个变量。例如,假设变量“id”未分配,并且变量“userid”被分配值“alice”,则表达式“{id :- {userid}}”将返回“alice”。
configuration 根标签
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="true" scanPeriod="60 seconds" packagingData="true">
...
</configuration>
支持以下四个属性:
debug:是否印出logback内部日志信息status,查看logback运行状态,系统属性logback.debug优先级更高。如果为true则跟在xml中注册一个OnConsoleStatusListener
功能一致
scan:为true时启动一个定时任务ReconfigureOnChangeTask去监听该xml,如果发生改变,将会被重新加载
scanPeriod:定时任务间隔,当scan为true时有效,默认为1 minute,格式参考Duration#valueOf();
packagingData:默认false。如果为true,logback可以包括它输出的堆栈跟踪行的每一行的打包数据。打包数据由jar文件的名称和版本组成,堆栈跟踪行的类源自jar文件。打包数据对于识别软件版本控制问题非常有用。然而,计算成本相当高,尤其是在频繁抛出异常的应用程序中。以下是输出示例:
14:28:48.835 [btpool0-7] INFO c.q.l.demo.prime.PrimeAction - 99 is not a valid value
java.lang.Exception: 99 is invalid
at ch.qos.logback.demo.prime.PrimeAction.execute(PrimeAction.java:28) [classes/:na]
at org.apache.struts.action.RequestProcessor.processActionPerform(RequestProcessor.java:431) [struts-1.2.9.jar:1.2.9]
at org.apache.struts.action.RequestProcessor.process(RequestProcessor.java:236) [struts-1.2.9.jar:1.2.9]
at org.apache.struts.action.ActionServlet.doPost(ActionServlet.java:432) [struts-1.2.9.jar:1.2.9]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:820) [servlet-api-2.5-6.1.12.jar:6.1.12]
at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:502) [jetty-6.1.12.jar:6.1.12]
at ch.qos.logback.demo.UserServletFilter.doFilter(UserServletFilter.java:44) [classes/:na]
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1115) [jetty-6.1.12.jar:6.1.12]
at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:361) [jetty-6.1.12.jar:6.1.12]
at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:417) [jetty-6.1.12.jar:6.1.12]
at org.mortbay.jetty.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:230) [jetty-6.1.12.jar:6.1.12]
也可以在java程序中手动启用/禁用打包数据
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); lc.setPackagingDataEnabled(true);
原理:ConfigurationAction
解析成 ConfigurationModel
交给 ConfigurationModelHandler
contextName 上下文名称
<contextName>myApplicationName</contextName>
原理:ContextNameAction
解析成 ContextNameModel
交给 ContextNameModelHandler
,
用来设置上下文名称,每个logger都关联到logger上下文,默认上下文名称为default。但可以使用该标签设置成其他名字, 用于区分不同应用程序的记录。一旦设置,不能修改
可以通过${CONTEXT_NAME}
获取contextName值,具体参考ch.qos.logback.core.ContextBase#getProperty
pattern中可以通过%contextName
来打印日志上下文名称
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<contextName>myApplicationName</contextName>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>stdout %d %p %contextName - %m%n</pattern>
</encoder>
</appender>
</configuration>
property 键值对定义
- 有5个属性,name、value、file、resource和local
- 通过
<property>
定义的键值对会被插入到logger上下文中 - 通过使" ${xxx} "来使用变量
name: 变量的名称,
value: 变量定义的值,然后调用ActionUtil.setProperty保存kv
scope可选值:system、context、local,默认local
file:映射properties文件,然后调用ModelUtil.setProperties保存kv
resource:映射properties文件,然后调用ModelUtil.setProperties保存kv
在Logback中,变量有三种不同的scope:local scope ,context scope,system scope。 在变量替换的时候,首先从local scope中找,然后是context scope,然后是system properties中找,最后是操作系统环境变量中找。 可以在 元素中使用scope属性,它的属性值可以是:local,context,system。如果不指定,默认是local。
原文链接:blog.csdn.net/youxijishu/…
只有满足以下三种情况中的一种,property标签才有意义:
- name和value非空,file为空和resource为空:name和value才有效
- name和value为空,file非空和resource为空:file有效
- name和value为空,file为空和resource非空:resource有效
用法如下:
<!-- 定义kv -->
<property name="APP_Name" value="myAppName"/>
<!-- 从resources中读取property -->
<property resource="property/test.properties"/>
<!-- 从磁盘上读取property -->
<property file="E:\WorkspaceIdea\01src\log\matio-slf4j\slf4j_logback\src\main\resources\property\xxx.properties"/>
test.properties
或xxx.properties
文件内容格式如下:
k1=v1
k2=v2
com.mx.a=yyy
name=matio
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- property同variable:PropertyAction 解析成 PropertyModel 交给 PropertyModelHandler
用来定义变量值,它有两个属性name和value,通过<property>定义的值会被插入到logger上下文中,可以使用“${}”来使用变量。
只有满足以下三种情况中的一种,property才有意义:
1.name和value非空,file为空和resource为空:name和value才有效
2.name和value为空,file非空和resource为空:file有效
3.name和value为空,file为空和resource非空:resource有效
name: 变量的名称,
value: 变量定义的值,然后调用ActionUtil.setProperty
scope可选值:system、context、local,默认local
file:映射properties文件,然后调用ModelUtil.setProperties
resource:映射properties文件,然后调用ModelUtil.setProperties
-->
<property name="APP_Name" value="myAppName"/>
<property name="xx" value="yyy"/>
<!-- 从resources中读取property -->
<property resource="property/test.properties"/>
<!-- 从磁盘上读取property -->
<property file="E:\WorkspaceIdea\01src\log\matio-slf4j\slf4j_logback\src\main\resources\property\xxx.properties"/>
<contextName>${APP_Name}</contextName>
<property name="Console_Pattern"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} ${k1} ${name} %contextName %-5level [%logger{50}] - %msg%n"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${Console_Pattern}</pattern>
</encoder>
</appender>
<root>
<appender-ref ref="STDOUT"/>
</root>
</configuration>
include 引入文件
可以使用<include>
标签在一个配置文件中包含另外一个配置文件
支持从多种源头包含:
<!-- 从文件中包含 -->
<include file="src/main/java/chapters/configuration/config.xml"/>
<!-- 从 classpath 中包含 -->
<include resource="config.xml"/>
<!-- 从 URL 中包含 -->
<include url="http://host:port/config.xml"/>
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
如果包含不成功,那么 logback 会打印出一条警告信息,如果不希望 logback打印告警信息,只需这样做:
<include optional="true" ..../>
optional:如果include不成功,logback会输出日志,设置为true可以关闭,默认false
file、url和resource只能三选一
另外被包含的文件必须有以下格式:
<included>
...
</included>
原理:IncludeAction
解析成 IncludeModel
示例代码如下:
package com.matio.logback.include;
import com.matio.logback.consoleplugin.TestConsolePlugin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TestInclude {
public static void main(String[] args) {
// 读取resources下logback-include.xml作为logback配置文件
System.setProperty("logback.configurationFile", "logback-include.xml");
Logger logger = LoggerFactory.getLogger(TestConsolePlugin.class);
logger.info("info");
logger.debug("debug");
}
}
logback-include.xml
文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<!-- include:IncludeAction 解析成 IncludeModel
optional:跟logback内部日志有关,可以忽略,默认false
file、url和resource只能三选一
-->
<include optional="false" resource="logback-include-resource.xml"/>
<!--<include file="E:\WorkspaceIdea\01src\log\matio-slf4j\slf4j_logback\src\main\resources\logback-include-resource.xml"/>-->
<root level="${LEVEL}">
<appender-ref ref="Console"/>
</root>
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<Pattern>%d %p - %m%n</Pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
</configuration>
被包含的文件logback-include-resource.xml
必须有以下格式:
<included>
<property name="LEVEL" value="debug"/>
<appender class="ch.qos.logback.core.rolling.RollingFileAppender" name="ROLLING_FILE_APPENDER">
<encoder>
<charset>UTF-8</charset>
<pattern>%d{dd.MMM.yyyy HH:mm:ss.SSS z}, [%6t], %6p, %C:%M %m%n</pattern>
</encoder>
<file>${LOGS_DIR}/${LOGFILE}.log</file>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>5MB</maxFileSize>
</triggeringPolicy>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<fileNamePattern>${LOGS_DIR}/${LOGFILE}%i.log.gz</fileNamePattern>
<minIndex>1</minIndex>
<maxIndex>21</maxIndex>
</rollingPolicy>
</appender>
<appender class="ch.qos.logback.classic.AsyncAppender" name="ASYNC_APPENDER">
<queueSize>2048</queueSize>
<includeCallerData>true</includeCallerData>
<discardingThreshold>0</discardingThreshold>
<appender-ref ref="${FILE_APPENDER}"/>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>${LOG_LEVEL}</level>
</filter>
</appender>
</included>
也可以参考:blog.csdn.net/xiyang_1990…
consolePlugin
<consolePlugin port="4321"/>
仅支持一个属性port,默认为4321,支持多个该标签
原理:由ConsolePluginAction
解析该标签后,会生成一个SocketAppender
(localhost:4321),然后注册到root logger中,
也就是说增加该标签后,默认logback中所有logger(只要继承了root)打印日志后,都会通过socket客户端发送给本地(固定为localhost)的4321端口
示例代码:
package com.matio.logback.consoleplugin;
import ch.qos.logback.classic.spi.LoggingEventVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ObjectInputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class TestConsolePlugin {
public static void main(String[] args) throws InterruptedException {
// 启用logback内部日志
System.setProperty("logback.debug111", "true");
// 读取resources下logback-consoleplugin.xml作为logback配置文件
System.setProperty("logback.configurationFile", "logback-consoleplugin.xml");
// 模拟启动一个socketserver准备接受日志
new Thread(TestConsolePlugin::openSocketServer).start();
// 等待socketserver启动完成
Thread.sleep(3000);
Logger logger = LoggerFactory.getLogger(TestConsolePlugin.class);
logger.info("info");
logger.debug("debug");
}
// 使用文心一言写的demo
private static void openSocketServer() {
int portNumber = 4321;
try (
ServerSocket serverSocket = new ServerSocket(portNumber);
Socket clientSocket = serverSocket.accept();
ObjectInputStream ois = new ObjectInputStream(clientSocket.getInputStream());
) {
Object resp;
while ((resp = ois.readObject()) != null) {
LoggingEventVO event = (LoggingEventVO) resp;
System.out.println("Received: " + event.getLevel().toString() + " : " + event.getMessage());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
logback-consoleplugin.xml
文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="${logback.debug111}" scan="false">
<!-- consolePlugin:ConsolePluginAction解析,port默认4321
生成一个SocketAppender(localhost:4321)注册到root中
-->
<consolePlugin port="4321"/>
</configuration>
receiver
后续补全
contextListener 上下文监听器
侦听与LoggerContext生命周期相关的事件
<!-- contextListener:LoggerContextListenerAction 解析成 LoggerContextListenerModel 交给 LoggerContextListenerModelHandler
class:非空,反射生成LoggerContextListener子实现,然后回调其(LifeCycle)start()方法,最后注册到LoggerContext中
-->
<contextListener class="com.matio.logback.contextlistener.CustomLoggerContextListener"/>
<!-- 将对logback记录器所做的级别更改传播到jul中的等效记录器中 -->
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>
仅支持一个属性class,非空,否则该标签无效。支持多个contextListener标签
原理:LoggerContextListenerAction
解析成 LoggerContextListenerModel
交给 LoggerContextListenerModelHandler
,class会被反射生成LoggerContextListener
子实现,回调其(LifeCycle)start()
方法,最后注册到LoggerContext
中,
也就是说通过该标签可以监听LoggerContext
的start、reset、stop和levelChange事件
levelChange事件是指logback中logger的level发生更改,然后会通知该LoggerContextListener
示例代码:
自定义LoggerContextListener
子实现如下:
package com.matio.logback.contextlistener;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.LoggerContextListener;
import ch.qos.logback.core.spi.ContextAwareBase;
import ch.qos.logback.core.spi.LifeCycle;
// 参考LoggerContextListenerModelHandler
// 参考LevelChangePropagator
public class CustomLoggerContextListener extends ContextAwareBase implements LoggerContextListener, LifeCycle {
boolean isStarted = false;
@Override
public void start() {
isStarted = true;
}
@Override
public void stop() {
isStarted = false;
}
@Override
public boolean isStarted() {
return isStarted;
}
@Override
public boolean isResetResistant() {
return false;
}
@Override
public void onStart(LoggerContext context) {
System.out.println("context start...");
}
@Override
public void onReset(LoggerContext context) {
System.out.println("context reset...");
}
@Override
public void onStop(LoggerContext context) {
System.out.println("context stop...");
}
@Override
public void onLevelChange(Logger logger, Level newLevel) {
System.out.println("修改logger[" + logger.getName() + "]的level为" + newLevel.toString());
}
}
statusListener
监听logback内部日志status
<!-- statusListener:StatusListenerAction 解析成 StatusListenerModel 交给 StatusListenerModelHandler
class:非空,反射生成StatusListener子实现,注册到Context.getStatusManager()中,顺便回调其(LifeCycle)start()方法
-->
<statusListener class="com.matio.logback.statuslistener.CusStatusListener"/>
原理:statusListener
标签的解析交由StatusListenerModelHandler
实现
另外,我们也可以通过Java代码注册StatusListener
,示例如下:
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
StatusManager statusManager = lc.getStatusManager();
OnConsoleStatusListener onConsoleListener = new OnConsoleStatusListener();
statusManager.add(onConsoleListener);
请注意,已注册的状态侦听器
StatusListener
将仅接收其注册后的状态事件。它不会接收之前的消息。因此,通常最好将状态侦听器StatusListener
注册指令放置在配置文件顶部的其他指令之前。
shutdownHook
安装 JVM 关闭钩子是关闭 logback 并释放相关资源的便捷方法
<!-- jvm停止后,延迟5s执行ShutdownHookBase#stop(),delay默认为0 -->
<shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook">
<!-- delay支持的参数形式可以参考Duration#valueOf() -->
<delay>5 seconds</delay>
</shutdownHook>
<!-- 自定义ShutdownHookBase实现 -->
<shutdownHook class="com.matio.logback.shutdownhook.CusShutdownHook">
<!-- 通过反射调用setName();注入到当前对象中,也就是CusShutdownHook,具体可以参考PropertySetter#setProperty(String, String); -->
<name>matio</name>
</shutdownHook>
仅支持一个属性class,如果class没有值,则默认为ch.qos.logback.core.hook.DefaultShutdownHook
,支持多个该标签
原理:ShutdownHookAction
解析成 ShutdownHookModel
交给 ShutdownHookModelHandler
,class会被反射生成ShutdownHookBase
子实现,回调其(LifeCycle)start()
方法,然后注册到jvm中等待jvm关闭时回调。
也就是说增加该标签后,程序停止时可以优雅停止logback,也可以实现用户自己的业务
自定义ShutdownHookBase
实现示例代码:
package com.matio.logback.shutdownhook;
import ch.qos.logback.core.hook.ShutdownHookBase;
public class CusShutdownHook extends ShutdownHookBase {
private String name;
@Override
public void run() {
System.out.println(name);
super.stop();
}
// 字段通过反射调用
public void setName(String name) {
this.name = name;
}
}
logger
<!--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>
name:该logger的名称。非空
level:接受不区分大小写的字符串值:TRACE、DEBUG、INFO、WARN、ERROR、ALL 或 OFF 之一。特殊的不区分大小写的值INHERITED或其同义词NULL,将强制从层次结构中的更高层继承记录器的级别
additivity:是否继承父parent(根据name判断,默认是root),默认是true。默认情况下子Logger会继承父Logger的appender,也就是说子Logger会在父Logger的appender里输出。 若是additivity设为false,则子Logger只会在自己的appender里输出,而不会在父Logger的appender里输出
该<logger>
元素可以包含零个或多个<appender-ref>
元素;这样引用的每个附加程序都会添加到指定的记录器中。与log4j不同,logback-classic在配置给定logger时 不会关闭或删除任何先前引用的附加程序
root
<!-- 从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF-->
<root level="ALL">
<appender-ref ref="Console"/>
<appender-ref ref="RollingFileWarn"/>
<appender-ref ref="RollingFileError"/>
</root>
只支持一个属性level。由于根记录器已被命名为“ROOT”,因此它也不允许使用名称属性。
level 属性的值可以是不区分大小写的字符串:TRACE、DEBUG、INFO、WARN、ERROR、ALL 或 OFF 之一。root的level值不能设置为 INHERITED 或 NULL
与元素<logger>
类似, <root>
元素可以包含零个或多个 <appender-ref>
元素;这样引用的每个附加程序都会添加到根记录器中。与 log4j 不同,logback-classic在配置root时 不会关闭或删除任何先前引用的附加程序
appender
<property name="APP_NAME" value="web-things"/>
<property name="LOG_HOME" value="home/shuncom/log/web-things"/>
<contextName>${APP_NAME}</contextName>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%-5p %d{yyyy-MM-dd HH:mm:ss:SSS} - [%thread] %c{0} - %m%n</pattern>
</encoder>
</appender>
<appender name="CONSOLE_DEBUG" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%-5p %d{yyyy-MM-dd HH:mm:ss:SSS} - [%thread] %c - %m%n%caller%n</pattern>
</encoder>
</appender>
<!-- File Rolling Appender -->
<appender name="FILE_ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${APP_NAME}.log</file>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%-5p %d{yyyy-MM-dd HH:mm:ss:SSS} - [%thread] %c{0} - %m%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 压缩模式.zip | .gz , WW 按周进行rollover-->
<FileNamePattern>
${LOG_HOME}/${APP_NAME}_%d{yyyy-MM-dd}.log.gz
</FileNamePattern>
</rollingPolicy>
</appender>
<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>${LOG_HOME}/${APP_NAME}_error.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${APP_NAME}_error_%d{yyyy-MM-dd}.log.gz</fileNamePattern>
</rollingPolicy>
<encoder>
<pattern>%-5p %d{yyyy-MM-dd HH:mm:ss:SSS} - [%thread] %c{0} - %m%n</pattern>
</encoder>
<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.TimeBasedRollingPolicy">
<!-- 压缩模式.zip | .gz , WW 按周进行rollover-->
<FileNamePattern>
${LOG_HOME}/${APP_NAME}_error_%d{yyyy-MM-dd}.log.gz
</FileNamePattern>
</rollingPolicy>
</appender>
两个强制属性name和class: name:指定appender的名称, class:指定要实例化的appender类的完全限定名称
import
后续补全
define 动态声明变量
<!-- key=hostname,value=当前主机名 -->
<define name="hostname" class="ch.qos.logback.core.property.CanonicalHostNamePropertyDefiner" scope="system"/>
<!-- key=fileExists,value=true/false(文件是否存在) -->
<define name="fileExists" class="ch.qos.logback.core.property.FileExistsPropertyDefiner" scope="system">
<path>E:\WorkspaceIdea\01src\log\matio-slf4j\slf4j_logback\src\main\resources\logback-define.xml</path>
</define>
<!-- key=resourceExists,value=true/false(资源是否存在) -->
<define name="resourceExists" class="ch.qos.logback.core.property.ResourceExistsPropertyDefiner" scope="system">
<resource>logback-define.xml</resource>
</define>
<!-- key=k1,value=v1(见ValuePropertyDefiner)这种方式同property标签 -->
<define name="k1" class="com.matio.logback.define.ValuePropertyDefiner" scope="system">
<value>v1</value>
</define>
您可以使用<define>元素动态定义属性。define元素具有两个强制属性:name
和class
。name
属性指定要设置的属性的名称,而class
属性指定实现PropertyDefiner
接口的任何类。PropertyDefiner
实例的getPropertyValue()
方法返回的值将是命名属性的值。也可以通过指定scope
属性来指定命名属性的作用域。
原理:DefinePropertyAction
解析成 DefineModel
交给 DefineModelHandler
,然后通过反射将class生成PropertyDefiner
子实现,回调其(LifeCycle)start()
方法进行初始化,后调用其getPropertyValue()
方法返回真正的value值,调用ActionUtil.setProperty()
保存kv
logback 附带了一些相当简单的PropertyDefiner
:
CanonicalHostNamePropertyDefiner | 将命名变量设置为本地主机的规范主机名,可能需要几秒钟的时间 |
FileExistsPropertyDefiner | 如果路径属性指定的文件存在,则将变量值置为“true” ,否则为“false” |
ResourceExistsPropertyDefiner | 如果用户指定的资源在类路径上可用, 则将变量值为“true” ,否则为“false” |
package com.matio.logback.define;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DefineTest {
public static void main(String[] args) {
// 读取resources下logback-define.xml作为logback配置文件
System.setProperty("logback.configurationFile", "logback-define.xml");
Logger logger = LoggerFactory.getLogger("logger123");
logger.info("info");
logger.debug("debug");
// 如果scope=system,则logback会把kv存储到系统属性中,见ActionUtil.setProperty
System.out.println("hostname=" + System.getProperty("hostname"));
System.out.println("fileExists=" + System.getProperty("fileExists"));
System.out.println("resourceExists=" + System.getProperty("resourceExists"));
System.out.println("k1=" + System.getProperty("k1"));
System.out.println("k2=" + System.getProperty("k2"));
System.out.println("k3=" + System.getProperty("k3"));
System.out.println("k3=" + System.getProperty("k4"));
}
}
打印结果如下:
hostname=WIN-VKULP3QRAGE
fileExists=true
resourceExists=true
k1=v1
k2=null
k3=null
k3=v4
logback-define.xml
文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- define:DefinePropertyAction 解析成 DefineModel 交给 DefineModelHandler
name:非空
class:非空,反射生成PropertyDefiner子实现,回调其(LifeCycle)start()方法,后调用其getPropertyValue()方法,ActionUtil.setProperty()
scope可选值:system、context、local,默认local
-->
<!-- key=hostname,value=当前主机名 -->
<define name="hostname" class="ch.qos.logback.core.property.CanonicalHostNamePropertyDefiner" scope="system"/>
<!-- key=fileExists,value=true/false(文件是否存在) -->
<define name="fileExists" class="ch.qos.logback.core.property.FileExistsPropertyDefiner" scope="system">
<path>E:\WorkspaceIdea\01src\log\matio-slf4j\slf4j_logback\src\main\resources\logback-define.xml</path>
</define>
<!-- key=resourceExists,value=true/false(资源是否存在) -->
<define name="resourceExists" class="ch.qos.logback.core.property.ResourceExistsPropertyDefiner" scope="system">
<resource>logback-define.xml</resource>
</define>
<!-- key=k1,value=v1(见ValuePropertyDefiner),这种方式同property标签 -->
<define name="k1" class="com.matio.logback.define.ValuePropertyDefiner" scope="system">
<value>v1</value>
</define>
<property name="k2" value="v2" scope="local"/>
<property name="k3" value="v3" scope="context"/>
<property name="k4" value="v4" scope="system"/>
</configuration>
自定义ValuePropertyDefiner
如下:
package com.matio.logback.define;
import ch.qos.logback.core.PropertyDefinerBase;
public class ValuePropertyDefiner extends PropertyDefinerBase {
private String value;
@Override
public String getPropertyValue() {
return value;
}
// logback通过反射注入value
public void setValue(String value) {
this.value = value;
}
}
timestamp 时间戳
<!-- timestamp:
获取时间戳字符串,他有两个属性key和datePattern
-->
<timestamp key="kkk" datePattern="yyyy-MM-dd" timeReference="contextBirth" scope="local"/>
- key: 非空,标识此
<timestamp>
的名字; - datePattern: 非空,设置将当前时间(解析配置文件的时间)转换为字符串的模式,遵循java.txt.SimpleDateFormat的格式
- timeReference:如果值为contextBirth,value=context.getBirthTime(),否则value=System.currentTimeMillis()
- scope可选值:system、context、local,默认local,见ActionUtil.setProperty
原理:TimestampAction
解析成 TimestampModel
交给 TimestampModelHandler
,解析后作为属性注册到logback中
示例代码如下:
package com.matio.logback.timestamp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class timestampTest {
public static void main(String[] args) {
// 读取resources下logback-timestamp.xml作为logback配置文件
System.setProperty("logback.configurationFile", "logback-timestamp.xml");
Logger logger = LoggerFactory.getLogger("logger123");
logger.info("info");
logger.debug("debug");
}
}
logback-timestamp.xml
文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<!-- 对该标签的解析参考TimestampModelHandler -->
<timestamp key="bySecond1" datePattern="yyyyMMdd'T'HHmmssSSS"/>
<timestamp key="bySecond2" datePattern="yyyyMMdd'T'HHmmssSSS" timeReference="contextBirth"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>解析timestamp标签的时间:${bySecond1},context实例化时间(也就是出生时间):${bySecond2} %d %p - %m%n</pattern>
</encoder>
</appender>
<root>
<appender-ref ref="STDOUT"/>
</root>
</configuration>
sequenceNumberGenerator 日志序列号生成器
<sequenceNumberGenerator class="com.matio.logback.sequencenumbergenerator.CusSequenceNumberGenerator"/>
仅支持一个属性class,如果class没有值,则该标签无效,支持多个该标签
原理:SequenceNumberGeneratorAction
解析成 SequenceNumberGeneratorModel
交给 SequenceNumberGeneratorModelHandler
,class会被反射生成SequenceNumberGenerator
子实现,然后赋值给LoggerContext对象
也就是说增加该标签后,logback会给生成的每一条log生成一个唯一的标识,即sequenceNumber,我们也可以在打印日志的时候通过%sequenceNumber(具体参考PatternLayout类)
显示该表示,比如:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<sequenceNumberGenerator class="com.matio.logback.sequencenumbergenerator.CusSequenceNumberGenerator"/>
<!-- 可以通过%sn或者%sequenceNumber去读取每条日志的sequenceNumber,具体参考PatternLayout -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-5level %d{yyyy-MM-dd HH:mm:ss.SSS} %sequenceNumber [%logger{50}] %file %-4line %method - %msg%n</pattern>
</encoder>
</appender>
<root>
<appender-ref ref="STDOUT"/>
</root>
</configuration>
CusSequenceNumberGenerator
示例代码如下:
package com.matio.logback.sequencenumbergenerator;
import ch.qos.logback.core.spi.ContextAwareBase;
import ch.qos.logback.core.spi.SequenceNumberGenerator;
import java.util.concurrent.atomic.AtomicLong;
// BasicSequenceNumberGenerator
public class CusSequenceNumberGenerator extends ContextAwareBase implements SequenceNumberGenerator {
private static final AtomicLong x = new AtomicLong();
public CusSequenceNumberGenerator() {
System.out.println("init CusSequenceNumberGenerator...");
}
@Override
public long nextSequenceNumber() {
// 也可以在分布式环境下基于redis为每个日志生成唯一key
return x.incrementAndGet();
}
}
evaluator
conversionRule
newRule
insertFromJNDI
<insertFromJNDI env-entry-name="java:comp/env/appName" as="appName1" scope="local"/>
支持多个该标签
- env-entry-name:非空,内容必须要以java:开头
- as:非空,key值
- scope可选值:system、context、local,默认local
原理:InsertFromJNDIAction
解析成 InsertFromJNDIModel
交给 InsertFromJNDIModelHandler
,然后通过env-entry-name去提取存储在JNDI中的env条目,并使用as属性作为key保存到不同的作用域中,最后通过${xxx}去引用。
在某些情况下,您可能希望使用JNDI中存储的env条目。<insertFromJNDI>配置指令提取存储在JNDI中的env条目,并使用as属性指定的键在本地作用域中插入属性。作为所有属性,可以在scope属性的帮助下将新属性插入到不同的作用域中。
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- insertFromJNDI:InsertFromJNDIAction 解析成 InsertFromJNDIModel 交给 InsertFromJNDIModelHandler
env-entry-name:非空,内容必须要以java:开头
as:非空
scope可选值:system、context、local,默认local
最终还是调用ModelUtil.setProperty
-->
<insertFromJNDI env-entry-name="java:comp/env/appName" as="appName" scope="local"/>
<contextName>${appName}</contextName>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d ${CONTEXT_NAME} %level -%kvp- %msg %logger{50}%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="CONSOLE" />
</root>
</configuration>
if、then、else 条件判断
在项目中,存在使用条件判断的场景,例如想为测试和生产设置不同的日志记录级别。幸好的是,logback本身已经支持这种场景。
<dependency>
<groupId>org.codehaus.janino</groupId>
<artifactId>janino</artifactId>
<version>3.0.6</version>
</dependency>
如果使用if,需要引入janino依赖,否则logback会提示以下信息:
15:21:42,469 |-ERROR in ch.qos.logback.core.model.processor.conditional.IfModelHandler - Could not find Janino library on the class path. Skipping conditional processing.
15:21:42,469 |-ERROR in ch.qos.logback.core.model.processor.conditional.IfModelHandler - See also http://logback.qos.ch/codes.html#ifJanino
15:21:42,469 |-ERROR in ch.qos.logback.core.model.processor.conditional.IfModelHandler - Unexpected unexpected empty model stack.
if
语法有两种:
<!-- if-then form -->
<if condition="some conditional expression">
<then>
...
</then>
</if>
<!-- if-then-else form -->
<if condition="some conditional expression">
<then>
...
</then>
<else>
...
</else>
</if>
接下来着重看一下condition
:
condition
是一个Java表达式,其中只能访问上下文属性或系统属性。
对于作为参数传递的键,property("x")
返回该属性的String值,跟p("x")
等价。如果键为“x”的属性未定义,则属性方法将返回空字符串,而不是null。这就避免空指针。
isDefined() :检查是否定义了属性,示例:isDefined("x")
isNull():检查属性是否为null,示例:isNull("x")
示例代码:
package com.matio.logback.ifelsethen;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class IfElseThenTest {
public static void main(String[] args) {
// 模拟测试环境
System.setProperty("app.mode", "test");
// 读取resources下logback-ifelse-then.xml作为logback配置文件
System.setProperty("logback.configurationFile", "logback-ifelse-then.xml");
Logger logger = LoggerFactory.getLogger("logger123");
logger.info("info");
logger.debug("debug");
}
}
logback-ifelse-then.xml
文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<property name="APP_NAME" value="test_logback"/>
<property name="LOG_HOME" value="D:/test_logback/"/>
<contextName>${APP_NAME}</contextName>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%-5p %d{yyyy-MM-dd HH:mm:ss:SSS} - [%thread] %c - %m%n</pattern>
</encoder>
</appender>
<!-- File Rolling Appender -->
<appender name="FILE_ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${APP_NAME}.log</file>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%-5p %d{yyyy-MM-dd HH:mm:ss:SSS} - [%thread] %c{0} - %m%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 压缩模式.zip | .gz , WW 按周进行rollover-->
<FileNamePattern>
${LOG_HOME}/${APP_NAME}_%d{yyyy-MM-dd}.log.gz
</FileNamePattern>
</rollingPolicy>
</appender>
<root level="debug">
<!-- property("xx") 也可以写成 p("xx") -->
<if condition='property("app.mode").equals("test")'>
<then>
<appender-ref ref="CONSOLE"/>
</then>
<else>
<appender-ref ref="FILE_ROLLING"/>
</else>
</if>
</root>
</configuration>
转载自:https://juejin.cn/post/7350824272322461734