Log4j 2 指南
Posted 書陋堂Slowtown
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Log4j 2 指南相关的知识,希望对你有一定的参考价值。
史料
鸿濛初辟,濁元漸清。日誌江湖,群雄逐鹿,紛爭四起。至于公元一九九六年初,西域諸國圍桌而謀 1,欲止其亂象。經百轉千回,終道成肉身,於公元一九九九年之秋,著道法歸約一卷,謂之 Log4j 2 。
Log4j 聚諸賢之智,可謂乃大成之作也。然同年末月,另有善此技者,递奏折于朝堂,望使其技入歸法典。彼時,滿朝文武,群策群力。又二年,終告之于天下,謂之 JSR-047 3 ,而坊間則以 JUL 稱之。至此,雙雄即現,JUL 居正統之位,而 Log4j 早已名揚于四海之外。
有詩云:江山代有人才出,各領風騷數百年。時至公元二〇〇六年,或慨于壯士終暮年,或路遙方知馬漸疲,Log4j 作者另創 Logback ,將其視為 Log4j 之後繼也。縱觀日誌江湖,前有正當英氣風發之 Log4j、JUL,后有初出茅廬之 Logback 4 ,三足鼎立,不在話下。
七度春秋,白馬過隙,青絲少年已然白髮如雪。公元二〇一二年五月,Log4j 迎其終版 5 。是年七月,Log4j 2 即公之于世 6 ,正所謂:千门万户曈曈日,总把新桃换旧符。
介绍
Apache Log4j 2 是 Log4j 的升级版,相较后者有了显著的改进,引入了很多来自于 Logback 的功能,并解决了一些 Logback 架构上的固有问题,使得其青出于蓝而胜于蓝。
特色
API 分离
清晰
高可维护性
基于 LMAX Disruptor 库 的异步日志
多 APIs 支持 Log4j 1.2 SLF4J JCL JUL
代码高适应性
使用适配器可切换到其他日志框架
自动化的配置重载
热替换
不丢失日志
灵活的过滤
高可定制的过滤
插入式架构
基于注解的自动识别
属性的支持
可在文件、系统/环境变量等多处定义属性
Java 8 Lambda 支持
灵活的日志等级定义
配置声明
代码变量声明
无垃圾或少垃圾
优势
配置热替换不丢日志,可被用于审计
应用可感知到输出源的异常信息
可做到无须 GC 或较少的 GC
使用注解方式引入输出源、过滤器等组件,更容易扩展
代码无须显示判断日志级别
通过 λ 表达式实现高开销的信息延迟构造
输出源的格式高度可定制
Syslog 输出源支持 TCPUDP
支持 BSD syslog 和 RFC 5424
使用 Java 5 并发框架使得并发控制锁发生在低级别
注:文章默认 Log4j 版本 2.11.0
资源
binary apache-log4j-2.11.0-bin.zip
md5:C0 E2 32 E6 7F EF FD 42 7E 49 74 0B 85 27 79 B5
source apache-log4j-2.11.0-src.zip
md5:23 38 66 93 16 BD 0C C7 DE 17 52 60 6A 5D 82 82
Maven 项目中,在
pom.xml
添加:<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies>
Log4j 2 将规范与实现做了 jar 包级别的分离,更好的引导开发者面向接口编程。规范部分名为 log4j-api
,实现部分名为 log4j-core
。严格来讲,编译期间只需要规范部分,实现部分到运行时再提供即可。然而,由于 log4j-core
部分提供了注解处理器,它在编译期间针对自定义插件提供元数据文件缓存及必要的自定义插件代码,从而加快自定义插件的启动速度。
架构
Log4j 2 的主要类关系如下图所示:
应用程序将获取 Logger
对象委托给 LogManager
类,后者通过 SPI 得到 LoggerContextFactory
工厂的实现类,并由它生产合适的 LoggerContext
,而该上下文对象包含一个 Configuration
,该对象包含若干个 Appenders、上下文级别的 Filters、StrSubstitutor、LoggerConfigs,其中 LoggerConfigs 每个都对象都可以根据 Logger
的 name 来定位,定位规则:
LoggerConfig 的名称与 Logger 的 name 相同
LoggerConfig 的名称与 Logger 的 父、先代包名相同
如果 1、2 条规则都没定位到具体 LoggerConfig 对象,则返回 root LoggerConfig
Loggers、LoggerConfigs 都是被命名的实体,名字大小写敏感。Log4j 1.x 则直接通过 Logger
来维持层级关系,而 Log4j 2 通过 LoggerConfig
的层级关系来维持 Logger
的层级关系。例如:名为 "com.foo"
的 LoggerConfig 是名为 "com.foo.Bar"
的 LoggerConfig 的父级。而顶层根 LoggerConfig 的名称为 LogManager.ROOT_LOGGER_NAME
或 ""
空字符串。
LoggerContext
整个日志系统的标记切入点。在一些情形中同一应用可以有多个活动的 LoggerContext。
Configuration
每个 LoggerContext 都有一个活动的 Configuration。该 Configuration 包含所有的 Appenders、上下文作用域范围的 Filters、LoggerConfigs 以及指向 StrSubstitutor 的引用。在重新加载配置期间,会有两个 Configuration 对象存在,一旦所有的 Loggers 都被重定向到新加载的 Configuration,旧的 Configuration 将被停止和遗弃。
Logger
本身不直接执行动作,仅包含一个与 LoggerConfig 关联的 name 属性,它继承 AbstractLogger 实现必要的方法,大部分记录日志的操作都在此抽象类中完成了。当 Configuration 被修改重新载入后,Loggers 将关联到不同的 LoggerCoinfig,从而更改其记录日志的行为。
LoggerConfig
LoggerConfig 在日志配置文件中声明定义了 Loggers 时被创建。该对象包含一组 Filters,这些过滤器能够在 LogEvent 被传递给任何 Appender 之前对其进行过滤操作。同时,LoggerConfig 还包含一组处理日志事件的 Appenders。
LoggerConfig 都将被分配一个日志级别,且存在 级别继承 的概念。前面讲过 LoggerConfig 通过名称实现层级关系,某个层级如果没有配置日志级别,默认就会继承其父、先代的,如果都没配置,根 LoggerConfig 默认的级别为 Level.ERROR
。
# 级别 权重 说明
ALL MAX_VALUE 记录所有
TRACE 600 追踪方法调用流向
DEBUG 500 满足基本调试
INFO 400 信息提示为目的
WARN 300 需引起重视的警告
ERROR 200 可恢复的错误
FATAL 100 系统崩溃的错误
OFF 0 不记录
注意:设置为 OFF 时,logger.log(Level.OFF, "msg")
的形式仍可被记录
Filter
除上面提及的日志级别配置外,还提供了 Filters 对日志进行过滤。这些过滤器可以作用于:
控制权移交给 LoggerConfig 之前
控制权移交给 LoggerConfig 之后,未调用任何 Appender 之前
控制权移交给 LoggerConfig 之后,调用特定 Appender 之前
每个 Appender 之上
每个过滤器可以返回以下三个结果之一:
Accept:日志事件直接由本过滤器接受并处理,且跳过其他过滤器
Neutral:日志事件在本过滤器不做处理,流转到其他过滤器
Deny:日志事件直接由本过滤器拒绝不处理,控制权直接交回调用方
注意:被过滤器接受并不意味着就能被记录日志。例如:事件被控制权移交给 LoggerConfig 之前型的过滤器接受,却被控制权移交给 LoggerConfig 之后型的过滤器拒绝,或者直接被所有 Appenders 拒绝。
Appender
除基于 Logger 来选择性的开启或关闭日志请求外,Log4j 还允许日志请求能够被打印到不同的目的地,这些目的地被称作 Appender,包括:控制台、文件、远程日志服务器、Apache Flume、JMS、Unix Syslog 进程,以及数据库。
默认下,日志消息除输出到当前 logger 的 LoggerConfig 关联的 Appenders 之外,还会顺着 LoggerConfig 的继承链往上传播到父、先代的 LoggerConfig 关联的 Appenders 上,并且所传递日志事件能否在父、先代 Appenders 上最终打印只受 Appenders 上的过滤器影响,而不受父、先代 LoggerConfigs 上的level 属性的影响。可以通过在对应的 logger 配置中设置 additivity="false"
来关闭传播行为。
Layout
Layout 负责对日志事件按照用户的格式进行格式化,而格式化之后的消息就交由 Appender 输出到指定目的地。PatternLayout 其格式化消息所用的格式非常类似于 C 语言的 printf
方法,%r [%t] %-5p %c -%m%n
的格式字符串将把日志信息格式化成:
176 [main] INFO org.foo.Bar - Located nearest gas station.
StrSubstitutor & StrLookup
两者结合能够使得 Configuration 可以引用一些变量,这些变量可在如下环境中被定义:系统属性、配置文件、线程上下文的 Map、日志事件的 StructuredData。在 Configuration 被处理时,这些变量能够被正常读取和解析。
源码分析
一只南美洲亚马逊河流域热带雨林中的蝴蝶,偶尔扇动几下翅膀,可以在两周以后引起美国得克萨斯州的一场龙卷风。
—— Edward Norton Lorenz
而引发 Log4j 日志运作的那只“蝴蝶”来源于此:
Code_01
private static final Logger log = LogManager.getLogger();
寻找 LoggerContextFactory 实现
加载 LoggerManager
时,其静态初始化代码块首先执行的就是寻找 LoggerContextFactory 的实现类。分别依照如下顺序进行搜寻:
搜名为
"log4j2.loggerContextFactory"
的系统属性,其值为上下文工厂实现类的全限定名。使用 Java SPI 技术 得到
org.apache.logging.log4j.spi.Provider
的实现类,借由该实现类定位到LoggerContextFactory
接口的实现类。找到单个就返回,找到多个返回优先级最高的那个。Log4j-core-2.11.0.jar 包中Provider
实现类为Log4jProvider
,此类给出的上下文工厂实现类为Log4jContextFactory
。前俩都没找到,返回
SimpleLoggerContextFactory
LoggerManager
静态代码块(有删减):
Code_02
// LoggerManager 的静态初始化代码块
static {
// *********************** 步骤一 *****************************
// FACTORY_PROPERTY_NAME = "log4j2.loggerContextFactory";
final String factoryClassName = managerProps.getStringProperty(FACTORY_PROPERTY_NAME);
factory = LoaderUtil.newCheckedInstanceOf(factoryClassName, LoggerContextFactory.class);
// *********************** 步骤二 *****************************
if (factory == null) {
final SortedMap<Integer, LoggerContextFactory> factories = new TreeMap<>();
if (ProviderUtil.hasProviders()) {
for (final Provider provider : ProviderUtil.getProviders()) {
final Class<? extends LoggerContextFactory> factoryClass = provider.loadLoggerContextFactory();
if (factoryClass != null) {
factories.put(provider.getPriority(), factoryClass.newInstance());
}
}
if (factories.isEmpty()) {
// *********************** 步骤三 *****************************
factory = new SimpleLoggerContextFactory();
} else if (factories.size() == 1) {
factory = factories.get(factories.lastKey());
} else {
factory = factories.get(factories.lastKey());
}
} else {
// *********************** 步骤三 *****************************
factory = new SimpleLoggerContextFactory();
}
}
}
静态初始化后,LogManager.getLogger()
被正式调用,最终被调用的方法为:
Code_03
// org.apache.logging.log4j.LogManager.getLogger(Class<?>)
public static Logger getLogger(final Class<?> clazz) {
final Class<?> cls = callerClass(clazz);
return getContext(cls.getClassLoader(), false).getLogger(toLoggerName(cls));
}
不难发现,需要两部操作:得到上下文实例,通过上下文实例取得最终的 Logger,接下来我们逐步来分析。
获取 LoggerContext 实例
由上面步骤找到的上下文工厂类 Log4jContextFactory
,它委托 ContextSelector
来定位或生产出一个 LoggerContext
实例,而此日志上下文就是 Loggers 以及它们的配置对象 configuration 的载体。
Code_04
// Log4jContextFactory.getContext(String, ClassLoader, Object, boolean)
public LoggerContext getContext(final String fqcn, final ClassLoader loader, final Object externalContext,
final boolean currentContext) {
// selector 为 ClassLoaderContextSelector,下文有介绍
final LoggerContext ctx = selector.getContext(fqcn, loader, currentContext);
if (externalContext != null && ctx.getExternalContext() == null) {
ctx.setExternalContext(externalContext);
}
// 刚初始化后的 LoggerContext 需调用 start 方法进配置的加载
if (ctx.getState() == LifeCycle.State.INITIALIZED) {
ctx.start();
}
return ctx;
}
而 ContextSelector
的实例的获取,Log4j 又给出了一个可配置的切入点:名为 "Log4jContextSelector"
的系统属性。如果配置了此属性,那么其值是一个实现了 ContextSelector
接口的类的全限定名。Log4j 提供了五种 selector :
BasicContextSelector
使用已存储在 ThreadLocal 中的 LoggerContext 或 通用的 LoggerContext
Code_05
//org.apache.logging.log4j.core.selector.BasicContextSelector
// 通用的 LoggerContext
private static final LoggerContext CONTEXT = new LoggerContext("Default");
public LoggerContext getContext(final String fqcn, final ClassLoader loader, final boolean currentContext) {
// ThreadLocal 中的 LoggerContext
final LoggerContext ctx = ContextAnchor.THREAD_CONTEXT.get();
return ctx != null ? ctx : CONTEXT;
}
ClassLoaderContextSelector
默认的 selector,将 LoggerContexts 与getLogger
方法调用者的ClassLoader
关联
Code_06
//org.apache.logging.log4j.core.selector.ClassLoaderContextSelector
protected static final ConcurrentMap<String, AtomicReference<WeakReference<LoggerContext>>> CONTEXT_MAP =
new ConcurrentHashMap<>();
// 代码有删减
private LoggerContext locateContext(final ClassLoader loaderOrNull, final URI configLocation) {
final ClassLoader loader = loaderOrNull != null ? loaderOrNull : ClassLoader.getSystemClassLoader();
// 将 loader 对象转化为 Map 的键值字符串,即对象存储地址的十六进制字符串
final String name = toContextMapKey(loader);
// 根据转化的键值在 Map 中查找是否存在,存在就返回,不存在就创建后添加入 MAP
AtomicReference<WeakReference<LoggerContext>> ref = CONTEXT_MAP.get(name);
if (ref == null) {
// ★★★ 没找到创建后,添加到 Map ★★★
LoggerContext ctx = createContext(name, configLocation);
final AtomicReference<WeakReference<LoggerContext>> r = new AtomicReference<>();
r.set(new WeakReference<>(ctx));
CONTEXT_MAP.putIfAbsent(name, r);
ctx = CONTEXT_MAP.get(name).get().get();
return ctx;
}
final WeakReference<LoggerContext> weakRef = ref.get();
LoggerContext ctx = weakRef.get();
// 找到就返回
if (ctx != null) {
return ctx;
}
ctx = createContext(name, configLocation);
ref.compareAndSet(weakRef, new WeakReference<>(ctx));
return ctx;
}
JndiContextSelector
通过查询 JNDI 来查找 LoggerContextAsyncLoggerContextSelector
由它创建的 AsyncLoggerContext 关联的 Logger 全部是异步的,即 AsyncLogger。它继承至ClassLoaderContextSelector
,对比后者,它存储在 Map 对象CONTEXT_MAP
中的键值仅仅是加了前缀"AsyncContext@"
或"DefaultAsyncContext@"
BundleContextSelector
OSGi 环境下的默认 selector,将 LoggerContexts 与getLogger
方法调用者所在的 bundle 的ClassLoader
关联。它同样继承至ClassLoaderContextSelector
,对比后者,它存储在 Map 对象CONTEXT_MAP
中的键值是来源于 bundle 的getSymbolicName
方法。
默认的 ClassLoaderContextSelector
在 Map 缓存中没找到当前的 loader 对应的 loggerContext,调用上面加 ‘★’ 处的代码进行创建并添加到 Map 缓存中,并返回给委托方 Log4jContextFactory
,并由它检测 loggerContext 的生命周期状态,如果是刚完成初始化,则需调用 start
方法进行配置的加载,参见下小节。
Tips:
/**
* LoggerContext 实现 LifeCycle2 生命周期接口
* 而 LifeCycle2 继承 LifeCycle,它定义了如下声明周期状态
*/
// org.apache.logging.log4j.core.LifeCycle.State
enum State {
/** 对象正在初始化. */
INITIALIZING,
/** 初始化完成,还没启动. */
INITIALIZED,
/** 正在调用 start 方法,启动中. */
STARTING,
/** start 方法执行完毕,已启动. */
STARTED,
/** stop 方法执行中,停止中. */
STOPPING,
/** stop 方法执行完毕,已停止 */
STOPPED
}
启动 LoggerContext 加载配置
上一节已经得到了 LoggerContext 的实例,接下来继续执行 Code_04 中的 ctx.start()
进行配置加载,具体代码:
Code_07
// org.apache.logging.log4j.core.LoggerContext.start()
// 代码有删减
public void start() {
if (configLock.tryLock()) {
try {
if (this.isInitialized() || this.isStopped()) {
// 生命周期调整为 启动中 状态
this.setStarting();
// 加载配置
reconfigure();
if (this.configuration.isShutdownHookEnabled()) {
setUpShutdownHook();
}
// 启动完毕,生命周期调整为 已启动 状态
this.setStarted();
}
} finally {
configLock.unlock();
}
}
}
// org.apache.logging.log4j.core.LoggerContext.reconfigure(URI)
// 上面的 reconfigure() 方法最终调用到此方法
// 代码有删减
private void reconfigure(final URI configURI) {
// configURI 此时为 null,故 cl 也为 null
final ClassLoader cl = ClassLoader.class.isInstance(externalContext) ? (ClassLoader) externalContext : null;
// ★★★ 委托给 ConfigurationFactory ★★★
final Configuration instance = ConfigurationFactory.getInstance().getConfiguration(this, contextName, configURI, cl);
if (instance == null) {
// 记录异常
} else {
// 将配置实例赋值给成员变量
setConfiguration(instance);
}
}
上面代码显示,LoggerContext 启动后,将获取 Configuration
实例的操作委托给了 ConfigurationFactory
工厂,先来看如何获取配置工厂实例:
Code_08
// org.apache.logging.log4j.core.config.ConfigurationFactory.getInstance()
// 代码有删减
public static ConfigurationFactory getInstance() {
if (factories == null) {
LOCK.lock();
try {
if (factories == null) {
final List<ConfigurationFactory> list = new ArrayList<>();
// ① 系统属性方式收集配置工厂实例
// CONFIGURATION_FACTORY_PROPERTY = "log4j.configurationFactory"
final String factoryClass = PropertiesUtil.getProperties().getStringProperty(CONFIGURATION_FACTORY_PROPERTY);
if (factoryClass != null) {
addFactory(list, factoryClass);
}
// ② 利用插件管理器搜集注解方式的配置工厂实例
// CATEGORY = "ConfigurationFactory"
final PluginManager manager = new PluginManager(CATEGORY);
/*
* 按照下面顺序进行追加合并收集:
* 1. 搜寻 CLASSPATH 中的 Log4j2Plugin.dat 插件缓存文件,里面都是 Log4j 内建的插件
* 2. 在 OSGi bundles 中搜寻 Log4j2Plugin.dat
* 3. 搜寻通过静态的 `addPackage` 方法添加的包中搜寻插件
* 4. 搜寻方法参数给定的包列表中的插件
*/
manager.collectPlugins();
final Map<String, PluginType<?>> plugins = manager.getPlugins();
final List<Class<? extends ConfigurationFactory>> ordered = new ArrayList<>(plugins.size());
for (final PluginType<?> type : plugins.values()) {
try {
ordered.add(type.getPluginClass().asSubclass(ConfigurationFactory.class));
} catch (final Exception ex) {
// 记录异常日志
}
}
// 插件找到的工厂,按照注解 @Order 的顺序排序,数值越大优先级越高
Collections.sort(ordered, OrderComparator.getInstance());
for (final Class<? extends ConfigurationFactory> clazz : ordered) {
// 系统属性参数配置的工厂高于插件找到的,由此可知
addFactory(list, clazz);
}
factories = Collections.unmodifiableList(list);
}
} finally {
LOCK.unlock();
}
}
/* ★★★
* 返回私有静态内部类 ConfigurationFactory.Factory
* 由它的 getConfiguration 方法获取 Configuration
*/
return configFactory;
}
在获取抽象类 ConfigurationFactory
子类实例时,Log4j 2 有两种方式:
提供了一个可供配置扩展的系统属性参数(最高优先级)
属性名:
"log4j.configurationFactory"
属性值:
ConfigurationFactory
子类全限定名字符串利用插件管理器搜寻注解式的插件工厂
如代码注释的步骤搜寻子类,篇幅原因具体 Log4j 的插件体系,不在这里展开,详见 Plugins。值得一提的是,为了加快插件搜寻载入速度,Log4j 内建的插件采用了 Log4j2Plugin.dat 文件进行缓存。目前内建的配置工厂有如下几种,@Order 数值越大优先级越高:
PropertiesConfigurationFactory @Order(8)
YamlConfigurationFactory @Order(7)
JsonConfigurationFactory @Order(6)
XmlConfigurationFactory @Order(5)
不知大家有没有注意到,其实抽象类 ConfigurationFactory
还有一个非插件式的子类 ConfigurationFactory.Factory
,它是前者的静态私有内部类,它不仅包装了对前面两种方式收集到的工厂子类的各种操作,还定义了配置文件加载的先后顺序。
配置文件加载的先后顺序:
优先加载以
"log4j2-test"
为前缀的文件其次加载以
"log4j2"
+ contextName 7 为前缀的文件最后加载以
"log4j2"
为前缀的文件相同前缀时,不同文件类型的优先顺序:
".properties"
".yml"
或".yaml"
".json"
或".jsn"
"xml"
优先级先考虑前缀,其次才是文件类型。
例如:
"log4j2-test.xml"
> "log4j2.properties"
虽然 .xml 文件优先级低于 .properties,但 log4j2-test 前缀优先级高于 log4j2 前缀。
若有名为 "log4j2.properties"
的配置文件,ConfigurationFactory.Factory
会执行如下代码:
Code_09
// org.apache.logging.log4j.core.config.ConfigurationFactory.Factory.getConfiguration(LoggerContext, boolean, String)
private Configuration getConfiguration(final LoggerContext loggerContext, final boolean isTest, final String name) {
final boolean named = Strings.isNotEmpty(name);
final ClassLoader loader = LoaderUtil.getThreadContextClassLoader();
// 按照优先级顺序,依次遍历插件工厂寻找与之相对应的配置文件类型,找到立即返回
for (final ConfigurationFactory factory : getFactories()) {
String configName;
final String prefix = isTest ? TEST_PREFIX : DEFAULT_PREFIX;
final String [] types = factory.getSupportedTypes();
if (types == null) {
continue;
}
for (final String suffix : types) {
if (suffix.equals(ALL_TYPES)) {
continue;
}
configName = named ? prefix + name + suffix : prefix + suffix;
// 根据配置文件名 log4j2.properties 使用 ClassLoader.getResource 的到实际的资源
final ConfigurationSource source = ConfigurationSource.fromResource(configName, loader);
if (source != null) {
if (!factory.isActive()) {
// 记录警告信息
}
// ★★★ 加载配置的操作交给真正的配置工厂实例 ★★★
return factory.getConfiguration(loggerContext, source);
}
}
}
return null;
}
由于配置的是 ".properties"
文件,所有 ‘★’ 标处的 factory 是 PropertiesConfigurationFactory
的实例。转到所调用的代码:
Code_10
// PropertiesConfigurationFactory.getConfiguration(LoggerContext, ConfigurationSource)
public PropertiesConfiguration getConfiguration(final LoggerContext loggerContext, final ConfigurationSource source) {
final Properties properties = new Properties();
try (final InputStream configStream = source.getInputStream()) {
// 读取 log4j2.properties
properties.load(configStream);
} catch (final IOException ioe) {
// 记录异常
}
// 采用 Builder 模式构建 PropertiesConfiguration
return new PropertiesConfigurationBuilder()
.setConfigurationSource(source)
.setRootProperties(properties)
.setLoggerContext(loggerContext)
.build();
}
从创建一个 PropertiesConfigurationBuilder
实例到调用它的 build
方法拿到配置对象 PropertiesConfiguration
涉及很多的类,先通过类图来理清下关系:
从图底部开始,工厂类 PropertiesConfigurationFactory
(简称 pcf
) 构建 PropertiesConfigurationBuilder
(简称 pcb
),它有 ConfigurationBuilder
类型的成员变量,该变量引用的对象由图中未画出来的 ConfigurationBuilderFactory
创建,目前该工厂创建的都是 DefaultConfigurationBuilder
(简称 dcb
)对象。pcb
读取配置文件,对全局的 rootProperties
按照组件分类成不同的子集,然后调用 dcb
的 add(XxxComponentBuilder )
方法生成 root 组件的各个子组件,最后由它的 build(boolean)
将这些组件信息去初始化继承链上的各个成员变量,最后新生成未初始化的 PropertiesConfiguration
返回给最初调用 pcf
工厂的 LoggerContext
,赋值给它的成员变量 configuration
。
Code_11
// org.apache.logging.log4j.core.LoggerContext.reconfigure(URI)
// 代码有删减
private void reconfigure(final URI configURI) {
final Configuration instance = ConfigurationFactory.getInstance().getConfiguration(this, contextName, configURI, cl);
if (instance == null) {
// 记录异常
} else {
// ★★★ 可不光仅仅是给成员变量赋值 ★★★
setConfiguration(instance);
}
}
setConfiguration(instance)
不是简单的赋值操作,它是另外一只 “蝴蝶” :
Code_12
// LoggerContext.setConfiguration(Configuration)
// 代码有删减
private Configuration setConfiguration(final Configuration config) {
configLock.lock();
try {
// 存档一份当前配置的引用
final Configuration prev = this.configuration;
// 给新传入的配置注册一个监听器
config.addListener(this);
//★★★ 新配置启动,【已初始化】->【已启动】
config.start();
// 将新传入的配置更新为当前的配置
this.configuration = config;
// 根据新配置更新所有 Logger 的配置信息
updateLoggers();
// 完成后清理旧的配置
if (prev != null) {
prev.removeListener(this);
prev.stop();
}
// 触发属性变更事件
firePropertyChangeEvent(new PropertyChangeEvent(this, PROPERTY_CONFIG, prev, config));
// AsyncLoggers update their nanoClock when the configuration changes
Log4jLogEvent.setNanoClock(configuration.getNanoClock());
return prev;
} finally {
configLock.unlock();
}
}
重点来看下 config.start()
方法调用栈:
org.apache.logging.log4j.core.LoggerContext.setConfiguration(Configuration)
org.apache.logging.log4j.core.config.AbstractConfiguration.start()
org.apache.logging.log4j.core.config.AbstractConfiguration.initialize()
org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration.setup()
// ① 把 Component组件树 转变为 Node节点树
org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration.convertToNode(Node, Component)
org.apache.logging.log4j.core.config.AbstractConfiguration.doConfigure()
org.apache.logging.log4j.core.config.AbstractConfiguration.createConfiguration(Node, LogEvent)
// ② Node 节点关联的插件类的具体实例设置到 Node.object 成员变量上
org.apache.logging.log4j.core.config.Node.setObject(Object)
org.apache.logging.log4j.core.config.AbstractConfiguration.createPluginObject(PluginType<?>, Node, LogEvent)
// ③ 解析插件注解,反射调用被 `@PluginFactory` 或 `@PluginBuilderFactory` 注解的方法,得到插件类实例
org.apache.logging.log4j.core.config.plugins.util.PluginBuilder.build()
该方法对配置内的所有具备生命周期的组件进行启动操作,其中重要操作就包括:
将配置文件中构建出的 Component 组件树转为 Node 节点树
反射调用被
@PluginFactory
或@PluginBuilderFactory
注解的方法构建插件实例,赋值给node.object
举个从配置文件读取过滤器设置的例子来更直观的了解这个过程:
log4j2.properties 文件
# 除过滤器配置外,其他省略
# 可以定义一组过滤器,以逗号隔开,例: foo,bar
filters = foo
# 定义一个过滤级别为 info 的 ThresholdFilter
filter.foo.type = ThresholdFilter
filter.foo.level = info
解析成为 Component
{
"pluginType": "ThresholdFilter",
"value": null,
"attributes": {
"onMatch": null,
"onMismatch": null,
"level": "info"
},
"components":[]
插件管理器由 Component 的 pluginType 值找到 PluginType<ThresholdFilter>
{
"elementName": "filter",
"pluginEntry": {
"key": "thresholdfilter",
"className":"org.apache.logging.log4j.core.filter.ThresholdFilter",
"name": "ThresholdFilter",
"printable": true,
"defer": false,
"category": "core"
},
"pluginClass": {
"name": "org.apache.logging.log4j.core.filter.ThresholdFilter"
}}
结合 Component 和 PluginType<> 转为 Node
{
"parent": null,
"name": "ThresholdFilter",
"value": null,
"type": {
"elementName": "filter",
"pluginEntry": {
"key": "thresholdfilter",
"className":"org.apache.logging.log4j.core.filter.ThresholdFilter",
"name": "ThresholdFilter",
"printable": true,
"defer": false,
"category": "core"
},
"pluginClass": {
"name": "org.apache.logging.log4j.core.filter.ThresholdFilter"
}
},
"attributes": {
"onMatch": null,
"onMismatch": null,
"level": "info"
},
"children": [],
"object": null}
PluginBuilder
build()
方法生成最终插件类,填充 Node 的 object 字段
{
"object": {
"level": {
"name": "INFO",
"intLevel": 400,
"standardLevel": {
"intLevel": 500
}
},
"onMatch": "NEUTRAL",
"onMismatch": "DENY",
"state": "INITIALIZED"
}}
注:TypeConverters 处理类型转换。(例:"info" 转化为 "Level.INFO" )
自此完成了 Code_03 的前半部分,拿到了一个已经加载了完整的配置,且已启动完成的 context 对象,接下来就委托该上下文对象,来获取 Logger。
取得 Logger
LoggerContext
包含 LoggerRegistry
注册器,当 context 把所需的记录器的 name 交给它,它就会在自身的 map 中检索是否包含该 name 的记录器,有就返回,没有就创建后添加到自身的 map 中,后返回新创建的 记录器。
Code_13
// org.apache.logging.log4j.core.LoggerContext.getLogger(String, MessageFactory)
public Logger getLogger(final String name, final MessageFactory messageFactory) {
// Note: This is the only method where we add entries to the 'loggerRegistry' ivar.
Logger logger = loggerRegistry.getLogger(name, messageFactory);
if (logger != null) {
AbstractLogger.checkMessageFactory(logger, messageFactory);
return logger;
}
logger = newInstance(this, name, messageFactory);
loggerRegistry.putIfAbsent(name, messageFactory, logger);
return loggerRegistry.getLogger(name, messageFactory);
}
LoggerContext
得到 Logger
后将其返回给 LoggerManager
,自此最初的那只“蝴蝶” Code_01 所带来的全部效应已经完成,得到的记录器对象就可以完成业务代码的记录工作了。
配置详解
参数配置
Log4j 2 可供配置参数的地方(数值小优先级越高):
环境变量(优先级:-100)
以
LOG4J_
开头,以_
分隔单词的全大写的配置参数新参数配置方案,无历史兼容参数
log4j2.component.properties (优先级:0)
在 classpath 中的此文件,也可用来配置 log4j 2 的配置参数系统属性(优先级:100)
具体有哪些可配置项请参考:系统参数。
-D
形式配置在 JVM 的启动参数中代码中调用
System.setProperty
设置2.10 版本统一以
log4j2.
开头命名参数,并兼容历史参数命名会被上面两种按优先级顺序覆盖掉相同的配置项
组件配置
log4j2 开头的配置文件,用来配置 Log4j 2 各种重要组件的配置。配置文件支持 properties
、yml
、json
、xml
格式,加载顺序详见: 配置文件加载规则 。
注意: properties
格式的配置文件是 2.4 版本后才被支持。
Log4j 2 目前的组件包括:Configuration、Properties、Scripts、CustomLevels、Filters、Appenders、Loggers、RootLogger, 共八种组件。点击 » log4j2.properties « 下载完整样板配置文件说明,排版要求我们拆散分别介绍:
Configuration
# 本配置的名称
name = nameOfThisConfig
# log4j2 内部日志打印级别
status = [trace|debug|info|warn|error|fatal]
# 加载插件是否显示诊断信息,默认 false
verbose = [false|true]
# 内部日志输出位置,可以是 stderr、文件、URL
dest = [err|filePath|URL]
# 配置文件是否变更检查,单位:秒
monitorInterval = 60
# JVM 关闭时是否 log4j2 也自动关闭,默认 enable
shutdownHook = [enable|disable]
# JVM 关闭时 appenders、后台任务多久后也关闭,单位:毫秒。
# 默认 0,表示 appenders 按其自身的超时时间,其忽略后台任务是否完成
# 设置时间太短有丢失记录,如果 shutdownHook 关闭时,最好不要用此参数
shutdownTimeout = 1000
# 插件搜索所在的包名,可含多个包,以逗号隔开
packages = org.foo,org.bar
# Advertiser 插件名,用来发布 FileAppender 或 SocketAppender 的配置信息
# 目前仅有的支持此功能的插件名为 multicastdns
advertiser = multicastdns
Properties
# 自定义属性变量,下面定义了两个变量:pro1、pro2
property.pro1 = value_1
property.pro2 = value_2
Scripts
# 类型一:直接在配置文件中写入脚本
script.s1.type = script
script.s1.name = s1
script.s1.language = javascript
script.s1.text = textOfScript
# 类型二:配置文件仅指名脚本文件路径
script.s2.type = scriptFile
script.s2.name = s2
script.s2.path = scriptFilePath
CustomLevels
# 下面定义两个自定义级别:level1、level2,权重分别为:101、102
customLevel.level1 = 101
customLevel.level2 = 102
详细关于自定义 Logger Level 的方式,及详细配置方法,请查阅:Log4j2 - Custom Log Levels
Filters
# 2.6 版本之前的写法
# --------------------------------------------
# 定义两过滤器变量 f1、f2
filters = f1,f2
# f1 的过滤器插件类型,值来源于具体过滤器类的注解 `@Plugin` 的 name 属性值
# 必填
filter.f1.type = nameOfFilterPlugin
# 可以不设置,默认 neutral
filter.f1.onMatch = [accept|neutral|deny]
# 可以不设置,默认 deny
filter.f1.onMismatch = [accept|neutral|deny]
# 可能还有其他属性,取决于具体过滤器类型
filter.f1.xxx = xxx
filter.f2.type = nameOfFilterPlugin
filter.f2.onMatch = [accept|neutral|deny]
filter.f2.onMismatch = [accept|neutral|deny]
# DENY - 丢弃当前日志事件,且不传给其它过滤器
# NEUTRAL - 中立,当前日志事件直接传给其他过滤器
# ACCEPT - 接受当前日志事件,且不传给其他过滤器
# 2.6 版本及之后的写法,自推断出不同过滤器,无须定义变量列表
# ----------------------------------------------
filter.f1.type = nameOfFilterPlugin
filter.f1.onMatch = [accept|neutral|deny]
filter.f1.onMismatch = [accept|neutral|deny]
filter.f1.xxx = xxx
filter.f2.type = nameOfFilterPlugin
filter.f2.onMatch = [accept|neutral|deny]
filter.f2.onMismatch = [accept|neutral|deny]
上面没介绍各种过滤器类型及各类型可配置的属性,请自行查阅:Log4j2 手册 - Filters
Appenders
# 2.6 版本前也需要定义变量列表,由于与过滤器类似,这里直接写 2.6版本之后的写法
# appender 类型,值来源于具体过滤器类的注解 `@Plugin` 的 name 属性值
# 必填
appender.ap1.type = Console
# 此 name 用来给下面的 Loggers 引用
# 必填
appender.ap1.name = nameOfAppender1
# appender 其他属性取决于具体的 appender 类型
# 本例中 Console 类型的它就含有一个 target 的属性
appender.ap1.target = [SYSTEM_OUT|SYSTEM_ERR]
# appender 自身的过滤器(选配),具体配置同上面独立的过滤器类似
appender.ap1.filter.type = [accept|neutral|deny]
appender.ap1.filter.onMatch = [accept|neutral|deny]
appender.ap1.filter.onMisMatch = [accept|neutral|deny]
# appender 自身的layout(选配),其 type 属性必填
# 值来源于具体过滤器类的注解 `@Plugin` 的 name 属性值
appender.ap1.layout.type = PatternLayout
# layout 其他属性取决于具体的 layout 类型
# 本例中 PatternLayout 具有 pattern 属性
appender.ap1.layout.pattern = [%t] %-5p %c - %m%n
appender.ap2.type = typeOfAppender2
appender.ap2.name = nameOfAppender2
appender.ap2.xxx = xxx
appender.ap2.layout.type = xxx
appender.ap2.layout.xxx = xxx
appender.ap2.filter.type = [accept|neutral|deny]
appender.ap2.filter.onMatch = [accept|neutral|deny]
appender.ap2.filter.onMisMatch = [accept|neutral|deny]
上面没介绍各种 Appender、Layout 类型及各类型可配置的属性及格式,请自行查阅:
Log4j2 手册 - Appenders
Log4j2 手册 - Layouts
Loggers
# 2.6 版本前也需要定义变量列表,由于与过滤器类似,这里直接写 2.6版本之后的写法
# 记录器名称,一般是包或类的全限定名称,注意:会忽略名称叫 `root` 的 logger
# 必填
logger.log1.name = org.foo
# 设置是否将日志事件打印到父、先辈的 appender 上
# 注意:向上传的是所有日志事件,未经过自己的 logger 过滤
# 具体在父、先辈如何过滤,要看父、先辈的 appender 上的过滤器
logger.log1.additivity = true
# 是否包含记录器所在业务代码的位置信息(高开销)
# 同步 logger 默认为 true,
# 异步 logger 默认为 false,因为涉及位置信息快照在不同线程之间传递
logger.log1.includeLocation = [true|false]
# 日志记录级别,左到右权重越来越大,确定级别后,小于等于其权重的都被记录
logger.log1.level = [off|fatal|error|warn|info|debug|trace|all]
# 需要异步 logger,就需要 type属性,其值为 AsyncLogger
logger.log1.type = AsyncLogger
# 该 logger 的记录信息需要发送到的 appender 引用,其值为 appender 定义中的 name 属性
# 一个 logger 可以配置多个 appender,通过 appenderRef.XXX 变量名来区分
# 必填
logger.log1.appenderRef.ap1.ref = nameOfAppender1
# 确定小于等于哪个日志级别的消息需要传递到该 appender
logger.log1.appenderRef.ap1.level = [off|fatal|error|warn|info|debug|trace|all]
# 在上面参数定义的 levle 范围内再次进行过滤,且只能包含一个过滤器
logger.log1.appenderRef.ap1.filter.f.type = xxx
logger.log1.appenderRef.ap1.filter.f.onMatch = [accept|neutral|deny]
logger.log1.appenderRef.ap1.filter.f.onMisMatch = [accept|neutral|deny]
# 如果想要多组过滤器,只能将这些过滤器组合成一个组合过滤器 CompositeFilter,格式如下:
# type 设置为 `filters`,就能找到 CompositeFilter 插件
logger.file.appenderRef.random.filter.f.type = filters
logger.file.appenderRef.random.filter.f.f1.type = xxx
logger.file.appenderRef.random.filter.f.f1.xxx = xxx
logger.file.appenderRef.random.filter.f.f2.type = xxx
logger.file.appenderRef.random.filter.f.f2.xxx = xxx
# 一个 logger 可以有多个 appenderRef,如下所示为第二个定义
logger.log1.appenderRef.ap2.ref = nameOfAppender2
logger.log1.appenderRef.ap2.level = [off|fatal|error|warn|info|debug|trace|all]
logger.log1.appenderRef.ap2.filter.f1.type = xxx
logger.log1.appenderRef.ap2.filter.f1.onMatch = [accept|neutral|deny]
logger.log1.appenderRef.ap2.filter.f1.onMisMatch = [accept|neutral|deny]
# 注意 logger 中日志级别的优先级:
# log1.level > log1.appenderRef.ap1.level > log1.appenderRef.ap1.filter
# 也就是说,ap1.filter 在 ap1.level定义的范围做过滤,
# 而 ap1.level 又在 log1.level 的范围内限制
上面没介绍 AsyncLogger 及其具体配置方式,请自行查阅:Log4j2 手册 - AsyncLoggers
RootLogger
# 与上面 Loggers 相比,除不能有 name、additivity 属性定义外
# 最重要是配置节点元素变成了 rootLogger,且它后面必要根变量名,直接写它的属性,因为只有一个 root
# rootLogger 不能配置名称,不代表它没有名称,它的名称为:“” 空字符串,不是 null
# 需要异步 logger,就需要 type属性,其值为 AsyncLogger
rootLogger.type = AsyncLogger
# 是否包含记录器所在业务代码的位置信息(高开销)
# 同步 logger 默认为 true,
# 异步 logger 默认为 false,因为涉及位置信息快照在不同线程之间传递
rootLogger.includeLocation = [true|false]
# 默认为 error
rootLogger.level = [off|fatal|error|warn|info|debug|trace|all]
rootLogger.appenderRef.ap1.ref = xxx
rootLogger.appenderRef.ap1.level = [off|fatal|error|warn|info|debug|trace|all]
# 在上面参数定义的 levle 范围内再次进行过滤,且只能包含一个过滤器
rootLogger.appenderRef.ap1.filter.f.type = xxx
rootLogger.appenderRef.ap1.filter.f.onMatch = [accept|neutral|deny]
rootLogger.appenderRef.ap1.filter.f.onMisMatch = [accept|neutral|deny]
配置实践
下面的配置假设的场景为:
每隔一分钟检测配置文件是否变化
标准控制台只打印警告及以上的更为严重的信息
更详细的包含提示信息的日志都保存到当前应用的
logs/rolling
目录下的log.log
每次应用启动、每隔 1 小时时,归档当前活动的文件,并创建新的活动文件
logs/rolling/log.log
活动文件限制 500 MB,超过归档处理,每小时内只允许保留最近的 5 个归档文件,如此循环
log-2018-05-15-19-1.log (最旧归档一直是此名称)
log-2018-05-15-19-2.log
log-2018-05-15-19-3.log
log-2018-05-15-19-4.log
log-2018-05-15-19-5.log (最新归档一直是此名称)
log.log (活动的日志文件一直是此名称)
# ------------------------------------------------------------------- loggerConfig
dest = err
status = error
name = PropertiesConfig
# 每隔 1 分钟检测一次配置文件是否有变动
monitorInterval = 60
# -------------------------------------------------------------------- properties
# 利用全局属性定义一个日志格式属性,方便下文引用
property.pattern = %d{yyyy-MM-dd HH:mm:ss,SSS} [%t] %-5p %c - %m%n
# -------------------------------------------------------------------- appenders
# ************* Console APPENDER **********************
# 控制台 Appender,只打印 警告及以上的信息
appender.console.type = Console
appender.console.name = STDOUT
appender.console.target = SYSTEM_OUT
appender.console.layout.type = PatternLayout
# 这里引用了全局属性变量:pattern
appender.console.layout.pattern = ${pattern}
appender.console.filter.threshold.type = ThresholdFilter
appender.console.filter.threshold.level = warn
# ************* RollingFile APPENDER **********************
# 可循环的日志记录文件 Appender,
appender.rolling.type = RollingFile
appender.rolling.name = ROLLINGFILE
appender.rolling.fileName = logs/rolling/log.log
appender.rolling.filePattern = logs/rolling/log-%d{yyyy-MM-dd-HH}-%i.log.gz
appender.rolling.layout.type = PatternLayout
appender.rolling.layout.pattern = ${pattern}
# 缓存大小,默认8192,单位:字节
appender.rolling.bufferSize = 8192
# 默认使用缓存,如想禁用取消注释
#appender.rolling.bufferedIO = false
# 是否立即刷新,默认false,建议保持默认
#appender.rolling.immediateFlush = true
# ************* Policies **********************
# 触发归档文件的原则(组合型):
# 1. 当前日志文件大小已达到 500 MB
# 2. 基于时间的触发
# %d{yyyy-MM-dd-HH} 最小单位为小时,interval = 1
# 表示:每 1 小时触发一次
appender.rolling.policies.type = Policies
appender.rolling.policies.size.type = SizeBasedTriggeringPolicy
appender.rolling.policies.size.size= 500MB
appender.rolling.policies.time.type = TimeBasedTriggeringPolicy
appender.rolling.policies.time.interval = 1
# 是否规整均分,例如:现在是凌晨3点,interval设置为4
# true - 均分为 0,4,8,12,16,20,则下一次应该要凌晨4点才触发
# false - 不均分,3,7,11,15,19,23
appender.rolling.policies.time.modulate = true
# ************* Strategy_1 **********************
# 文件发生重名时,控制 filePattern 里 '%i' 的数值变化策略
# 此处为默认策略:DefaultRolloverStrategy,它需要 appender 的 fileName 属性
# 因为活动状态的文件一直用 fileName 的名称
appender.rolling.strategy.type = DefaultRolloverStrategy
# 最小值,默认为 1,如果想从 0 开始编号,取消注释
# appender.rolling.strategy.min = 0
# 默认为 7,这里设置为5
appender.rolling.strategy.max = 5
# ************* Strategy_2 **********************
# 设置最新归档文件的 `%i` 是数值最大,还是最小,或是一直累加
# 1. max -最新的归档数值最大;
# 2. min -最新的归档数值最小;
# 3. nomax -最新的归档数值最大,且不在 [min,max] 中循环,而是一直累加
# 这里采用默认的 max,故无需设置
#appender.rolling.strategy.fileIndex = nomax
# 文件发生重名时,控制 filePattern 里 '%i' 的数值变化策略,这里的 i 是一直累加
# 此处为默认策略:DirectWriteRolloverStrategy,它【不】需要 appender 的 fileName 属性
# 因为活动状态的文件名就是 filePattern 去除 .gz 的文件名
#appender.rolling.strategy.type = DirectWriteRolloverStrategy
# 允许保留的归档文件数,默认全部保留,如果设置,值必须 ≥ 1
# 如下面设置,归档文件超过 3,就删除最旧的那个归档包
#appender.rolling.strategy.maxFiles = 3
# ------------------------------------------------------------------------ loggers
# ************* ROOT LOGGER **********************
rootLogger.level = warn
rootLogger.appenderRef.stdout.ref = STDOUT
# ************* org.reion LOGGER **********************
logger.file.name = org.reion
# 是否打印到父、先辈的 appender,默认为 true,如果关闭,请取消注释
#logger.file.additivity = false
logger.file.level = info
logger.file.includeLocation = false
logger.file.appenderRef.rolling.ref = ROLLINGFILE
总结
自此完成了基础的日志配置,Log4j 2 的一些新特性,如:异步日志、Free GC、MDC、Lookups 等内容还没有完全展开,另外其记录性能如何有待测试,有空再续……
参考资料:
Log4j 1.x 手册 http://logging.apache.org/log4j/1.2/manual.html
Log4j 2 官方文档 https://logging.apache.org/log4j/2.x/
Which Java Logging Framework Has the Best Performance?
代码索引
Code_01 - LogManager.getLogger() 最初的“蝴蝶”
Code_02 - LogManager 静态初始化代码
Code_03 - LogManager 委托上下文获取记录器
Code_04 - Log4jContextFactory.getContext
Code_05 - BasicContextSelector.getContext
Code_06 - ClassLoaderContextSelector.getContext 默认
Code_07 - LoggerContext.start
Code_08 - ConfigurationFactory.getInstance
Code_09 - ConfigurationFactory.Factory.getConfiguration
Code_10 - PropertiesConfigurationFactory.getConfiguration
Code_11 - LoggerContext.reconfigure
Code_12 - LoggerContext.setConfiguration 第二只“蝴蝶”
Code_13 - LoggerContext.getLogger
脚注
1996 年初,E.U. SEMPER 制定日志 API 来统一规则 ↩
1999-10-15,log4j 初始版发布 ↩
1999-12-17,Sun、IBM 提交 JSR-047, 2001-12-17 纳入 Java 规范 ↩
2006-5-14,Log4j 作者 Ceki Gülcü 提交初始 Logback 代碼,详见 Maillist ↩
2012-05-13,Log4j 发布终版,并于 2015-08-05 停止维护 ↩
2012-07-29, Ralph Goers 发布初始版 Log4j 2,详见 Changes ↩
以上是关于Log4j 2 指南的主要内容,如果未能解决你的问题,请参考以下文章
markdown 打字稿...编码说明,提示,作弊,指南,代码片段和教程文章
Vue3官网-高级指南(十七)响应式计算`computed`和侦听`watchEffect`(onTrackonTriggeronInvalidate副作用的刷新时机`watch` pre)(代码片段