一. 前言
1. 相关日志文档
2. 说明
- logback也是基于slf4j开发的一套日志框架, 使用到门面模式. 点击了解更多门面模式
- 本文基于使用logback.xml配置文件进行源码解析
- 本文只讲核心代码, 不是主线的代码不展示出来
- 本文基于的logback-classic依赖版本为1.2.12
- 如有疑问可留意或查看官方文档
3. 代码准备
3.1 添加logback依赖
a. 非maven项目添加依赖 (三个)
- logback-classic.jar (示例版本: 1.2.12)
- logback-core.jar (示例版本: 1.2.12)
- slf4j-api.jar (示例版本: 1.7.36) -> 该依赖是slf4j的
b. maven项目添加依赖
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.12</version>
</dependency>
说明: logback-classic中会自动导入logback-core和slf4j-api的包, 故只需要添加一个即可
c. SpringBoot项目添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.11-SNAPSHOT</version>
</dependency>
<!-- 或者 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.7.11-SNAPSHOT</version>
</dependency>
说明: spring-boot-starter-web依赖会自动导入spring-boot-starter依赖, 而spring-boot-starter又会自动导入logback-classic. 如下图
3.2 添加logback.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 简单配置:
自己项目的打印info级别及以上的日志, 输出到控制台和info.log中
非自己项目只打印error级别日志, 输出到控制台和error.log中
-->
<!-- 日志存放路径 -->
<property name="log.path" value="/logs/logback-study" />
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d [%thread] %-5level [%logger:%line] - %msg%n" />
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/info/info.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/info/info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 3天 -->
<maxHistory>3</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- 过滤的级别: info及以上 -->
<level>INFO</level>
</filter>
</appender>
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/error/error.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/error/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 3天 -->
<maxHistory>3</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>ERROR</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<logger name="com.chenlongji.logbackstudy" level="INFO" additivity="false">
<appender-ref ref="console"/>
<appender-ref ref="file_info"/>
</logger>
<root level="error">
<appender-ref ref="console" />
<appender-ref ref="file_error"/>
</root>
</configuration>
- 注: 想了解如何配置的, 点击查询此文章
- 说明:
- 非Spring项目可以使用的配置文件:
- logback-test.xml(优先级更高)
- logback.xml
- Spring项目可以使用的配置文件
- logback-test.groovy
- logback-test.xml
- logback.groovy
- logback.xml
- logback-test-spring.groovy
- logback-test-spring.xml
- logback-spring.groovy
- logback-spring.xml -> 详情见[SpringBoot环境下logback-spring.xml配置文件一般配置]
3.3 添加main测试方法
// 使用logback.xml (下面源码都是基于logback.xml进行)
public class Test {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(Test.class);
logger.info("我是info级别的日志");
}
}
// 使用logback-spring.xml
@SpringBootApplication
public class LogbackStudyApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(LogbackStudyApplication.class, args);
Logger logger = LoggerFactory.getLogger(Test1.class);
logger.info("我是info级别的日志");
context.close();
}
}
二. 流程图和常用类图
初始化流程
说明: 额, 此人很懒,什么也没有留下
输出日志流程
GenericConfigurator类图
Action类图
Appender类图
全部Appender类实现类
说明:
- 常用的主要是UnsynchronizedAppenderBase的实现类
Appender的UnsynchronizedAppenderBase的实现类
说明:
- 最最常用的就是ConsoleAppender 和 RollingFileAppender
- AsyncAppender为异步输出日志的日志输出器(较简单, 就不讲其源码了).
AsyncAppender不执行具体的日志输出操作, 它包含了多个appender, 使用包含的appender执行日志输出
AsyncAppender启动后, 会开启一个Worker线程异步处理添加到blockingQueue的日志事件
AsyncAppender接受到日志事件时, 都是直接丢入blockingQueue队列中, 待worker去处理
Appender的OutputStreamAppender的实现类详情
三. 初始化源码
从main方法出发
public class Test {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(Test.class);
}
}
进入LoggerFactory的getLogger(Class<?> clazz)方法
public static Logger getLogger(Class<?> clazz) {
// 获取logger对象
Logger logger = getLogger(clazz.getName());
// 传入的clazz != 实际创建logger所在的类时, 输出预警信息(不展开讲)
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)方法
public static Logger getLogger(String name) {
// 核心代码: 获取ILoggerFactory对象, 即LoggerContext (该值类似于log4j中的Hierarchy)
ILoggerFactory iLoggerFactory = getILoggerFactory();
// 核心代码: 获取logger对象
return iLoggerFactory.getLogger(name);
}
说明:
- getILoggerFactory()方法执行了Logback框架的初始化
- iLoggerFactory.getLogger(name)方法获取logger对象
进入LoggerFactory的getILoggerFactory()方法
public static ILoggerFactory getILoggerFactory() {
// 未初始化, 执行初始化
if (INITIALIZATION_STATE == UNINITIALIZED) {
synchronized (LoggerFactory1.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
// 核心代码: 初始化
performInitialization();
}
}
}
// 根据初始化结果的状态, 返回ILoggerFactory实现或抛出异常
switch (INITIALIZATION_STATE) {
case SUCCESSFUL_INITIALIZATION:
// 正常完成初始化, 返回LoggerContext
// 注: LoggerFactory为slf4j-api的类, 该jar中是不包含StaticLoggerBinder的.
// 据网上说法是编译前是有的, 编译后slf4j-api中的StaticLoggerBinder.class被手动删除了
return StaticLoggerBinder.getSingleton().getLoggerFactory();
case NOP_FALLBACK_INITIALIZATION:
return NOP_FALLBACK_FACTORY;
case FAILED_INITIALIZATION:
throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
case ONGOING_INITIALIZATION:
return SUBST_FACTORY;
}
throw new IllegalStateException("Unreachable code");
}
说明:
- performInitialization()方法为初始化代码入口
- StaticLoggerBinder.getSingleton().getLoggerFactory()返回初始化后的LoggerContext对象
- StaticLoggerBinder的 SINGLETON 属性为单例的全局静态变量, 之后获取logger对象都是通过该对象的LoggerContext属性获取
进入LoggerFactory的performInitialization()方法
private final static void performInitialization() {
// 核心代码: 执行绑定(初始化)
bind();
if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
// 初始化成功后, 校验logback-classic版本是否和slf4j-api版本匹配, 不匹配输出预警信息(不展开讲)
versionSanityCheck();
}
}
进入LoggerFactory的bind()方法
private final static void bind() {
try {
Set<URL> staticLoggerBinderPathSet = null;
// 安卓环境下跳过
if (!isAndroid()) {
// 获取可能的StaticLoggerBinder路径. 对类加载器感兴趣的可自行读源码 (不展开讲)
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
// 找到多个StaticLoggerBinder时, 输出预警信息, 并输出所有StaticLoggerBinder的路径 (不展开讲)
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// 核心代码: 执行初始化
// 注: 多个StaticLoggerBinder时有人说是随机取一个, 有人说是取staticLoggerBinderPathSet中加载的第一个(实测多次都是取第一个)
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
// 打印出实际使用的StaticLoggerBinder类的ContextSelectorStaticBinder的类型
reportActualBinding(staticLoggerBinderPathSet);
} catch (NoClassDefFoundError ncde) {
} catch (NoSuchMethodError nsme) {
} catch (Exception e) {
} finally {
// 初始化替代记录器,同时输出初始化替代记录器的日志事件, 置空初始化替代记录器的信息 (不展开讲)
postBindCleanUp();
}
}
说明: 核心代码: StaticLoggerBinder.getSingleton()
进入StaticLoggerBinder的getSingleton()方法
public class StaticLoggerBinder implements LoggerFactoryBinder {
private static StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
// 加载StaticLoggerBinder类到jvm中, 执行状态代码块
static {
// 核心代码: 初始化
SINGLETON.init();
}
public static StaticLoggerBinder getSingleton() {
return SINGLETON;
}
}
说明:
- 通过调用StaticLoggerBinder.getSingleton()方法, 加载StaticLoggerBinder类到jvm中
- 加载StaticLoggerBinder类时,
进入StaticLoggerBinder的init()方法
void init() {
try {
try {
// 核心代码: 读取配置, 完成LoggerContext初始化
new ContextInitializer(defaultLoggerContext).autoConfig();
} catch (JoranException je) {
Util.report("Failed to auto configure default logger context", je);
}
// 有状态监听器时并且阈值为错误或警告时, 打印LoggerContext的状态内容 (不展开讲)
if (!StatusUtil.contextHasStatusListener(defaultLoggerContext)) {
StatusPrinter.printInCaseOfErrorsOrWarnings(defaultLoggerContext);
}
// 重要代码: 这里初始化contextSelectorBinder的contextSelector属性, contextSelector属性中包含了LoggerContext.
// 1. 之前的StaticLoggerBinder.getSingleton().getLoggerFactory()方法获取的就是contextSelector属性中的LoggerContext
// 2. contextSelector的默认实现为DefaultContextSelector. 可以通过配置指定实现, 例如logback.ContextSelector=JNDI(实现为ContextJNDISelector)
// 3. DefaultContextSelector的getLoggerContext()直接返回defaultLoggerContext,
// 4. ContextJNDISelector的getLoggerContext()可能为不同环境(contextName)提供不同的LoggerContext对象
contextSelectorBinder.init(defaultLoggerContext, KEY);
initialized = true;
} catch (Exception t) {
Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", t);
}
}
说明:
- 核心初始化代码在 ContextInitializer的autoConfig()方法中执行
- ContextInitializer的autoConfig()会完成LoggerContext内属性的初始化
- contextSelectorBinder.init(…)方法作用是指定其属性contextSelector的实现. 若配置文件指定logback.ContextSelector=JNDI, 则为不同的context-name返回不同的LoggerContext对象(日志分离), 这里不展开
进入ContextInitializer的autoConfig()方法
public void autoConfig() throws JoranException {
// 若配置了logback.statusListenerClass, 则为loggerContext添加状态监听器 (不展开)
StatusListenerConfigHelper.installIfAsked(loggerContext);
// 获取配置文件URL
URL url = findURLOfDefaultConfigurationFile(true);
if (url != null) {
// 核心代码: 通过配置文件完成配置
configureByResource(url);
} else {
// 使用SPI方式加载Configurator的实现类. (不展开) spi拓展: https://blog.youkuaiyun.com/blueheartstone/article/details/128005322
Configurator c = EnvUtil.loadFromServiceLoader(Configurator.class);
if (c != null) {
try {
c.setContext(loggerContext);
c.configure(loggerContext);
} catch (Exception e) {
throw new LogbackException(String.format("Failed to initialize Configurator: %s using ServiceLoader", c != null ? c.getClass().getCanonicalName() : "null"), e);
}
} else {
// 重要代码: 使用默认的配置BasicConfigurator. 默认配置见 [补充: 默认配置BasicConfigurator类]
BasicConfigurator basicConfigurator = new BasicConfigurator();
basicConfigurator.setContext(loggerContext);
basicConfigurator.configure(loggerContext);
}
}
}
说明:
- 先通过配置文件的方式获取配置信息, 获取到则执行configureByResource(url)方法完成配置
- 没有找到配置文件, 则使用SPI方式加载Configurator的实现类完成配置
- 上述两种都找不到, 则使用BasicConfigurator 默认配置
进入ContextInitializer的findURLOfDefaultConfigurationFile(.)方法
public URL findURLOfDefaultConfigurationFile(boolean updateStatus) {
ClassLoader myClassLoader = Loader.getClassLoaderOfObject(this);
// 获取指定的配置文件. 使用-Dlogback.configurationFile=xxx.xml 指定使用的配置文件(注, 必须的xml结尾的)
URL url = findConfigFileURLFromSystemProperties(myClassLoader, updateStatus);
if (url != null) {
return url;
}
// 有logback-test.xml配置文件, 则取它
url = getResource(TEST_AUTOCONFIG_FILE, myClassLoader, updateStatus);
if (url != null) {
return url;
}
// 前面都没找到, 则找logback.xml配置文件
return getResource(AUTOCONFIG_FILE, myClassLoader, updateStatus);
}
说明:
读取配置文件有优先级. 下面按优先级高到低枚举, 有高优先级的则直接返回
- 获取指定的配置文件. 使用-Dlogback.configurationFile=xxx.xml 指定使用的配置文件(非xml结尾的文件无效)
- logback-test.xml
- logback.xml
补充: springboot项目初始化也是这样执行, 但初始化执行到spring的监听器时, 会使用带有spring字眼的logback配置文件重新初始化LoggerContext. 详情见 [SpringBoot中logback的初始化流程]
进入ContextInitializer的configureByResource(URL url)方法
public void configureByResource(URL url) throws JoranException {
if (url == null) {
throw new IllegalArgumentException("URL argument cannot be null");
}
final String urlString = url.toString();
// 配置文件必须为xml结尾的文件
if (urlString.endsWith("xml")) {
// 使用JoranConfigurator加载配置文件, 完成配置
// 注: springboot项目有logback-spring.xml等配置文件时, 在监听器初始化时使用SpringBootJoranConfigurator完成配置. 详情见 [SpringBoot中logback的初始化流程]
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(loggerContext);
// 核心代码: doConfigure(URL url)
configurator.doConfigure(url);
} else {
throw new LogbackException("Unexpected filename extension of file [" + url.toString() + "]. Should be .xml");
}
}
进入GenericConfigurator的doConfigure(URL url)方法
了解: [GenericConfigurator类图]
public final void doConfigure(URL url) throws JoranException {
InputStream in = null;
try {
// 获取文件流
informContextOfURLUsedForConfiguration(getContext(), url);
URLConnection urlConnection = url.openConnection();
urlConnection.setUseCaches(false);
in = urlConnection.getInputStream();
// 核心代码: doConfigure(InputStream inputStream, String systemId)
doConfigure(in, url.toExternalForm());
} catch (IOException ioe) {
} finally {
}
}
进入GenericConfigurator的doConfigure(InputStream inputStream, String systemId)方法
public final void doConfigure(InputStream inputStream, String systemId) throws JoranException {
InputSource inputSource = new InputSource(inputStream);
inputSource.setSystemId(systemId);
// 核心代码: doConfigure(final InputSource inputSource)
doConfigure(inputSource);
}
进入GenericConfigurator的doConfigure(final InputSource inputSource)方法
public final void doConfigure(final InputSource inputSource) throws JoranException {
long threshold = System.currentTimeMillis();
// 使用SAXParser解析配置文件, 得到saxEventList. (不展开)
SaxEventRecorder recorder = new SaxEventRecorder(context);
recorder.recordEvents(inputSource);
// 核心代码: 执行配置
doConfigure(recorder.saxEventList);
// 没有XML解析错误发生时, 将当前配置注册为安全回退点. (不展开)
StatusUtil statusUtil = new StatusUtil(context);
if (statusUtil.noXMLParsingErrorsOccurred(threshold)) {
addInfo("Registering current configuration as safe fallback point");
registerSafeConfiguration(recorder.saxEventList);
}
}
进入GenericConfigurator的doConfigure(final List eventList)方法
public void doConfigure(final List<SaxEvent> eventList) throws JoranException {
// 重要代码: 构建解析器 (注: 假设GenericConfigurator的实现类为JoranConfigurator (默认值))
buildInterpreter();
// 核心代码: 上锁, 解析一个个SaxEvent事件(以栈的格式逐个处理配置文件中的各个标签解析出来的内容), 完成LoggerContext初始化
synchronized (context.getConfigurationLock()) {
interpreter.getEventPlayer().play(eventList);
}
}
进入JoranConfiguratorBase的buildInterpreter()方法
注: 假设GenericConfigurator的实现类为JoranConfigurator (默认值)
// 先进入子类JoranConfiguratorBase
abstract public class JoranConfiguratorBase<E> extends GenericConfigurator {
protected void buildInterpreter() {
super.buildInterpreter();
Map<String, Object> omap = interpreter.getInterpretationContext().getObjectMap();
omap.put(ActionConst.APPENDER_BAG, new HashMap<String, Appender<?>>());
}
}
// 再回到父类GenericConfigurator
protected void buildInterpreter() {
// 创建规则存储实现对象
RuleStore rs = new SimpleRuleStore(context);
// 重要代码: 添加实例的常规规则
// 说明: 添加规则示例: configuration/appender -> AppenderAction, configuration/logger -> LoggerAction
// 用途: 例如AppenderActionAction是用来完成<appender>标签的解析和appender的初始化的
addInstanceRules(rs);
// 初始化出解析器对象, 该对象完成核心的SaxEvent解析工作
this.interpreter = new Interpreter(context, rs, initialElementPath());
// 初始化出解析器上下文对象, 该对象存储解析过程重要的信息
InterpretationContext interpretationContext = interpreter.getInterpretationContext();
interpretationContext.setContext(context);
// 添加隐含的规则 (不展开)
// 说明: 添加Action: NestedComplexPropertyIA 或 NestedBasicPropertyIA
// 用途: 完成嵌套属性值的初始化和设置绑定, 例如设置appender内的encoder
addImplicitRules(interpreter);
// 添加默认嵌套组件注册表 (不展开)
// 说明: 指定嵌套组件的默认实现, 例如UnsynchronizedAppenderBase的encoder属性默认实现为PatternLayoutEncoder
addDefaultNestedComponentRegistryRules(interpretationContext.getDefaultNestedComponentRegistry());
}
说明:
- 获取实例的常规规则, 添加到JoranConfigurator的interpreter的ruleStore中
常规规则用于解析 appender, logger 等常规标签, 完成这些组件的初始化和绑定- 获取隐含的规则, 添加到JoranConfigurator的interpreter的implicitActions中
隐含规则用于解析 例如appender的内嵌标签, 例如encoder标签- 获取默认嵌套组件注册表, 添加到JoranConfigurator的interpreter的interpretationContext的defaultNestedComponentRegistry的defaultComponentMap中
默认嵌套组件注册表指定部分嵌套组件的实现
进入JoranConfigurator的addInstanceRules(RuleStore rs)
注: 假设GenericConfigurator的实现类为JoranConfigurator (默认值)
public void addInstanceRules(RuleStore rs) {
// 添加父类的规则
super.addInstanceRules(rs);
// 添加标签<configuration>的解析规则ConfigurationAction
rs.addRule(new ElementSelector("configuration"), new ConfigurationAction());
rs.addRule(new ElementSelector("configuration/contextName"), new ContextNameAction());
rs.addRule(new ElementSelector("configuration/contextListener"), new LoggerContextListenerAction());
rs.addRule(new ElementSelector("configuration/insertFromJNDI"), new InsertFromJNDIAction());
rs.addRule(new ElementSelector("configuration/evaluator"), new EvaluatorAction());
rs.addRule(new ElementSelector("configuration/appender/sift"), new SiftAction());
rs.addRule(new ElementSelector("configuration/appender/sift/*"), new NOPAction());
// 添加标签<configuration>内的<logger>的解析规则LoggerAction
rs.addRule(new ElementSelector("configuration/logger"), new LoggerAction());
rs.addRule(new ElementSelector("configuration/logger/level"), new LevelAction());
rs.addRule(new ElementSelector("configuration/root"), new RootLoggerAction());
rs.addRule(new ElementSelector("configuration/root/level"), new LevelAction());
rs.addRule(new ElementSelector("configuration/logger/appender-ref"), new AppenderRefAction<ILoggingEvent>());
rs.addRule(new ElementSelector("configuration/root/appender-ref"), new AppenderRefAction<ILoggingEvent>());
rs.addRule(new ElementSelector("*/if"), new IfAction());
rs.addRule(new ElementSelector("*/if/then"), new ThenAction());
rs.addRule(new ElementSelector("*/if/then/*"), new NOPAction());
rs.addRule(new ElementSelector("*/if/else"), new ElseAction());
rs.addRule(new ElementSelector("*/if/else/*"), new NOPAction());
if (PlatformInfo.hasJMXObjectName()) {
rs.addRule(new ElementSelector("configuration/jmxConfigurator"), new JMXConfiguratorAction());
}
rs.addRule(new ElementSelector("configuration/include"), new IncludeAction());
rs.addRule(new ElementSelector("configuration/consolePlugin"), new ConsolePluginAction());
rs.addRule(new ElementSelector("configuration/receiver"), new ReceiverAction());
}
进入JoranConfiguratorBase的addInstanceRules(RuleStore rs)
注: 假设GenericConfigurator的实现类为JoranConfigurator (默认值)
protected void addInstanceRules(RuleStore rs) {
rs.addRule(new ElementSelector("configuration/variable"), new PropertyAction());
rs.addRule(new ElementSelector("configuration/property"), new PropertyAction());
rs.addRule(new ElementSelector("configuration/substitutionProperty"), new PropertyAction());
rs.addRule(new ElementSelector("configuration/timestamp"), new TimestampAction());
rs.addRule(new ElementSelector("configuration/shutdownHook"), new ShutdownHookAction());
rs.addRule(new ElementSelector("configuration/define"), new DefinePropertyAction());
rs.addRule(new ElementSelector("configuration/contextProperty"), new ContextPropertyAction());
rs.addRule(new ElementSelector("configuration/conversionRule"), new ConversionRuleAction());
rs.addRule(new ElementSelector("configuration/statusListener"), new StatusListenerAction());
// 添加标签<configuration>内的<appender>的解析规则AppenderAction
rs.addRule(new ElementSelector("configuration/appender"), new AppenderAction<E>());
rs.addRule(new ElementSelector("configuration/appender/appender-ref"), new AppenderRefAction<E>());
rs.addRule(new ElementSelector("configuration/newRule"), new NewRuleAction());
rs.addRule(new ElementSelector("*/param"), new ParamAction(getBeanDescriptionCache()));
}
进入EventPlayer的play(List aSaxEventList)方法
前置说明:
- 这部分代码为正式的初始化, 代码非常的多, 篇幅非常的长, 有兴趣的C友可以往下看, 觉得不需要了解这么深的可直接跳到[默认配置BasicConfigurator类] 继续阅读此文
- 从paly方法开始, 逐行解析xml标签, 很多地方使用到栈, 设计非常巧妙
- 解析流程主要借助了Action组件, 该组件就是执行的动作, 类图见 [Action类图].
- 由于这里为遍历事件重复调用, 这里直接给出主要类的源码. 这里Action仅给出ContextNameAction、AppenderAction和NestedBasicPropertyIA的源码
EventPlayer类主要源码
public class EventPlayer {
// 解析器
final Interpreter interpreter;
// xml解析出来的事件列表
List<SaxEvent> eventList;
// 当前解析事件的索引
int currentIndex;
public void play(List<SaxEvent> aSaxEventList) {
eventList = aSaxEventList;
SaxEvent se;
// 遍历SaxEventList. 可以理解为逐个解析xml中的每一个标签(以栈的形式处理)
for (currentIndex = 0; currentIndex < eventList.size(); currentIndex++) {
se = eventList.get(currentIndex);
// 处理开始事件, 示例: 解析<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
if (se instanceof StartEvent) {
// 核心代码: 解析标签开始元素
interpreter.startElement((StartEvent) se);
// 将当前事件添加到所有解析器的监听器中待后续使用 (非核心代码, 不展开)
interpreter.getInterpretationContext().fireInPlay(se);
}
// 处理body事件, 示例: 解析<pattern>${log.pattern}</pattern>中的${log.pattern}
if (se instanceof BodyEvent) {
// 将当前事件添加到所有解析器的监听器中待后续使用 (非核心代码, 不展开)
interpreter.getInterpretationContext().fireInPlay(se);
// 核心代码: 解析标签体内的字符串
interpreter.characters((BodyEvent) se);
}
// 处理结束时事件, 示例: 解析</appender>
if (se instanceof EndEvent) {
// 将当前事件添加到所有解析器的监听器中待后续使用 (非核心代码, 不展开)
interpreter.getInterpretationContext().fireInPlay(se);
// 核心代码: 解析标签结束元素
interpreter.endElement((EndEvent) se);
}
}
}
}
说明:
- 开始事件, 主要执行逻辑 interpreter.startElement((StartEvent) se)
- body事件, 主要执行逻辑 interpreter.characters((BodyEvent) se)
- 结束事件, 主要执行逻辑 interpreter.endElement((EndEvent) se)
- 下图为解析xml得到的事件列表
Interpreter类主要源码
public class Interpreter {
private static List<Action> EMPTY_LIST = new Vector<Action>(0);
// 实例规则仓库. 示例: [configuration][appender] -> AppenderAction
final private RuleStore ruleStore;
// 解析器上下文, 存储解析过程重要的信息
final private InterpretationContext interpretationContext;
// 隐晦的规则列表, 用于处理ruleStore不包含的嵌套属性解析初始化和绑定等
final private ArrayList<ImplicitAction> implicitActions;
// 保留定位器信息的记录类 (不重要)
final private CAI_WithLocatorSupport cai;
// 当前解析标签的路径, 例如 [configuration] [appender]. 解析过程中类似栈的使用
private ElementPath elementPath;
// 当前解析位于xml文件的位置
Locator locator;
// 事件解析执行器, 用于解析SaxEvent事件
EventPlayer eventPlayer;
// action列表的栈, 用于记录解析过程中的解析规则
Stack<List<Action>> actionListStack;
// skip指定的标签, 它的所有嵌套元素都会被跳过
ElementPath skip = null;
/**
* 添加空的action列表占位 (入栈占位)
*/
private void pushEmptyActionList() {
actionListStack.add(EMPTY_LIST);
}
/**
* 解析标签头(入口)
*/
public void startElement(StartEvent se) {
// 记录当前解析 位于xml文件的位置
setDocumentLocator(se.getLocator());
startElement(se.namespaceURI, se.localName, se.qName, se.attributes);
}
/**
* 解析标签头
*/
private void startElement(String namespaceURI, String localName, String qName, Attributes atts) {
// localName有值取localName, 没值取qName. 拓展: 示例<a bb:ccc="123"/> 其中cc就是本地名称localName, bb:ccc就是限定名称qName
String tagName = getTagName(localName, qName);
// 使用elementPath, 类似栈的形式记录当前解析标签的层级. 例如解析到<configuration>内的<appender>标签, 则elementPath伪栈中有configuration和appender
elementPath.push(tagName);
// 解析标签头异常时使用skip跳过 异常解析的标签的内嵌标签
if (skip != null) {
// 添加空的action列表占位 (入栈占位)
pushEmptyActionList();
return;
}
// 重要代码: 获取适用的Action列表, 一般都是返回一个元素. 先从ruleStore找常规的Action, 找不到再到implicitActions中找
List<Action> applicableActionList = getApplicableActionList(elementPath, atts);
if (applicableActionList != null) {
// action事件列表进栈, 以便处理body和end事件时使用
actionListStack.add(applicableActionList);
// 核心代码: 执行action列表的begin事件
callBeginAction(applicableActionList, tagName, atts);
} else {
// 添加空的action列表占位 (入栈占位)
pushEmptyActionList();
String errMsg = "no applicable action for [" + tagName + "], current ElementPath is [" + elementPath + "]";
cai.addError(errMsg);
}
}
/**
* 执行action列表的begin事件
*/
void callBeginAction(List<Action> applicableActionList, String tagName, Attributes atts) {
if (applicableActionList == null) {
return;
}
// 遍历执行action的begin事件, 一般一个标签都是只有一个Action的. 这里源码仅看AppenderAction
Iterator<Action> i = applicableActionList.iterator();
while (i.hasNext()) {
Action action = (Action) i.next();
try {
// 核心代码
action.begin(interpretationContext, tagName, atts);
} catch (ActionException e) {
// 解析标签开始时就异常则记录当前标签路径, 防止其内嵌的标签继续解析
skip = elementPath.duplicate();
cai.addError("ActionException in Action for tag [" + tagName + "]", e);
} catch (RuntimeException e) {
// 解析标签开始时就异常则记录当前标签路径, 防止其内嵌的标签继续解析
skip = elementPath.duplicate();
cai.addError("RuntimeException in Action for tag [" + tagName + "]", e);
}
}
}
/**
* 解析标签体 (标签体就是 字符串)
*/
public void characters(BodyEvent be) {
// 记录当前解析 位于xml文件的位置
setDocumentLocator(be.locator);
String body = be.getText();
// 读出startElement时加入的栈顶Action列表. 注意, 这里仅读出
List<Action> applicableActionList = actionListStack.peek();
if (body != null) {
body = body.trim();
if (body.length() > 0) {
// 核心代码: 执行action列表的body事件
callBodyAction(applicableActionList, body);
}
}
}
/**
* 核心代码: 执行action列表的body事件
*/
private void callBodyAction(List<Action> applicableActionList, String body) {
if (applicableActionList == null) {
return;
}
// 遍历执行action的body事件, 一般一个标签都是只有一个Action的. 这里源码仅看ContextNameAction
Iterator<Action> i = applicableActionList.iterator();
while (i.hasNext()) {
Action action = i.next();
try {
// 核心代码
action.body(interpretationContext, body);
} catch (ActionException ae) {
cai.addError("Exception in end() methd for action [" + action + "]", ae);
}
}
}
/**
* 解析标签尾(入口)
*/
public void endElement(EndEvent endEvent) {
// 记录当前解析 位于xml文件的位置
setDocumentLocator(endEvent.locator);
endElement(endEvent.namespaceURI, endEvent.localName, endEvent.qName);
}
/**
* 解析标签尾
*/
private void endElement(String namespaceURI, String localName, String qName) {
// 弹出startElement时加入的栈顶Action列表
List<Action> applicableActionList = (List<Action>) actionListStack.pop();
// 若当前标签路径和skip中记录的解析异常标签头路径一致时, 则表示该标签闭环了(内嵌都遍历完了), 设置skip为空
if (skip != null) {
if (skip.equals(elementPath)) {
skip = null;
}
} else if (applicableActionList != EMPTY_LIST) {
// 核心代码: 执行action列表的end事件
callEndAction(applicableActionList, getTagName(localName, qName));
}
// 当前解析的标签路径出栈 (父层级还是保留的)
elementPath.pop();
}
/**
* 执行action列表的end事件
*/
private void callEndAction(List<Action> applicableActionList, String tagName) {
if (applicableActionList == null) {
return;
}
// 遍历执行action的end事件, 一般一个标签都是只有一个Action的. 这里源码仅看AppenderAction
Iterator<Action> i = applicableActionList.iterator();
while (i.hasNext()) {
Action action = i.next();
try {
// 核心代码
action.end(interpretationContext, tagName);
} catch (ActionException ae) {
cai.addError("ActionException in Action for tag [" + tagName + "]", ae);
} catch (RuntimeException e) {
cai.addError("RuntimeException in Action for tag [" + tagName + "]", e);
}
}
}
/**
* 到implicitActions中查找符合的Action. 有返回时仅返回一个元素
*/
List<Action> lookupImplicitAction(ElementPath elementPath, Attributes attributes, InterpretationContext ec) {
int len = implicitActions.size();
// 遍历列表, 其实列表就只有NestedComplexPropertyIA和NestedBasicPropertyIA
for (int i = 0; i < len; i++) {
ImplicitAction ia = (ImplicitAction) implicitActions.get(i);
// 若属性为简单类型 或 静态set和add方法属性则返回NestedBasicPropertyIA, 反之返回NestedComplexPropertyIA
// 这里的简单类型包含八大基本类型, Void, Enum, Charset, java.lang包下对象等
if (ia.isApplicable(elementPath, attributes, ec)) {
List<Action> actionList = new ArrayList<Action>(1);
actionList.add(ia);
return actionList;
}
}
return null;
}
/**
* 获取适用的Action列表
*/
List<Action> getApplicableActionList(ElementPath elementPath, Attributes attributes) {
// 通过当前解析标签的路径, 从ruleStore中获取匹配的Action列表
List<Action> applicableActionList = ruleStore.matchActions(elementPath);
// ruleStore找不到, 则到implicitActions中查找, 根据上级标签映射的对象和当前标签的名字查找. 这里返回的列表只有一个元素
if (applicableActionList == null) {
applicableActionList = lookupImplicitAction(elementPath, attributes, interpretationContext);
}
return applicableActionList;
}
}
InterpretationContext类主要源码
解析器上下文作为仅存储解析过程重要的信息, 可以理解为缓存工具类
public class InterpretationContext extends ContextAwareBase implements PropertyContainer {
// 对象栈, 存储记录解析过程中标签解析出来的对象, 完成该对象所有属性赋值则出栈
Stack<Object> objectStack;
// 记录临时的标签对象. 例如先解析出Appender, 则会暂存在map中, 键值对为 APPENDER_BAG -> {console=consoleAppender对象, ...}
Map<String, Object> objectMap;
// 记录解析出来的<property>标签的属性. 示例: log.path -> /logs/logback-study
Map<String, String> propertiesMap;
// 解析器对象
Interpreter joranInterpreter;
// 监听器列表, 用于执行inlay
final List<InPlayListener> listenerList = new ArrayList<InPlayListener>();
// 默认嵌套组件注册表. 指定嵌套组件如AppenderBase的layout, encoder以及SSl等属性的默认实现
DefaultNestedComponentRegistry defaultNestedComponentRegistry = new DefaultNestedComponentRegistry();
/**
* 添加property属性, 存在则覆盖
*/
public void addSubstitutionProperty(String key, String value) {
if (key == null || value == null) {
return;
}
value = value.trim();
propertiesMap.put(key, value);
}
/**
* 批量添加property属性
*/
public void addSubstitutionProperties(Properties props) {
if (props == null) {
return;
}
for (Object keyObject : props.keySet()) {
String key = (String) keyObject;
String val = props.getProperty(key);
addSubstitutionProperty(key, val);
}
}
/**
* 获取property属性值
*/
public String getProperty(String key) {
String v = propertiesMap.get(key);
if (v != null) {
return v;
} else {
return context.getProperty(key);
}
}
/**
* 从上下文, propertiesMap等中获取属性值
* 可以解析{}内的内容出来
*/
public String subst(String value) {
if (value == null) {
return null;
}
return OptionHelper.substVars(value, this, context);
}
public boolean isListenerListEmpty() {
return listenerList.isEmpty();
}
public void addInPlayListener(InPlayListener ipl) {
if (listenerList.contains(ipl)) {
addWarn("InPlayListener " + ipl + " has been already registered");
} else {
listenerList.add(ipl);
}
}
public boolean removeInPlayListener(InPlayListener ipl) {
return listenerList.remove(ipl);
}
void fireInPlay(SaxEvent event) {
for (InPlayListener ipl : listenerList) {
ipl.inPlay(event);
}
}
}
ContextNameAction类主要源码
public class ContextNameAction extends Action {
@Override
public void begin(InterpretationContext ec, String name, Attributes attributes) {
}
@Override
public void body(InterpretationContext ec, String body) {
// 解析出消息体内的内容. subst方法可以解析出{}内的值
String finalBody = ec.subst(body);
try {
// 设置LoggerContext的名字
context.setName(finalBody);
} catch (IllegalStateException e) {
}
}
@Override
public void end(InterpretationContext ec, String name) {
}
}
AppenderAction类主要源码
public class AppenderAction1<E> extends Action {
Appender<E> appender;
private boolean inError = false;
@Override
public void begin(InterpretationContext ec, String localName, Attributes attributes) throws ActionException {
appender = null;
inError = false;
// 获取<appender>的class属性
String className = attributes.getValue(CLASS_ATTRIBUTE);
if (OptionHelper.isEmpty(className)) {
inError = true;
return;
}
try {
// 反射创建Appender对象
appender = (Appender<E>) OptionHelper.instantiateByClassName(className, Appender.class, context);
appender.setContext(context);
// 获取<appender>的name属性并设置
String appenderName = ec.subst(attributes.getValue(NAME_ATTRIBUTE));
if (OptionHelper.isEmpty(appenderName)) {
} else {
appender.setName(appenderName);
}
// 将appender寄存到 InterpretationContext的objectMap中. 键值对为 APPENDER_BAG -> {console=consoleAppender对象, ...}
HashMap<String, Appender<E>> appenderBag = (HashMap<String, Appender<E>>) ec.getObjectMap().get(ActionConst.APPENDER_BAG);
appenderBag.put(appenderName, appender);
// 同时将appender对象压入objectStack栈中待后续处理内嵌标签时使用
ec.pushObject(appender);
} catch (Exception oops) {
inError = true;
throw new ActionException(oops);
}
}
/**
* 子元素都被解析之后,现在激活appender其他属性
*/
@Override
public void end(InterpretationContext ec, String name) {
if (inError) {
return;
}
if (appender instanceof LifeCycle) {
// 执行appender的start方法, 不展开
((LifeCycle) appender).start();
}
// 当前操作的appender和objectStack栈顶一致时, 弹出
Object o = ec.peekObject();
if (o != appender) {
} else {
ec.popObject();
}
}
}
NestedBasicPropertyIA类主要源码
public class NestedBasicPropertyIA extends ImplicitAction {
// 记录action的解析时需要使用到数据的栈.
// 其中IADataForBasicProperty包含当前action要解析的属性 的类型、名称、父类的bean构建的PropertySetter
Stack<IADataForBasicProperty> actionDataStack = new Stack<IADataForBasicProperty>();
// bean描述器的缓存器
private final BeanDescriptionCache beanDescriptionCache;
public NestedBasicPropertyIA1(BeanDescriptionCache beanDescriptionCache) {
this.beanDescriptionCache = beanDescriptionCache;
}
/**
* 判断NestedBasicPropertyIA是否适用
*/
@Override
public boolean isApplicable(ElementPath elementPath, Attributes attributes, InterpretationContext ec) {
// 获取当前标签的名字
String nestedElementTagName = elementPath.peekLast();
if (ec.isEmpty()) {
return false;
}
// 获取当前action要解析的属性 的父类bean, 构建PropertySetter对象, 用于判断属性参数类型和反射赋值
Object o = ec.peekObject();
PropertySetter parentBean = new PropertySetter(beanDescriptionCache, o);
parentBean.setContext(context);
// 获取当前action要解析的属性的类型 (不展开)
AggregationType aggregationType = parentBean.computeAggregationType(nestedElementTagName);
// NestedBasicPropertyIA仅支持处理 属性为简单类型的 或 静态set和add方法属性
// 这里的简单类型包含八大基本类型, Void, Enum, Charset, java.lang包下对象等
switch (aggregationType) {
case NOT_FOUND:
case AS_COMPLEX_PROPERTY:
case AS_COMPLEX_PROPERTY_COLLECTION:
return false;
case AS_BASIC_PROPERTY:
case AS_BASIC_PROPERTY_COLLECTION:
// 构建IADataForBasicProperty, 缓存到actionDataStack栈中, 供NestedBasicPropertyIA此Action执行start、body、end方法时使用
IADataForBasicProperty ad = new IADataForBasicProperty(parentBean, aggregationType, nestedElementTagName);
actionDataStack.push(ad);
return true;
default:
addError("PropertySetter.canContainComponent returned " + aggregationType);
return false;
}
}
@Override
public void begin(InterpretationContext ec, String localName, Attributes attributes) {
// NOP
}
public void body(InterpretationContext ec, String body) {
// 获取具体的属性值和actionData
String finalBody = ec.subst(body);
IADataForBasicProperty actionData = (IADataForBasicProperty) actionDataStack.peek();
switch (actionData.aggregationType) {
case AS_BASIC_PROPERTY:
// 根本进不来这个方法
actionData.parentBean.setProperty(actionData.propertyName, finalBody);
break;
case AS_BASIC_PROPERTY_COLLECTION:
// 只会执行该case. 反射的方式设置对象的属性值
actionData.parentBean.addBasicProperty(actionData.propertyName, finalBody);
break;
default:
addError("Unexpected aggregationType " + actionData.aggregationType);
}
}
public void end(InterpretationContext ec, String tagName) {
// 处理结束, 弹出缓存的action内的IADataForBasicProperty数据
actionDataStack.pop();
}
}
提一嘴NestedComplexPropertyIA
- NestedComplexPropertyIA的代码和NestedBasicPropertyIA类似
- NestedComplexPropertyIA多出的部分就是会为属性实例化和绑定部分值, 还有最重要的是在end方法中会执行属性的start方法, 这里就涉及到了一个很重要的(Appender的encoder重要属性layout初始化), 请看[Appender的encoder重要属性layout初始化源码]
补充: Appender的encoder重要属性layout初始化源码
layout初始化入口
public class NestedComplexPropertyIA extends ImplicitAction {
public void end(InterpretationContext ec, String tagName) {
IADataForComplexProperty actionData = (IADataForComplexProperty) actionDataStack.pop();
// ...
// 获取解析出来的属性对象. 这里假设属性对象为PatternLayoutEncoder实例
Object nestedComplexProperty = actionData.getNestedComplexProperty();
if (nestedComplexProperty instanceof LifeCycle && NoAutoStartUtil.notMarkedWithNoAutoStart(nestedComplexProperty)) {
// 执行PatternLayoutEncoder的start()方法
((LifeCycle) nestedComplexProperty).start();
}
Object o = ec.peekObject();
// ...
}
}
说明:
- 解析appender的encoder结束时, 会执行encoder的start()方法
- 未声明encoder的实现类时, 属于UnsynchronizedAppenderBase的Appender其encoder实现类都是PatternLayoutEncoder
未声明layout的实现类时, 属于UnsynchronizedAppenderBase的Appender其layout实现类都是PatternLayout(本文未涉及)- 下面进入PatternLayoutEncoder的start()方法
进入atternLayoutEncoder的start()方法
public class PatternLayoutEncoder extends PatternLayoutEncoderBase<ILoggingEvent> {
@Override
public void start() {
// 创建PatternLayout对象
PatternLayout patternLayout = new PatternLayout();
patternLayout.setContext(context);
patternLayout.setPattern(getPattern());
patternLayout.setOutputPatternAsHeader(outputPatternAsHeader);
// 初始化layout
patternLayout.start();
this.layout = patternLayout;
super.start();
}
}
进入patternLayout父类PatternLayoutBase的start()方法
abstract public class PatternLayoutBase<E> extends LayoutBase<E> {
// 转换器执行链. 用于日志输出时拼接出最终的日志内容
Converter<E> head;
// xml配置的日志模式
String pattern;
abstract public Map<String, String> getDefaultConverterMap();
public void start() {
if (pattern == null || pattern.length() == 0) {
addError("Empty or null pattern.");
return;
}
try {
// 由于此文篇幅太长, Parser类的代码不展开
// 初始化布局解析器, 使用状态机模式完成Parser的tokenList初始化(将pattern解析成一个个token对象). 不展开
Parser<E> p = new Parser<E>(pattern);
if (getContext() != null) {
p.setContext(getContext());
}
// 将Parser的tokenList解析成Node链表. 不展开
Node t = p.parse();
// 核心代码: 获取有效的关键字映射的转换器全限定类名, 使用Node链表 解析出 转换器链. compile方法不展开
this.head = p.compile(t, getEffectiveConverterMap());
if (postCompileProcessor != null) {
// 没有异常转换器则在转换器链尾添加一个
postCompileProcessor.process(context, head);
}
// 设置所有转换器上下文属性
ConverterUtil.setContextForConverters(getContext(), head);
// 设置所有转换器start属性为true
ConverterUtil.startConverters(this.head);
super.start();
} catch (ScanException sce) {
StatusManager sm = getContext().getStatusManager();
sm.add(new ErrorStatus("Failed to parse pattern \"" + getPattern() + "\".", this, sce));
}
}
public Map<String, String> getEffectiveConverterMap() {
Map<String, String> effectiveMap = new HashMap<String, String>();
// 添加默认的转换器实现映射 (主要来源)
Map<String, String> defaultMap = getDefaultConverterMap();
if (defaultMap != null) {
effectiveMap.putAll(defaultMap);
}
// 添加初始化过程中添加到LoggerContext的objectMap中key为PATTERN_RULE_REGISTRY的转换器实现映射
Context context = getContext();
if (context != null) {
@SuppressWarnings("unchecked")
Map<String, String> contextMap = (Map<String, String>) context.getObject(CoreConstants.PATTERN_RULE_REGISTRY);
if (contextMap != null) {
effectiveMap.putAll(contextMap);
}
}
// 添加实例的转换器实现映射. 例如使用了SyslogAppender, 就会添加syslogStart -> ch.qos.logback.classic.pattern.SyslogStartConverter
effectiveMap.putAll(instanceConverterMap);
return effectiveMap;
}
}
说明:
- 在start方法中, 会先实例化一个布局解析器Parser对象, 并使用交换机模式解析pattern属性设置到Parser的tokenList属性上
- 通过调用Parser的parse方法, 将tokenList属性解析成Node链表
- 获取有效的关键字 与 转换器全限定名的映射, 调用Parser的compile方法解析出 转换器执行链, 绑定到appender的encoder的layout上
- 其中getDefaultConverterMap()获取到了什么呢, 我们进入PatternLayout瞧瞧
进入PatternLayout的getDefaultConverterMap()方法
public class PatternLayout1 extends PatternLayoutBase<ILoggingEvent> {
// 默认的转换器实现映射. 关键字 -> 转换器实现全限定类名
public static final Map<String, String> DEFAULT_CONVERTER_MAP = new HashMap<String, String>();
// 转换器实现全限定类名 -> 关键字
public static final Map<String, String> CONVERTER_CLASS_TO_KEY_MAP = new HashMap<String, String>();
public Map<String, String> getDefaultConverterMap() {
return DEFAULT_CONVERTER_MAP;
}
// 详情配置可查看官方文档: https://logback.qos.ch/manual/layouts.html
static {
// 添加Parser中的两个. BARE -> ch.qos.logback.core.pattern.IdentityCompositeConverter, replace -> ch.qos.logback.core.pattern.ReplacingCompositeConverter
DEFAULT_CONVERTER_MAP.putAll(Parser.DEFAULT_COMPOSITE_CONVERTER_MAP);
// 输出时间. 使用示例: %d, %date{HH:mm:ss}. 默认时间格式: yyyy-MM-dd HH:mm:ss,SSS
DEFAULT_CONVERTER_MAP.put("d", DateConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("date", DateConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(DateConverter.class.getName(), "date");
// 输出当前输出日志时间 - 日志系统启动后的时间差. 示例示例: %r, %relative
DEFAULT_CONVERTER_MAP.put("r", RelativeTimeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("relative", RelativeTimeConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(RelativeTimeConverter.class.getName(), "relative");
// 输出日志等级. 示例: %level, %5le %-5level
DEFAULT_CONVERTER_MAP.put("level", LevelConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("le", LevelConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("p", LevelConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(LevelConverter.class.getName(), "level");
// 输出日志所在的线程. 示例: %t, %thread
DEFAULT_CONVERTER_MAP.put("t", ThreadConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("thread", ThreadConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(ThreadConverter.class.getName(), "thread");
// 输出logger名字. 示例: %lo, %logger, %c{36}
DEFAULT_CONVERTER_MAP.put("lo", LoggerConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("logger", LoggerConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("c", LoggerConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(LoggerConverter.class.getName(), "logger");
// 输出用户添加的原日志内容. 示例: %m, %msg, %message
DEFAULT_CONVERTER_MAP.put("m", MessageConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("msg", MessageConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("message", MessageConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(MessageConverter.class.getName(), "message");
// 输出发出日志请求的类的全限定名称. 示例: %C, %class{20}
DEFAULT_CONVERTER_MAP.put("C", ClassOfCallerConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("class", ClassOfCallerConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(ClassOfCallerConverter.class.getName(), "class");
// 输出发出日志请求的方法名. 示例: %M, %method
DEFAULT_CONVERTER_MAP.put("M", MethodOfCallerConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("method", MethodOfCallerConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(MethodOfCallerConverter.class.getName(), "method");
// 输出发出日志请求所在的行号. 示例: %L, %line
DEFAULT_CONVERTER_MAP.put("L", LineOfCallerConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("line", LineOfCallerConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(LineOfCallerConverter.class.getName(), "line");
// 输出发出日志请求的 Java 源文件名. 示例: %F, %file
DEFAULT_CONVERTER_MAP.put("F", FileOfCallerConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("file", FileOfCallerConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(FileOfCallerConverter.class.getName(), "file");
// 输出映射调试上下文信息, 常用于日志追踪. 示例: %X{traceId}, %mdc{traceId:-默认值}. mdc拓展: https://blog.youkuaiyun.com/Erica_java/article/details/128616137
DEFAULT_CONVERTER_MAP.put("X", MDCConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("mdc", MDCConverter.class.getName());
// 输出与日志记录事件关联的异常的堆栈跟踪(如果有的话). 示例: %ex, %exception{2}
DEFAULT_CONVERTER_MAP.put("ex", ThrowableProxyConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("exception", ThrowableProxyConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("rEx", RootCauseFirstThrowableProxyConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("rootException", RootCauseFirstThrowableProxyConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("throwable", ThrowableProxyConverter.class.getName());
// 输出与日志记录事件关联的异常的堆栈跟踪(如果有的话)和类封装信息. 示例: %xEx, %xException{2}
DEFAULT_CONVERTER_MAP.put("xEx", ExtendedThrowableProxyConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("xException", ExtendedThrowableProxyConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("xThrowable", ExtendedThrowableProxyConverter.class.getName());
// 此转换器不输出任何数据(?多余的东西). 示例: %nopex, %nopexception
DEFAULT_CONVERTER_MAP.put("nopex", NopThrowableInformationConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("nopexception", NopThrowableInformationConverter.class.getName());
// 输出logger上下文名称. 示例: %cn, %contextName
DEFAULT_CONVERTER_MAP.put("cn", ContextNameConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("contextName", ContextNameConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(ContextNameConverter.class.getName(), "contextName");
// 输出生成日志的调用者所在的位置信息. 示例: %caller, %caller{2}
DEFAULT_CONVERTER_MAP.put("caller", CallerDataConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(CallerDataConverter.class.getName(), "caller");
// 输出与记录器请求关联的标记. 示例: %marker
DEFAULT_CONVERTER_MAP.put("marker", MarkerConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(MarkerConverter.class.getName(), "marker");
// 输出属性 key 所对应的值, 先从日志记录器上下文找, 找不到再从系统属性中找. 示例: %property{user.name}
DEFAULT_CONVERTER_MAP.put("property", PropertyConverter.class.getName());
// 输出当前平台的换行符. 示例: %n
DEFAULT_CONVERTER_MAP.put("n", LineSeparatorConverter.class.getName());
// 以给定的颜色样式输出内容. 示例: %yellow(%d), %yellow(中国人不打中国人)
DEFAULT_CONVERTER_MAP.put("black", BlackCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("red", RedCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("green", GreenCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("yellow", YellowCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("blue", BlueCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("magenta", MagentaCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("cyan", CyanCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("white", WhiteCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("gray", GrayCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("boldRed", BoldRedCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("boldGreen", BoldGreenCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("boldYellow", BoldYellowCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("boldBlue", BoldBlueCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("boldMagenta", BoldMagentaCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("boldCyan", BoldCyanCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("boldWhite", BoldWhiteCompositeConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("highlight", HighlightingCompositeConverter.class.getName());
// 输出自增序列, 初始值为该转换器实例化时的时间戳. 示例: %lsn
DEFAULT_CONVERTER_MAP.put("lsn", LocalSequenceNumberConverter.class.getName());
CONVERTER_CLASS_TO_KEY_MAP.put(LocalSequenceNumberConverter.class.getName(), "lsn");
// 对于%prefix括号包含的所有子转换器,在每个转换器的输出前加上转换器的名称. 示例: %prefix(%d)
DEFAULT_CONVERTER_MAP.put("prefix", PrefixCompositeConverter.class.getName());
}
}
说明:
- 通过源码可见, 指定了一个个关键字的转换器实现类, 这里列出了各个转换器的使用和含义
- 若想进一步了解每个转换器如何使用, 读者们可以自行查看源码或查看官方文档(https://logback.qos.ch/manual/layouts.html)了解使用
补充: 默认配置BasicConfigurator类
public class BasicConfigurator extends ContextAwareBase implements Configurator {
public BasicConfigurator() {
}
public void configure(LoggerContext lc) {
addInfo("Setting up default configuration.");
// 初始化一个Appender -> ConsoleAppender
ConsoleAppender<ILoggingEvent> ca = new ConsoleAppender<ILoggingEvent>();
ca.setContext(lc);
ca.setName("console");
// 初始化encoder对象
LayoutWrappingEncoder<ILoggingEvent> encoder = new LayoutWrappingEncoder<ILoggingEvent>();
encoder.setContext(lc);
// 设置encoder的布局layout属性
// TTLLLayout 类似于使用PatternLayout(其中pattern = %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n)
TTLLLayout layout = new TTLLLayout();
layout.setContext(lc);
layout.start();
encoder.setLayout(layout);
// 设置ConsoleAppender的encoder
ca.setEncoder(encoder);
ca.start();
// 初始化根logger并设置appender
Logger rootLogger = lc.getLogger(Logger.ROOT_LOGGER_NAME);
rootLogger.addAppender(ca);
}
}
补充: BasicConfigurator 的配置等价于下面的xml配置
<configuration>
<!-- 模拟logback默认配置 ch.qos.logback.classic.BasicConfigurator -->
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="ch.qos.logback.classic.layout.TTLLLayout"/>
</encoder>
</appender>
<!--系统操作日志-->
<root level="debug">
<appender-ref ref="console" />
</root>
</configuration>
进入LoggerContext的getLogger(String name)方法
前置说明:
- logger是链式结构, 所有的非根logger其最顶的祖先都是根logger
- 每个logger都有一个属性childrenList, 维护其下的所有直属子logger
- 每个logger都有一个AppenderAttachableImpl aai属性, 维护其绑定的appender列表
- 以上关系较为简单, 就不画类图展示了, 我们直接看源码
入口说明:
- 在LoggerFactory中调用了getILoggerFactory()方法
- 初始化成功后, 会返回StaticLoggerBinder.getSingleton().getLoggerFactory(), 这里返回的就是LoggerContext
- 然后iLoggerFactory.getLogger(name)即LoggerContext的getLogger(String name)方法
public class LoggerContext extends ContextBase implements ILoggerFactory, LifeCycle {
// 根logger
final Logger root;
// logger总数
private int size;
// logger缓存
private Map<String, Logger> loggerCache;
public LoggerContext() {
//...
this.root = new Logger(Logger.ROOT_LOGGER_NAME, null, this);
this.root.setLevel(Level.DEBUG);
//...
}
@Override
public final Logger getLogger(final String name) {
if (name == null) {
throw new IllegalArgumentException("name argument cannot be null");
}
// 如果获取的是根logger则直接返回. 根logger在LoggerContext构造器中就创建出来(name=ROOT, level=DEBUG)
if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) {
return root;
}
// 缓存有则从缓存中获取
Logger childLogger = (Logger) loggerCache.get(name);
if (childLogger != null) {
return childLogger;
}
// 根据入参name获取logger. 示例name=com.chenlongji.Test, 假设初始化时只存在一个根root
// 则这里会先按"."切割name, 依次得到com, com.chenlongji, com.chenlongji.Test
// 这里会先创建出name为com的logger, 作为root的子logger.
// 再创建name为com.chenlongji的logger, 作为logger(com)的子logger, 直到创建出目标logger(com.chenlongji.Test)
int i = 0;
Logger logger = root;
String childName;
while (true) {
// 按照"."切割name(示例: name=com.chenlongji.Test). 循环中依次获得com, com.chenlongji, com.chenlongji.Test
int h = LoggerNameUtil.getSeparatorIndexOf(name, i);
if (h == -1) {
childName = name;
} else {
childName = name.substring(0, h);
}
// 查找"."的索引右移
i = h + 1;
synchronized (logger) {
// 根据childName获取当前节点的子节点(不展开), 没有则创建
childLogger = logger.getChildByName(childName);
if (childLogger == null) {
// 根据名字创建出新的节点, 并写入缓存
childLogger = logger.createChildByName(childName);
loggerCache.put(childName, childLogger);
// 累计上下文中logger的个数
incSize();
}
}
// 将子logger切换为当前循环引用对象, 继续找
logger = childLogger;
// 已经创建出或查找到目标logger, 返回
if (h == -1) {
return childLogger;
}
}
}
private void incSize() {
size++;
}
}
说明:
- 逻辑较简单, 直接看注释
进入Logger的createChildByName(final String childName)方法
Logger createChildByName(final String childName) {
// 判断当前logger的name 和 要创建logger的name之间不能存在多余的"."
// 例如当前logger(com), 要创建的logger(com.Test), 从Test的T开始往后是没有多余的"."了
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<Logger1>();
}
// 创建子节点, 并绑定到childrenList上
Logger childLogger = new Logger(childName, this, this.loggerContext);
childrenList.add(childLogger);
// 子类继承父类的有效level等级
childLogger.effectiveLevelInt = this.effectiveLevelInt;
return childLogger;
}
说明:
- 逻辑较简单, 直接看注释
四. 输出日志源码
入口
前置说明
- 这里仅从info方法开始看源码, 其他方法也是一样的
public class Test {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(Test1.class);
// 入口
logger.info("我是error级别的日志");
}
}
进入Logger的info(String msg)方法
public void info(String msg) {
// 过滤和输出日志
filterAndLog_0_Or3Plus(FQCN, null, Level.INFO, msg, null, null);
}
进入Logger的filterAndLog_0_Or3Plus(…)方法
/**
* 了解: 时间优化到极致的体现
* filterAndLog有三个方法:
* filterAndLog_0_Or3Plus
* filterAndLog_1
* filterAndLog_2
* 其中filterAndLog_1和filterAndLog_2中创建了一个Object[]新对象, filterAndLog_0_Or3Plus中不需要创建该对象
* 节省Object[]对象的创建可以节省约20纳秒
*/
private void filterAndLog_0_Or3Plus(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params,
final Throwable t) {
// 判断是否可以通过上下文的过滤器. logback初始化完成后该方法都是返回NEUTRAL, 不展开
final FilterReply decision = loggerContext.getTurboFilterChainDecision_0_3OrMore(marker, this, level, msg, params, t);
if (decision == FilterReply.NEUTRAL) {
// logback初始化完成后都会进入该位置. 这里判断当前logger的level是否 大于 输出日志的level, 大于则结束
if (effectiveLevelInt > level.levelInt) {
return;
}
} else if (decision == FilterReply.DENY) {
return;
}
// 核心代码: 构建LoggingEvent并输出日志
buildLoggingEventAndAppend(localFQCN, marker, level, msg, params, t);
}
进入Logger的buildLoggingEventAndAppend(…)方法
private void buildLoggingEventAndAppend(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params,
final Throwable t) {
// 构建LoggingEvent对象, 该对象包含了日志内容、日志级别、当前logger对象、线程名称等信息
LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params);
// 设置标记信息(较少使用)
le.setMarker(marker);
// 核心代码: 调用appender执行日志输出
callAppenders(le);
}
进入Logger的callAppenders(ILoggingEvent event)方法
public void callAppenders(ILoggingEvent event) {
int writes = 0;
// 若没有提前中断, 则递归当前logger的执行链, 一直到root
for (Logger1 l = this; l != null; l = l.parent) {
// 核心代码: 获取当前logger下的所有appender, 遍历输出日志
writes += l.appendLoopOnAppenders(event);
// 若当前logger的additive属性为false, 则不再调用父类logger输出日志
if (!l.additive) {
break;
}
}
// 递归的logger都没找到一个appender, 则添加警告信息(系统启动后仅能添加一次). 输出警告信息需要配置configuration下的statusListener
if (writes == 0) {
loggerContext.noAppenderDefinedWarning(this);
}
}
进入AppenderAttachableImpl的appendLoopOnAppenders(E e)方法
public int appendLoopOnAppenders(E e) {
int size = 0;
// 从appenderList拷贝出appender数组, 通过fresh原子属性控制安全性, 感兴趣的可以看下, 这里不展开
final Appender<E>[] appenderArray = appenderList.asTypedArray();
final int len = appenderArray.length;
for (int i = 0; i < len; i++) {
// 核心代码: 遍历执行一个个appender的doAppend方法
// 住: 这里我们仅看下RollingFileAppender是如何处理的
appenderArray[i].doAppend(e);
size++;
}
// 返回当前logger的appender个数
return size;
}
进入UnsynchronizedAppenderBase的doAppend(E eventObject)方法
前置说明:
- 这里我们仅看RollingFileAppender的处理逻辑
- appender相关的类图请看[Appender类图]
- 异步的日志输出器AsyncAppender功能请看[Appender的UnsynchronizedAppenderBase的实现类]简单描述
public void doAppend(E eventObject) {
// 使用ThreadLocal确保当前doAppend防重入 (不改源码正常调用其实不会出现重入问题)
if (Boolean.TRUE.equals(guard.get())) {
return;
}
try {
// 设置为true防重入
guard.set(Boolean.TRUE);
// 确保当前appender已完成初始化
if (!this.started) {
if (statusRepeatCount++ < ALLOWED_REPEATS) {
addStatus(new WarnStatus("Attempted to append to non started appender [" + name + "].", this));
}
return;
}
// 获取当前appender的所有过滤器, 判断是否可以通过. 较简单不展开
if (getFilterChainDecision(eventObject) == FilterReply.DENY) {
return;
}
// 核心代码: 执行日志输出
this.append(eventObject);
} catch (Exception e) {
if (exceptionCount++ < ALLOWED_REPEATS) {
addError("Appender [" + name + "] failed to append.", e);
}
} finally {
// 重置为false, 允许进入
guard.set(Boolean.FALSE);
}
}
进入OutputStreamAppender的append(E eventObject)方法
protected void append(E eventObject) {
// 确保当前appender已完成初始化 (因为其他线程使用该appender出错时start属性就变成了false)
if (!isStarted()) {
return;
}
// 核心代码: 执行日志输出
subAppend(eventObject);
}
进入RollingFileAppender的subAppend(E event)方法
protected void subAppend(E event) {
// 同步代码块确保判断和滚动时线程安全
synchronized (triggeringPolicy) {
// 重要代码: 判断是否达到滚动时机
if (triggeringPolicy.isTriggeringEvent(currentlyActiveFile, event)) {
// 重要代码: 滚动文件
rollover();
}
}
// 核心代码: 执行父类代码进行日志输出
super.subAppend(event);
}
注: 假设指定了rollingPolicy为TimeBasedRollingPolicy类型, 没有指定triggeringPolicy, 则RollingFileAppender下主要属性关系如下图
进入TimeBasedRollingPolicy的isTriggeringEvent(File, E)方法
public class TimeBasedRollingPolicy<E> extends RollingPolicyBase implements TriggeringPolicy<E> {
TimeBasedFileNamingAndTriggeringPolicy<E> timeBasedFileNamingAndTriggeringPolicy;
public void start() {
//...
if (timeBasedFileNamingAndTriggeringPolicy == null) {
// 指定TimeBasedFileNamingAndTriggeringPolicy的实现类
timeBasedFileNamingAndTriggeringPolicy = new DefaultTimeBasedFileNamingAndTriggeringPolicy<E>();
}
timeBasedFileNamingAndTriggeringPolicy.setContext(context);
// 缓存了TimeBasedRollingPolicy到其tbrp字段上
timeBasedFileNamingAndTriggeringPolicy.setTimeBasedRollingPolicy(this);
timeBasedFileNamingAndTriggeringPolicy.start();
//...
}
public boolean isTriggeringEvent(File activeFile, final E event) {
// 调用DefaultTimeBasedFileNamingAndTriggeringPolicy的isTriggeringEvent(File, E)方法
return timeBasedFileNamingAndTriggeringPolicy.isTriggeringEvent(activeFile, event);
}
}
进入DefaultTimeBasedFileNamingAndTriggeringPolicy的isTriggeringEvent(File, E)方法
public boolean isTriggeringEvent(File activeFile, final E event) {
long time = getCurrentTime();
// 判断时间是否超过 上次算出来的下次滚动时间. 这里是根据输入的滚动文件时间格式获取滚动时间间隔, 逻辑与log4j类似, 这里不展开
// 获取时间间隔单位逻辑(RollingCalendar.computePeriodicityType):
// 遍历的时间单位从小到大判断(毫秒->秒...->月)
// 取一个1970年0点时间epoch, 按照配置的滚动日期格式格式化得到ro,
// epoch加上遍历单位1个单位的值, 再按照配置的滚动日期格式格式化得到r1,
// 判断两个日期是否相等, 如果不相等, 则返回当前遍历的时间单位 (类似于整数舍弃余数的方式 判断累加数是否会使整数值变化)
if (time >= nextCheck) {
// 获取上次的进入isTriggeringEvent方法获取的时间戳time
Date dateOfElapsedPeriod = dateInCurrentPeriod;
addInfo("Elapsed period: " + dateOfElapsedPeriod);
// 获取需要滚动文件的替换文件名. 这里涉及到了转换器Converter的初始化和使用, 本文讲述layout的地方也讲到, 故不展开
elapsedPeriodsFileName = tbrp.fileNamePatternWithoutCompSuffix.convert(dateOfElapsedPeriod);
// 刷新dateInCurrentPeriod=time, 不展开
setDateInCurrentPeriod(time);
// 刷新下次滚动时间 nextCheck, 不展开
computeNextCheck();
return true;
} else {
return false;
}
}
进入RollingFileAppender的rollover()方法
public void rollover() {
// 上锁. 关流和打开文件必须在同一个代码块内, 不关流(打开的文件)无法完成重命名
lock.lock();
try {
// 关闭输出流. 不展开
this.closeOutputStream();
// 核心代码: 尝试滚动文件
attemptRollover();
// 打开新文件, 设置输出流. 不展开
attemptOpenFile();
} finally {
lock.unlock();
}
}
进入RollingFileAppender的attemptRollover()方法
private void attemptRollover() {
try {
// 滚动
rollingPolicy.rollover();
} catch (RolloverFailure rf) {
addWarn("RolloverFailure occurred. Deferring roll-over.");
this.append = true;
}
}
进入TimeBasedRollingPolicy的rollover()方法
public void rollover() throws RolloverFailure {
// 取出需要滚动文件的替换文件名, 在前面isTriggeringEvent方法已经得到该名字了
String elapsedPeriodsFileName = timeBasedFileNamingAndTriggeringPolicy.getElapsedPeriodsFileName();
// 获取elapsedPeriodsFileName中最后"/"后的文件名
String elapsedPeriodStem = FileFilterUtil.afterLastSlash(elapsedPeriodsFileName);
// 配置的滚动文件名没有gz. zip后缀, 则不需要压缩
if (compressionMode == CompressionMode.NONE) {
// getParentsRawFileProperty()获取的是<file>标签体内的文件名
if (getParentsRawFileProperty() != null) {
// 直接修改文件名. 注意: 系统禁止的文件名符号重命名会失败, 例如window系统下":"
renameUtil.rename(getParentsRawFileProperty(), elapsedPeriodsFileName);
}
} else {
if (getParentsRawFileProperty() == null) {
// 使用压缩器进行文件压缩. 不展开
// 这里使用日志上下文中的线程池异步进行文件压缩
// gz包使用java的GZIPOutputStream压缩, zip包使用java的ZipOutputStream压缩
compressionFuture = compressor.asyncCompress(elapsedPeriodsFileName, elapsedPeriodsFileName, elapsedPeriodStem);
} else {
// 先将滚动文件改为临时文件名, 再进行压缩(执行compressor.asyncCompress), 不展开
compressionFuture = renameRawAndAsyncCompress(elapsedPeriodsFileName, elapsedPeriodStem);
}
}
if (archiveRemover != null) {
Date now = new Date(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
// 使用归档删除器将过期的文件删除掉, future用于appender销毁时阻塞线程. 不展开
// 这里会使用日志上下文中的线程池异步进行文件删除.
// 删除过程会调用clean方法删除超出时间范围内的文件(文件保留数为maxHistory+1), capTotalCap会删除超过总文件大小的旧文件(需配置maxHistory和totalSizeCap才有效)
this.cleanUpFuture = archiveRemover.cleanAsynchronously(now);
}
}
说明:
- compressor和archiveRemover都是使用异步方式执行, 感兴趣的可自行看下这部分代码, 篇幅问题不再展开
回到OutputStreamAppender的subAppend(E event)方法
protected void subAppend(E event) {
// 确保当前appender已完成初始化(因为其他线程使用该appender出错时start属性就变成了false)
if (!isStarted()) {
return;
}
try {
// 设置LoggerEvent的内容, 线程名, 和mdc属性值. 不展开
if (event instanceof DeferredProcessingAware) {
((DeferredProcessingAware) event).prepareForDeferredProcessing();
}
// 使用编码器的解析出最终的日志内容. 这里由于每个转换器都保证了线程安全, 故该方法不用上锁
// 注: 后续假设Encoder的实现类为PatternLayoutEncoder
byte[] byteArray = this.encoder.encode(event);
// 使用流输出日志信息. 为避免多线程出现写入写出, 该方法需要使用同步锁
writeBytes(byteArray);
} catch (IOException ioe) {
// 异常则该appender就关闭掉了
this.started = false;
addStatus(new ErrorStatus("IO failure in appender", this, ioe));
}
}
进入LayoutWrappingEncoder的encode(E event)方法
前置说明:
- 假设Encoder的实现类为PatternLayoutEncoder (UnsynchronizedAppenderBase下appender的encoder的默认实现类)
public byte[] encode(E event) {
// 使用layout解析出最终的日志内容
String txt = layout.doLayout(event);
// 将最终的日志内容转为byte数组返回. 不展开
return convertToBytes(txt);
}
进入PatternLayout的doLayout(ILoggingEvent event)方法
前置说明:
- 假设Layout的实现类为PatternLayout(UnsynchronizedAppenderBase下appender的encoder的layout的默认实现类)
public String doLayout(ILoggingEvent event) {
// 确保layout已启动
if (!isStarted()) {
return CoreConstants.EMPTY_STRING;
}
// 使用转化器链获取实际日志内容
return writeLoopOnConverters(event);
}
进入PatternLayoutBase的writeLoopOnConverters(E event)方法
protected String writeLoopOnConverters(E event) {
StringBuilder strBuilder = new StringBuilder(INTIAL_STRING_BUILDER_SIZE);
// 遍历转换器执行链解析日志内容
Converter<E> c = head;
while (c != null) {
// 使用该转换器 获取该转换器 转换出来的部分内容, 加入strBuilder中. 下面我们找一个DateConverter看下源码
c.write(strBuilder, event);
c = c.getNext();
}
return strBuilder.toString();
}
进入DateConverter父类FormattingConverter的write(StringBuilder buf, E event)方法
前置说明:
- 后面的Converter示例都是使用DateConverter来演示
final public void write(StringBuilder buf, E event) {
// 执行子类convert方法获取内容
String s = convert(event);
// 根据配置的格式格式化内容
// FormatInfo属性
// min: 小数点前的正整数, 表示最小长度. min前面的"-"表示leftPad取反, 即左对齐
// max: 小数点后的正整数, 表示最大长度. max前面的"-"表示leftTruncate取反, 即截掉右边, 保留左边(长度超出时)
// leftPad: 左边加空格, 即右对齐
// leftTruncate: 截掉左边, 保留右边(长度超出时)
// 示例说明:
// %-2.5method表示输出的方法名 最小长度为2, 最大长度为5, 左对齐(小于最小长度右边补空格), 超出长度截掉左边, 保留右边
// "s"方法 -> "s "
// "soLongMethodNameIsMy"方法 -> "eIsMy"
// %2.-5method表示输出的方法名 最小长度为2, 最大长度为3, 右对齐(小于最小长度左边补空格), 超出长度截掉右边, 保留左边
// "s"方法 -> " s"
// "soLongMethodNameIsMy"方法 -> "soLon"
if (formattingInfo == null) {
// 没有配置格式化信息, 直接返回
buf.append(s);
return;
}
// 小数点左边的数字 (示例%-2.5level中的 2)
int min = formattingInfo.getMin();
// 小数点右边的数字 (示例%-2.5level中的 5)
int max = formattingInfo.getMax();
if (s == null) {
if (0 < min) {
// 补充min个空格. 不展开
SpacePadder.spacePad(buf, min);
}
return;
}
int len = s.length();
// max控制最大长度. 超出长度则截取
if (len > max) {
if (formattingInfo.isLeftTruncate()) {
// 截掉左边, 保留右边
buf.append(s.substring(len - max));
} else {
// 截掉右边, 保留左边
buf.append(s.substring(0, max));
}
// min控制最小长度. 小于最小长度则添加空格
} else if (len < min) {
if (formattingInfo.isLeftPad()) {
// 右对齐, 则左边加空格. 不展开
SpacePadder.leftPad(buf, s, min);
} else {
// 左对齐, 则右边加空格. 不展开
SpacePadder.rightPad(buf, s, min);
}
} else {
buf.append(s);
}
}
说明:
- 格式化示例说明
[%-2.5method]: 输出方法名, 长度在[2, 5]范围内, 长度小于2则右边补空格, 长度超过5则截左边留右边
[s] -> [s ]
[abcdef] -> [bcdef]
[%2.-5method]: 输出方法名, 长度在[2, 5]范围内, 长度小于2则左边补空格, 长度超过5则截右边留左边
[s] -> [ s]
[abcdef] -> [abcde]
进入DateConverter的convert(ILoggingEvent le)方法
public String convert(ILoggingEvent le) {
long timestamp = le.getTimeStamp();
// 使用之前就设置好的DateFormatter格式化时间
return cachingDateFormatter.format(timestamp);
}
进入OutputStreamAppender的writeBytes(byte[] byteArray)方法
private void writeBytes(byte[] byteArray) throws IOException {
if(byteArray == null || byteArray.length == 0) {
return;
}
// 上锁, 避免多线程写入写出时出现错误
lock.lock();
try {
// 写入输出流中
this.outputStream.write(byteArray);
// 若immediateFlush=true, 立即写出. 该属性默认值为true
if (immediateFlush) {
this.outputStream.flush();
}
} finally {
lock.unlock();
}
}
五. 拓展
1. SpringBoot中logback的初始化流程
见另一篇文章: SpringBoot中logback的初始化流程(待出)
2. 为什么使用lombok的@Slf4j注解后就可以直接使用log对象打印日志
简而言之, 就是贴有@Slf4j注解的在编译是会为该类生产一个成员变量log
本文不讲具体内容, 刚兴趣请查看为什么加了@slf4j注解 就可以直接使用log:
六. 附件
1. SpringBoot环境下logback-spring.xml配置文件一般配置
<?xml version="1.0" encoding="UTF-8"?>
<!-- 配置详情: https://blog.youkuaiyun.com/weixin_41377777/article/details/120962037 -->
<!-- 拷贝即可使用: 额外需要关注的就三个点, 1.配置spring.application.name可指定文件前缀名 2.修改dev的springProfile中自定义logger的name指定当前项目 3.修改rollingPolicy的MaxHistory确定日志保留数 -->
<configuration>
<property name="LOG_PATH" value="/apps/logs" />
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %highlight(%level) [%C{36}.%M:%line] tid=%X{traceId}, %msg%n" />
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="defaultAppName"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!--本地环境 -->
<springProfile name="dev">
<logger name="com.chenlongji" level="DEBUG" />
<!--<logger name="org.mybatis" level="DEBUG" />-->
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</springProfile>
<!--测试环境 -->
<springProfile name="test">
<!-- 备注: 若springProfile仅激活一个, 那appender的name重复也不影响, 因为没激活的profile里面的appender不会进行初始化 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- layout使用encoder配置应该也行, 如CONSOLE的配置 -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>${LOG_PATTERN}</pattern>
</layout>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${LOG_PATH}/${APP_NAME}.%d{yyyy-MM-dd}.log</FileNamePattern>
<MaxHistory>1</MaxHistory>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- 过滤的级别: info及以上 -->
<level>INFO</level>
</filter>
</appender>
<root level="INFO">
<appender-ref ref="FILE" />
</root>
</springProfile>
<!--生产环境 -->
<springProfile name="prd">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>${LOG_PATTERN}</pattern>
</layout>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${LOG_PATH}/${APP_NAME}.%d{yyyy-MM-dd}.log</FileNamePattern>
<MaxHistory>7</MaxHistory>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- 过滤的级别: info及以上 -->
<level>INFO</level>
</filter>
</appender>
<root level="INFO">
<appender-ref ref="FILE" />
</root>
</springProfile>
</configuration>