Log4j2 源码浅析
[TOC]
介绍Log4j2的原理之前,回顾下之前配置文件中提到的几个概念:Configuration、LoggerConfig、Logger、Appender、Layout、Filter。Log4j2的官网上给出了类图,能清晰地看清楚它们之间的关联。
从图中可以看出LoggerContext、Configuration是一对一的关系,在一般的应用中通常也都只存在一个实例(Share one ClassLoader(Standalone Application、Web Application、Java EE Applications、”Shared” Web Applications and REST Service Containers),Not OSGi Applications Logging Separation)。
- LoggerContext: 整个日志系统的锚点(承载全局日志上下文信息),类似于Spring的WebApplicationContext
- Configuration: 全局配置信息,每个LoggerContext对应一个Configuration
- Appender: 追加器,对应配置文件中的
<Appenders>
下定义的Appender,定义日志输出位置以及日志输出格式,。用户可以自定义Appender,只需继承AbstractAppender并实现append(LogEvent)方法。- Layout: 定义日志的输出格式
- Filter: 过滤器,在整个日志系统的类图中,多个地方应用。起到一个过滤的作用,用于过滤特定日志级别的日志事件。有多个地方引用
Filter
:日志事件进入LoggerConfig之前;进入LoggerConfig后调用任何Appender之前;进入LoggerConfig后调用特殊的Appender之前;每个Appender内部。 - LoggerConfig: 真正的日志操作实体,对应配置文件中的
<Logger>
下定义的Logger。含有全局唯一标识(名称),一般对应的是一个包目录名称。 - Logger: 壳,每个Logger内部都对应一个LoggerConfig(通过名称来对应)。对日志事件的操作,都交由其对应的LoggerConfig进行处理
- 在使用日志系统前,首先需要初始化日志系统:解析日志配置信息
Configuration
,建立日志上下文LoggerContext
(属性、Appender、LogerConfig…) - 给定name获取日志对象Logger时,
LoggerContext
结合上下文信息,返回一个绑定了特定名称的LoggerConfig
的Loger
- 在执行日志操作时,会经过多层过滤(Filter),并且真正的日志操作由LoggerConfig来处理。
因此下面,将从这三方面来分析日志系统。1. 日志系统的初始化;2.获取日志对象;3.日志操作过程。其中,日志系统的初始化较其他两部分稍复杂。
1. 日志系统的初始化过程,获取LoggerContext
初始化的过程关键是 加载并解析配置文件,建立日志上下文。
初始化过程的时序图:
当应用首次调动LogManager.getLogger
方法时,触发日志系统的初始化:
1 | public static Logger getLogger(final Class<?> clazz) { |
getContext
方法是建立日志系统上下文的起点。LogManager在类初始化时,会初始化一个生成LogerContext工厂,用户可以指定这个工厂,默认情况下为Log4jContextFactory。下文我们以Log4jContextFactory为主线来分析,其他情况类似。
1 | public static LoggerContext getContext(final ClassLoader loader, final boolean currentContext) { |
在初始化Log4jContextFactory工厂对象时,同时会实例化一个实现ContextSelector
(用于定位LogerContext
)接口的日志上下文选择器。同样,用户可以指定这个选择器(属性配置:Log4jContextSelector
);默认为ClassLoaderContextSelector
(前面一节提到的全异步化是将ClassLoaderContextSelector
设置为AsyncLoggerContextSelector
)。下文同样以ClassLoaderContextSelector
为例。
进一步,Log4jContextFactory
会调动ClassLoaderContextSelector
的getContext
方法来获取LoggerContext
,然后调用LoggerContext的start方法进行日志系统的初始化(核心部分):
1 | public LoggerContext getContext(final String fqcn, final ClassLoader loader, final Object externalContext,final boolean currentContext) { |
可以看出,首先会拿ClassLoader去缓存中拿,如果有则直接返回,没有则创建一个。
因为是根据ClassLoader来进行缓存LoggerContext,所以对于同一个应用来说其所有的日志对象对应同一个LoggerContext。
1 | private LoggerContext locateContext(final ClassLoader loaderOrNull, final URI configLocation) { |
LoggerContext的初始化方法start
会间接调用reconfigure(URI) (start() -> reconfigure() -> reconfigure(URI)),其是加载并解析配置文件的核心内容
1 | private void reconfigure(final URI configURI) { |
其主要内容如下:
- 首先获取所有可用的配置工厂(ConfigurationFactory),然后根据应用中有的的配置文件类型(yml|json|xml)来适配一种配置对象生成工厂,通过工厂对象生成相应的配置对象。下面将以XMLConfigurationFactory为例,来介绍XMLConfiguration的生成和文件解析过程。其他两种(
YamlConfigurationFactory
,JsonConfigurationFactory
)类似。- 在生成XMLConfiguration的同时,也会利用XML解析器将配置文件解析成Document类型,便于后面解析。
setConfiguration(instance)
是将Xml文档对象解析成真正的日志对象的主要过程。其调用start方法来完成解析的整个过程。
解析的过程分两步:1.setup
利用XML文档对象构建起Node配置信息结构树(带有XML标签对应的插件类型信息); 2.doConfigure
利用Node配置信息结构树,进一步完成从插件对象到实体对象的转换。
1 | public void start() { |
setup
方法通过递归调用constructHierarchy
完成从XML标签元素到Node的转换,Node中最重要的属性是PluginType信息,它是配置文件Tag标签对应的插件类。(pluginManager是啥?为啥能根据tag标签名获取插件类型?留到最后讲解)
以
<Console>
标签为例,它的插件类型为ConsoleAppender
1 | public void setup() { |
doConfigure
递归执行createConfiguration
方法,其通过插件类信息构建日志对象(Appender
、LogerConfig
、Filter
)。创建完日志成员对象后,会进一步建立他们之间的联系。最后对于LogerConfig
会构建起其层次结构。
1 | protected void doConfigure() { |
createConfiguration
会调动createPluginObject(type, node, event)
方法来生成对象
1 | public void createConfiguration(final Node node, final LogEvent event) { |
通过构建器来穿件对象,首先通过createBuilder(this.class)
来创建构建器,然后,通过反射对构建器设置属性值,最后调用构建器的build()
来生成对象。通过工厂的方式也是类似的。
1 | private Object createPluginObject(final PluginType<?> type, final Node node, final LogEvent event) { |
createBuilder(this.clazz)
是通过反射的形式扫描clazz类的方法,找到带有@PluginBuilderFactory
注解静态方法,并调用它来生成构建器。
1 | private static Builder<?> createBuilder(final Class<?> clazz) |
来看一个具体的栗子,加深理解。
配置文件中配置了一个追加器 Console
,其对应的插件类型为 ConsoleAppender
。
1 | <Configuration status="WARN" monitorInterval="1000"> |
- 在
setup()
方法中会将XML转换成Node(PluginType=ConsoleAppender.class,...)
- 在doConfigure方法中会扫描ConsoleAppender类的所有方法,找到带有
@PluginBuilderFactory
的静态方法newBuilder()
,通过调用newBuilder
方法获得构造器ConsoleAppender.Builder
到此,LoggerContext的初始化就完成了。
2. 通过LoggerContext获取Logger
通过LoggerContext获取Logger的逻辑比较简单。
- 首先尝试从缓存中获取,有则直接返回,没有则到下一步;
- 调用
newInstance
来创建一个Logger。
1 | public Logger getLogger(final String name) { |
newInstance
方法间接调用 Logger的构造函数来实例化一个Logger对象。
1 | protected Logger(final LoggerContext context, final String name, final MessageFactory messageFactory) { |
其中,PrivateConfig建立起Logger与具体的LoggerConfig的关联。getLoggerConfig
方法中可以看出:
1. 先通过Logger的名称看是否有与其名称一致的LoggerConfig,有则返回;
2. 通过以 .
为分割符,从后往前截取字符串name.substring(0,name.lastIndexOf('.'))
,并以此来寻找是否能找到相应的LoggerConfig。若都没找到,则直接返回根LoggerConfig
比如Logger的名称为com.maoyan.order.biz,会顺序查找是否有 以 “com.maoyan.order.biz”、“com.maoyan.order” “com.maoyan”、“com”命名的LoggerConfig,若找到则直接返回,否则返回根LoggerConfig
1 | public PrivateConfig(final Configuration config, final Logger logger) { |
最终就完成了Logger对象的创建。
3. 触发日志事件
下面将从一条日志语句出发,分析进行日志操作的过程中,经过了哪些过程。
1 | logger.info("Hello World"); |
首先,这条日志会间接调用AbstractLogger中的logIfEnabled
方法,其中IsEnabled
会第一次调用Filter
对象,判断是否可以继续下去;若Configuration没有配置Filter
,则判断日志事件的Level是否大于对应的LoggerConfig的Level。ps:这里的Filter
是Configuration的全局Filter
。
1 | public void logIfEnabled(final String fqcn, final Level level, final Marker marker, final String message, |
当操作不被拦截后,进入Logger的logMessage
方法。可以看出,操作最终交由LoggerConfig来处理。
1 | public void logMessage(final String fqcn, final Level level, final Marker marker, final Message message, final Throwable t) { |
LoggerConfig的log
方法会对日志操作进行一次包装,封装成LogEvent,随后调用log(event)
方法。
1 | public void log(final String loggerName, final String fqcn,final Marker marker, final Level level, final Message data,final Throwable t) { |
可以看出,第二次调用filter
,这里调用的是LoggerConfig配置的Filter。进行过滤后开始调用LoggerConfig中配置的每个Appender。
1 | public void log(final LogEvent event) { |
方法中进行了两次过滤,第一次是调用Appender的Filter进行过滤,第二次当Appender实现了Filerable接口时,则执行接口方法。
1 | public void callAppender(final LogEvent event) { |
4. 附加信息
获取ConfigurationFactory
上面提到ConfigurationFactory会根据配置文件的文件类型来使用相应的配置工厂类,那么整个过程是怎么进行的呢?
1 | Configuration instance = ConfigurationFactory.getInstance().getConfiguration(name, configURI, cl); |
首先getInstance
方法会收集所有可用的ConfigurationFactory
,然后getConfiguration
遍历所有可用的ConfigurationFactory
,
1 | public static ConfigurationFactory getInstance() { |
在获得所有可用的配置工厂后,接下来就来确定到底用那种类型的配置工厂。其逻辑在getConfiguration(isTest,name)
方法中。
需要注意的是:在能找到多个不同类型的日志配置文件(json、yml、xml),因为会按yml -> json -> xml的顺序来加载配置文件并确定配置工厂。
1 | private Configuration getConfiguration(final boolean isTest, final String name) { |
PluginManager的初始化
在上面整个分析过程中,可以看到多处用到了PluginManager
,通过它可以获得特定类型的插件,那么这些插件是怎么初始化的呢?
事实上,在PluginManger初始化时,其属性 pluginRegistry(单例)初始化时,会调用
decodeCacheFiles
方法用于从META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat文件中加载并解析所有的插件类型对象,并按类别存储。目前共有6中类型插件:converter,lookup,core,configurationfactory,fileconverter,typeconverter
PatternLayout的特殊处理
在对配置文件解析时,一件有意思的事是,对日志输出样式的处理。
在日志系统初始化阶段,会对字符串形式的输出样式进行解析,将其转换为convetor的列表。在日志输出时,只需要顺序调用这些转换器,拼接返回结果得到输出内容。这样使得日志输出效率高。
[TOC]
参考: