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]
参考: