什么是插件Plugin
每一个被注解@Plugin
标记的类就是一个插件。@Plugin
注解定义如下:
java
package org.apache.logging.log4j.core.config.plugins;
public @interface Plugin {
String EMPTY = Strings.EMPTY;
String name();
String category();
String elementType() default EMPTY;
boolean printObject() default false;
boolean deferChildren() default false;
}
字段解释:
- name:插件的名称
- category:插件的类型
插件根据其功能可以分为以下5个类型category:
- Core:由配置文件中的元素直接表示的插件,例如Appender、Layout、Logger或Filter
- Level:自定义level
- ConfigurationFactory:负责读取配置文件,比如log4j2.xml
- TypeConverter:支持将string类型的数据转成用户想要的类型,然后注入到某属性或某方法参数中
- Lookup:等同于spring中的占位符${k:v}替换,只是用法上有些不同
- Converter:PatternLayout能够根据指定规则进行匹配
log4j2中定义了很多plugin,那么是如何扫描到他们的的呢?见下
PluginManager
专门负责扫描并管理各种类型的插件
如何扫描标记了@Plugin
的类
负责扫描标记了@Plugin
的所有类,每次只能扫描指定类型的插件,比如下面的代码就扫描了"Core"类型的插件:
java
PluginManager manager = new PluginManager("Core");
manager.collectPlugins();
final Map<String, PluginType<?>> plugins = manager.getPlugins();
默认情况下log4j2会加载org.apache.logging.log4j.core
包下所有标记了@Plugin
的类,解析@Plugin
和PluginAliases
注解,然后将每一个类转成一个PluginType
实例。当然也可以通过调用addPackages()
方法让PluginManager
扫描自定义包中的插件。
PluginType
和PluginEntry
类定义如下:
java
public class PluginType<T> {
private final PluginEntry pluginEntry; // 见下
private final Class<T> pluginClass; // @Plugin标记类
private final String elementName; // @Plugin的elementType,默认是@Plugin的name
}
public class PluginEntry implements Serializable {
private String key; // @Plugin的name全小写
private String className; // @Plugin标记类
private String name; // @Plugin的name
private boolean printable; // @Plugin的printObject
private boolean defer; // @Plugin的deferChildren
private transient String category; // @Plugin的category
}
如何生成Plugin实例
当我们通过PluginManager
这个工具类扫描到了org.apache.logging.log4j.core
包下所有标记了@Plugin
的类后,如何生成对应的实例呢?这节我们专门来看这个问题。
具体参考以下方法,它返回的其实就是一个具体的Plugin
实例
java
public abstract class AbstractConfiguration extends AbstractFilterable implements Configuration {
private Object createPluginObject(final PluginType<?> type, final Node node, final LogEvent event) {
final Class<?> clazz = type.getPluginClass(); // @Plugin所标记的类
if (Map.class.isAssignableFrom(clazz)) {
return createPluginMap(node);
}
if (Collection.class.isAssignableFrom(clazz)) {
return createPluginCollection(node);
}
return new PluginBuilder(type).withConfiguration(this).withConfigurationNode(node).forLogEvent(event).build();
}
}
@Override
public Object build() {
// first try to use a builder class if one is available
try {
// 1 解析@PluginBuilderFactory
final Builder<?> builder = createBuilder(this.clazz);
if (builder != null) {
// 2
injectFields(builder);
// 3
return builder.build();
}
} catch (final ConfigurationException e) { // LOG4J2-1908
return null; // no point in trying the factory method
} catch (final Exception e) {
}
// or fall back to factory method if no builder class is available
try {
// 4 解析@PluginFactory
final Method factory = findFactoryMethod(this.clazz);
// 5
final Object[] params = generateParameters(factory);
// 6
return factory.invoke(null, params);
} catch (final Exception e) {
return null;
}
}
具体的逻辑都在build()
方法中,分两步:
- 扫描
Plugin
类中标记了@PluginBuilderFactory
的共有静态方法,反射调用该方法会生成一个Builder
实例, - 调用
injectFields(builder);
方法为Builder
实例中的属性赋值 - 调用
builder.build();
方法生成具体的Plugin
类实例
如果成功地生成了具体的Plugin
类实例,则返回,否则尝试执行以下步骤去生成实例
- 扫描
Plugin
类中标记了@PluginFactory
的共有静态方法 - 调用
generateParameters(factory);
方法解析该静态方法中的所有参数 - 反射调用该静态方法注入所有参数,生成具体的
Plugin
类实例
总结:log4j2提供了@PluginBuilderFactory
和@PluginFactory
这两个注解去生成具体的Plugin
类实例(前者的优先级较高),在生成实例的时候为了方便注入属性值(后者则是方法参数),log4j2提供了很多注解(这些注解会在第2步或第5步被解析),比如:
@PluginBuilderAttribute :跟@PluginBuilderFactory
搭配使用。必须使用TypeConverter从字符串转换参数。大多数内置类型已经得到支持,但也可以提供定制的TypeConverter插件以获得更多类型支持
@PluginAttribute :跟@PluginFactory
搭配使用。功能同@PluginBuilderAttribute
@PluginConfiguration :适合用在属性或方法参数上,且类型为Configuration。把当前配置对象Configuration赋值给该属性或方法参数
@PluginElement:适合用在属性或方法参数上,该参数可以表示本身具有可配置参数的复杂对象。这还支持注入元素数组
@PluginNode :适合用在属性或方法参数上,且类型为Node。把正在解析的当前节点作为参数赋值给该属性或方法参数
@PluginValue :适合用在属性或方法参数上。将参数标识为值。这些通常与属性值相对应,但意味着要在某个位置用作占位符值的值。把当前节点的值或其名为value的属性赋值给该属性或方法参数
@PluginAliases :适合用在类、属性或方法参数上。可以为一个Plugin
、PluginAttribute
、PluginBuilderAttribute
声明一个别名,注解定义如下:
大家如果感兴趣的可以去看看log4j2中的
FileAppender
类,它里面同时包含了@PluginBuilderFactory
和@PluginFactory
这两个注解,但是后者已经过期了,因为它的优先级较低。
@PluginBuilderFactory
用于标记@Plugin
类中的某个方法为工厂方法
java
package org.apache.logging.log4j.core.config.plugins;
@Target(ElementType.METHOD)
public @interface PluginBuilderFactory {
}
使用如下:
java
@Plugin(name = "Async", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE, printObject = true)
public final class AsyncAppender extends AbstractAppender {
@PluginBuilderFactory
public static Builder newBuilder() {
return new Builder();
}
public static class Builder implements org.apache.logging.log4j.core.util.Builder<AsyncAppender> {
@PluginElement("AppenderRef")
@Required(message = "No appender references provided to AsyncAppender")
private AppenderRef[] appenderRefs;
@PluginBuilderAttribute
@PluginAliases("error-ref")
private String errorRef;
}
}
那么是在何处解析该注解的呢?可以全局搜索PluginBuilderFactory.class
即可,发现只有一处地方使用到了该注解,见下:
PluginBuilder#createBuilder(Class<?>);
遍历@Plugin
中的所有方法,如果有静态方法标记了PluginBuilderFactory
注解, 直接反射调用返回一个Builder
对象就返回,源码很简单就不粘上来了。
生成Builder
对象之后呢,对象里面的属性还没有填充进去呢?接下来就专门干这事了,具体参考:
PluginBuilder#injectFields(Builder);
校验注解
主要以下三种:
- @ValidHost:host必须有效
- @ValidPort: port必须有效
- @Required: 值必须非空
具体参考以下两处调用,原理很简单:
1.在反射生成Plugin
中的Builder
对象后,调用该方法为Builder
对象中的属性赋值,如果有的属性标注了以上三种注解,则会校验被注入的值是否是否合法
PluginBuilder#generateParameters();
2.在找到Plugin
中标记了@PluginFactory
的静态方法后,在反射调用该方法生成Plugin
实例前,需要先注入参数值,如果有的参数标注了以上三种注解,则会校验被注入的值是否非法
PluginBuilder#injectFields();
ConfigurationFactory类型的插件
ConfigurationFactory
,顾名思义就是产生Configuration
的工厂,而Configuration
则是Log4j2定义的配置接口,每一个Configuration
对应一个配置文件,根据不同的配置分别有不同的实现类。
log4j2默认支持XML、JSON、YAML和properties四种配置文件格式,各自对应Log4j2中的一个Configuration
实现类,分别是: XmlConfiguration
、JsonConfiguration
、YamlConfiguration
和PropertiesConfiguration
。每一个Configuration
都包含了对应配置文件中的所有配置,那么log4j2是如何解析找到配置文件并且根据对应格式去解析成Configuration
的呢?
我们先可以通过以下代码去扫描到log4j2中默认提供的ConfigurationFactory类型的插件:
java
final PluginManager manager = new PluginManager("ConfigurationFactory");
manager.collectPlugins();
扫描出的结果默认按照以下优先级排序依次是:
- PropertiesConfigurationFactory:支持的配置文件后缀:.properties
- YamlConfigurationFactory:支持的配置文件后缀:.yml、.yaml
- JsonConfigurationFactory:支持的配置文件后缀:.json、.jsn
- XmlConfigurationFactory:支持的配置文件后缀:.xml、*
这四个类其实都是ConfigurationFactory
接口的四个实现,负责解析对应的log4j2配置文件的,也就是产生一个Configuration
实例,它包含了这个配置文件中的所有信息。 更多关于ConfigurationFactory
底层实现的可以参考:# 10.java日志之log4j2认识篇
TypeConverter类型的插件
我们都知道把配置文件解析出来的键值对kv其实默认都是string类型的,为了把string类型转成任何指定类型,log4j2特地声明了一个接口用来做这件事:
java
public interface TypeConverter<T> {
T convert(String s) throws Exception;
}
这个接口就是负责接收string类型的参数,返回一个用户指定的类型数据。
跟这个接口密切相关的有两个类,它们分别是TypeConverters
和TypeConverterRegistry
。
大家先去看一下TypeConverters
类,它里面声明了很多TypeConverter
的实现类,比如他们可以将string转成BigDecimal、BigInteger、Boolean、Byte、ByteArray、Duration、File等,可以看成是一个工具类,而这些实现类其实都被标记了@Plugin
注解,其插件类型category都是TypeConverter
,也就是说通过以下代码可以将log4j中默认提供的TypeConverter类型的插件都扫描出来:
java
PluginManager manager = new PluginManager(TypeConverters.CATEGORY);
manager.collectPlugins();
当然这些代码其实log4j2都帮我们写好了,TypeConverterRegistry
这个类就是负责扫描TypeConverter类型的插件。
lgo4j2一切都帮我们准备好了,现在如果我们想将string类型值转成指定的类型,我们该如何做呢?
TypeConverters
类中提供了一个convert()
方法帮我们非常方便地实现这个功能
示例代码见下:
java
package com.matio.log4j2.typeconverter;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.config.plugins.convert.TypeConverters;
import java.math.BigInteger;
public class TestTypeConverter {
public static void main(String[] args) {
// 将123转成integer类型
System.out.println(TypeConverters.convert("123", Integer.class, null));
// 将123转成BigInteger类型
System.out.println(TypeConverters.convert("123", BigInteger.class, null));
// 将true转成Boolean类型
System.out.println(TypeConverters.convert("true", Boolean.class, "false"));
Level level = TypeConverters.convert("info", Level.class, "debug");
System.out.println(level);
}
}
除了log4j2中提供的那些string转换器之外,我们也可以定义自己的转换器,只需要保证它们可以被log4j2扫描到就可以了。
自定义TypeConverter
实现代码如下:
java
package com.matio.log4j2.typeconverter;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.convert.TypeConverter;
import org.apache.logging.log4j.core.config.plugins.convert.TypeConverters;
// name不能重复
@Plugin(name = "person", category = TypeConverters.CATEGORY)
public class PersonConverter implements TypeConverter<Person> {
@Override
public Person convert(String s) throws Exception {
return new Person(s, 22);
}
}
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
Client类如下:
java
package com.matio.log4j2.typeconverter;
import org.apache.logging.log4j.core.config.plugins.convert.TypeConverters;
import org.apache.logging.log4j.core.config.plugins.util.PluginManager;
public class PersonTest {
public static void main(String[] args) {
// PluginManager.addPackage("com.matio.log4j2.typeconverter");
System.out.println(TypeConverters.convert("matio", Person.class, null));
}
}
Core类型的插件
可以通过以下方式扫描到
java
PluginManager manager = new PluginManager("Core");
manager.collectPlugins();
Core插件是指那些由配置文件中的元素直接表示的插件,例如Appender、Layout、Logger或Filter 。每个Core插件都必须声明一个用@PluginFactory
或@PluginBuilderFactory
注释的静态方法。
@PluginFactory
用于提供所有选项作为方法参数的静态工厂方法,@PluginBuilderFactory
用于构造一个新的Builder
类,其字段用于注入属性和子节点。
要允许配置将正确的参数传递给方法,方法的每个参数都必须注释为以下属性类型之一: PluginAttribute
、PluginElement
、PluginConfiguration
、PluginNode
或PluginValue
每个属性或元素注释必须包含配置中必须存在的名称,以便将配置项与其各自的参数相匹配。对于插件生成器,如果注释中未指定名称,则默认情况下将使用字段的名称
插件工厂字段和参数可以在运行时使用受Bean验证规范启发的约束验证器自动验证。以下注释捆绑在Log4j中,但也可以创建自定义约束验证器。
Required:验证值是否为非空。这包括检查null以及其他几个场景:空CharSequence对象、空数组、空集合实例和空映射实例
ValidHost:验证值是否对应于有效的主机名
ValidPort:验证值是否对应于介于0和65535之间的有效端口号。
Level类型的插件
可以通过以下方式扫描到
java
PluginManager manager = new PluginManager("Level");
manager.collectPlugins();
log4j2中没有提供该类型的插件
Lookup类型的插件
lookup功能其实等同于spring中的占位符${k:v}替换,只是用法上有些不同。
必须在@Plugin
中将其category声明为"Lookup",并且被标记的类必须实现StrLookup
接口。该接口有2个方法;
1.接受字符串键并返回字符串值的查找方法,
2.接受LogEvent和字符串键并返回字符串的第二个查找方法。
可以通过指定${name:key}
来引用查找,其中name是插件注释中指定的名称,key是要查找的项的名称。也支持$${env:user:-x}
配置默认值。 可以参考# log4j2用户手册 或者 # log4j2使用手册(中文)第八章 Lookups
lookup类型的插件可以通过以下方式扫描到
java
PluginManager manager = new PluginManager("Lookup");
manager.collectPlugins();
被标记Lookup类型的@Plugin
,必须要实现StrLookup
接口。
log4j2提供了很多类型的Lookup实现,方便用户可以在不同的场景使用,现列举一些常用的:
DateLookup :以date
开头,按照指定格式输出当前时间
java
System.out.println(new StrSubstitutor(new Interpolator()).replace("${date:yyyy-MM-dd}"));
logger.error("测试时间:${date:yyyy-MM-dd HH:mm:ss}");
xml
<RollingFile name="RF-${map:type}" fileName="${filename}" filePattern="test1-$${date:MM-dd-yyyy}.%i.log.gz">
<PatternLayout>
<pattern>%d %p %c{1.} [%t] %m%n</pattern>
</PatternLayout>
<SizeBasedTriggeringPolicy size="128" />
</RollingFile>
ContextMapLookup :以ctx
开头,允许应用程序将数据存储在Log4j ThreadContext Map中,然后检索Log4j配置中的值。
在下面的示例中,应用程序将使用键loginId
将当前用户的登录ID存储在ThreadContext Map中。 在初始配置处理期间,第一个$
将被删除。 PatternLayout支持使用Lookup进行插值,然后为每个事件解析变量。 请注意,模式%X{loginId}
将获得相同的结果。
xml
<File name="app" fileName="application.log">
<PatternLayout>
<pattern>%d %p %c{1.} [%t] $${ctx:loginId} %m%n</pattern>
</PatternLayout>
</File>
MarkerLookup :以marker
开头 ,输出marker的name
java
System.out.println(new StrSubstitutor(new Interpolator()).replace("${marker:}"));
logger.error(MarkerManager.getMarker("marker1"), "测试marker:${marker:}");
JavaLookup :以java
开头,支持读取java环境信息
bash
System.out.println("测试 " + new StrSubstitutor(new Interpolator()).replace("${java:vm}"));
System.out.println("支持递归:" + new StrSubstitutor(new Interpolator()).replace("${java:${xx:-vm}}"));
logger.error("${java:${xx:-version}}");
SystemPropertiesLookup :以sys
开头,从计算机环境变量中读取值
java
System.setProperty("pp1", "vv1");
System.out.println(new StrSubstitutor(new Interpolator()).replace("${sys:user.home}"));
logger.error("测试系统变量:${sys:user.home} ${sys:pp1}");
System.clearProperty("pp1");
EnvironmentLookup :以env
开头,从计算机环境变量中读取值
java
System.out.println(new StrSubstitutor(new Interpolator()).replace("${env:JAVA_HOME}"));
logger.error("测试环境变量:${env:JAVA_HOME}");
JndiLookup :以jndi
开头,允许通过JNDI检索变量
默认情况下,密钥将以java:comp/env/
为前缀,但如果密钥包含:
,则不会添加前缀。
java
try {
startJNDIRegistry();
System.out.println(new StrSubstitutor(new Interpolator(new Interpolator())).replace("测试jndi1:${jndi:rmi://127.0.0.1:1099/hello}"));
logger.error("测试jndi2:${jndi:rmi://127.0.0.1:1099/hello}");
} catch (Exception e) {
e.printStackTrace();
}
private static void startJNDIRegistry() throws RemoteException, AlreadyBoundException, MalformedURLException {
RMIHello rmiHello = new RMIHello();
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1:1099/hello", rmiHello);
System.out.println("Registry运行中......");
}
public static class RMIHello extends UnicastRemoteObject implements IHello {
private String name;
protected RMIHello() throws RemoteException {
super();
}
@Override
public String sayHello(String name) throws RemoteException {
this.name = name;
return "response : " + name;
}
@Override
public String toString() {
return "jndi成功" + name;
}
}
public interface IHello extends Remote {
String sayHello(String name) throws RemoteException;
}
xml
<File name="app" fileName="application.log">
<PatternLayout>
<pattern>%d %p %c{1.} [%t] $${jndi:logging/context-name} %m%n</pattern>
</PatternLayout>
</File>
JmxRuntimeInputArgumentsLookup :以jvmrunargs
开头,映射JVM输入参数 - 但不是主参数 - 使用JMX获取JVM参数。
Log4jLookup :以log4j
开头,仅支持${log4j:configLocation}
或${log4j:configParentLocation}
分别提供log4j配置文件及其父文件夹的绝对路径。
以下示例使用此Lookup将日志文件放在相对于log4j配置文件的目录中。
xml
<File name="Application" fileName="${log4j:configParentLocation}/logs/application.log">
<PatternLayout>
<pattern>%d %p %c{1.} [%t] %m%n</pattern>
</PatternLayout>
</File>
MainMapLookup :以main
开头,读取程序启动参数
前提是必须手动将应用程序的主要参数设置给Log4j2,可以通过以下方式:
MainMapLookup.setMainArguments(args);
如果已设置主要参数,则此查找允许应用程序从日志记录配置中检索这些主要参数值。前缀后面的键main:
可以是参数列表中基于 0 的索引,也可以是字符串,其中${main:myString}
替换 myString
为主参数列表中后面的值。
注意:许多应用程序使用前导破折号来标识命令参数。指定 ${main:--file}
将导致查找失败,因为它将查找名为main
且默认值为-file
的变量。为了避免这种情况,分隔查找名称和键的:
后面必须跟一个反斜杠作为转义字符,如下所示${main:\--file}
例如,假设 static void main(String[] args) 参数是:
--file foo.txt --verbose -x bar
java
// --file foo.txt --verbose -x bar
if (args != null && args.length > 0) {
System.out.println(Arrays.toString(args));
}
// 手动将应用程序的主要参数提供给Log4j
MainMapLookup.setMainArguments(args);
System.out.println(new StrSubstitutor(new Interpolator()).replace("测试main:${main:0} ${main:1} ${main:2} $${main:--file} ${main:-x} ${main:bar}"));
logger.error("测试main:${main:0} ${main:1} ${main:2} $${main:--file} ${main:-x} ${main:bar}");
那么可以进行以下替换:
Expression | Result |
---|---|
${main:0} | --file |
${main:1} | foo.txt |
${main:2} | --verbose |
${main:3} | -x |
${main:4} | bar |
${main:\--file} | foo.txt |
${main:\-x} | bar |
${main:bar} | null |
${main:\--quiet:-true} | true |
Example usage:
xml
<File name="app" fileName="application.log">
<PatternLayout header="File: ${main:--file}">
<Pattern>%d %m%n</Pattern>
</PatternLayout>
</File>
自定义LookUp:
log4j2提供不下十种获取所运行环境配置信息的方式,基本能满足实际运行环境中获取各类配置信息的需求。 我们在自定义lookup时,可以根据自身需求自由选择继承自StrLookup
,AbstractLookup
或AbstractConfigurationAwareLookup
等等来简化我们的代码。
- 作为lookup对外门面的
Interpolator
是通过 log4j2中负责解析<properties/>
节点的PropertiesPlugin
类来并入执行流程中的。具体源码可以参见PropertiesPlugin.configureSubstitutor
方法。其中注意的是,我们在中提供的属性是以default的优先级提供给外界的 - 作为lookup对外门面的
Interpolator
,在其构造函数中载入了所有category值为StrLookup.CATEGORY的plugin【即包括log4j2内置的(org.apache.logging.log4j.core
包下的),也包括用户自定义的(log4j2.xml文件中的Configuration.packages
属性值指示的package下的)】 - Interpolator可以单独使用,但某些值可能取不到
- 获取MDC中的内容,log4j2提供了两种方式:
$${ctx:user}
或%X{user}