掘金 后端 ( ) • 2024-04-10 09:42

theme: smartblue

什么是插件Plugin

每一个被注解@Plugin标记的类就是一个插件。@Plugin注解定义如下:

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:

  1. Core:由配置文件中的元素直接表示的插件,例如Appender、Layout、Logger或Filter
  2. Level:自定义level
  3. ConfigurationFactory:负责读取配置文件,比如log4j2.xml
  4. TypeConverter:支持将string类型的数据转成用户想要的类型,然后注入到某属性或某方法参数中
  5. Lookup:等同于spring中的占位符${k:v}替换,只是用法上有些不同
  6. Converter:PatternLayout能够根据指定规则进行匹配

log4j2中定义了很多plugin,那么是如何扫描到他们的的呢?见下

PluginManager

专门负责扫描并管理各种类型的插件

如何扫描标记了@Plugin的类

负责扫描标记了@Plugin的所有类,每次只能扫描指定类型的插件,比如下面的代码就扫描了"Core"类型的插件:

PluginManager manager = new PluginManager("Core");
manager.collectPlugins();
final Map<String, PluginType<?>> plugins = manager.getPlugins();

默认情况下log4j2会加载org.apache.logging.log4j.core包下所有标记了@Plugin的类,解析@PluginPluginAliases注解,然后将每一个类转成一个PluginType实例。当然也可以通过调用addPackages()方法让PluginManager扫描自定义包中的插件。

PluginTypePluginEntry类定义如下:

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实例

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()方法中,分两步:

  1. 扫描Plugin类中标记了@PluginBuilderFactory的共有静态方法,反射调用该方法会生成一个Builder实例,
  2. 调用injectFields(builder);方法为Builder实例中的属性赋值
  3. 调用builder.build();方法生成具体的Plugin类实例

如果成功地生成了具体的Plugin类实例,则返回,否则尝试执行以下步骤去生成实例

  1. 扫描Plugin类中标记了@PluginFactory的共有静态方法
  2. 调用generateParameters(factory);方法解析该静态方法中的所有参数
  3. 反射调用该静态方法注入所有参数,生成具体的Plugin类实例

总结:log4j2提供了@PluginBuilderFactory@PluginFactory这两个注解去生成具体的Plugin类实例(前者的优先级较高),在生成实例的时候为了方便注入属性值(后者则是方法参数),log4j2提供了很多注解(这些注解会在第2步或第5步被解析),比如:

@PluginBuilderAttribute:跟@PluginBuilderFactory搭配使用。必须使用TypeConverter从字符串转换参数。大多数内置类型已经得到支持,但也可以提供定制的TypeConverter插件以获得更多类型支持

@PluginAttribute:跟@PluginFactory搭配使用。功能同@PluginBuilderAttribute

@PluginConfiguration:适合用在属性或方法参数上,且类型为Configuration。把当前配置对象Configuration赋值给该属性或方法参数

@PluginElement:适合用在属性或方法参数上,该参数可以表示本身具有可配置参数的复杂对象。这还支持注入元素数组

@PluginNode:适合用在属性或方法参数上,且类型为Node。把正在解析的当前节点作为参数赋值给该属性或方法参数

@PluginValue:适合用在属性或方法参数上。将参数标识为值。这些通常与属性值相对应,但意味着要在某个位置用作占位符值的值。把当前节点的值或其名为value的属性赋值给该属性或方法参数

@PluginAliases:适合用在类、属性或方法参数上。可以为一个PluginPluginAttributePluginBuilderAttribute声明一个别名,注解定义如下:

大家如果感兴趣的可以去看看log4j2中的FileAppender类,它里面同时包含了@PluginBuilderFactory@PluginFactory这两个注解,但是后者已经过期了,因为它的优先级较低。

@PluginBuilderFactory

用于标记@Plugin类中的某个方法为工厂方法

package org.apache.logging.log4j.core.config.plugins;

@Target(ElementType.METHOD)
public @interface PluginBuilderFactory {
}

使用如下:

@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实现类,分别是: XmlConfigurationJsonConfigurationYamlConfigurationPropertiesConfiguration。每一个Configuration都包含了对应配置文件中的所有配置,那么log4j2是如何解析找到配置文件并且根据对应格式去解析成Configuration的呢?

我们先可以通过以下代码去扫描到log4j2中默认提供的ConfigurationFactory类型的插件:

final PluginManager manager = new PluginManager("ConfigurationFactory");
manager.collectPlugins();

扫描出的结果默认按照以下优先级排序依次是:

  1. PropertiesConfigurationFactory:支持的配置文件后缀:.properties
  2. YamlConfigurationFactory:支持的配置文件后缀:.yml、.yaml
  3. JsonConfigurationFactory:支持的配置文件后缀:.json、.jsn
  4. XmlConfigurationFactory:支持的配置文件后缀:.xml、*

这四个类其实都是ConfigurationFactory接口的四个实现,负责解析对应的log4j2配置文件的,也就是产生一个Configuration实例,它包含了这个配置文件中的所有信息。 更多关于ConfigurationFactory底层实现的可以参考:# 10.java日志之log4j2认识篇

TypeConverter类型的插件

我们都知道把配置文件解析出来的键值对kv其实默认都是string类型的,为了把string类型转成任何指定类型,log4j2特地声明了一个接口用来做这件事:

public interface TypeConverter<T> {
    T convert(String s) throws Exception;
}

这个接口就是负责接收string类型的参数,返回一个用户指定的类型数据。

跟这个接口密切相关的有两个类,它们分别是TypeConvertersTypeConverterRegistry

大家先去看一下TypeConverters类,它里面声明了很多TypeConverter的实现类,比如他们可以将string转成BigDecimal、BigInteger、Boolean、Byte、ByteArray、Duration、File等,可以看成是一个工具类,而这些实现类其实都被标记了@Plugin注解,其插件类型category都是TypeConverter,也就是说通过以下代码可以将log4j中默认提供的TypeConverter类型的插件都扫描出来:

PluginManager manager = new PluginManager(TypeConverters.CATEGORY);
manager.collectPlugins();

当然这些代码其实log4j2都帮我们写好了,TypeConverterRegistry这个类就是负责扫描TypeConverter类型的插件。

lgo4j2一切都帮我们准备好了,现在如果我们想将string类型值转成指定的类型,我们该如何做呢?

TypeConverters类中提供了一个convert()方法帮我们非常方便地实现这个功能

示例代码见下:

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实现代码如下:

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类如下:

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类型的插件

可以通过以下方式扫描到

PluginManager manager = new PluginManager("Core");
manager.collectPlugins();

Core插件是指那些由配置文件中的元素直接表示的插件,例如Appender、Layout、Logger或Filter。每个Core插件都必须声明一个用@PluginFactory@PluginBuilderFactory 注释的静态方法。

@PluginFactory用于提供所有选项作为方法参数的静态工厂方法,@PluginBuilderFactory用于构造一个新的Builder类,其字段用于注入属性和子节点。

要允许配置将正确的参数传递给方法,方法的每个参数都必须注释为以下属性类型之一: PluginAttributePluginElementPluginConfigurationPluginNodePluginValue

每个属性或元素注释必须包含配置中必须存在的名称,以便将配置项与其各自的参数相匹配。对于插件生成器,如果注释中未指定名称,则默认情况下将使用字段的名称

插件工厂字段和参数可以在运行时使用受Bean验证规范启发的约束验证器自动验证。以下注释捆绑在Log4j中,但也可以创建自定义约束验证器。

Required:验证值是否为非空。这包括检查null以及其他几个场景:空CharSequence对象、空数组、空集合实例和空映射实例

ValidHost:验证值是否对应于有效的主机名

ValidPort:验证值是否对应于介于0和65535之间的有效端口号。

Level类型的插件

可以通过以下方式扫描到

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类型的插件可以通过以下方式扫描到

PluginManager manager = new PluginManager("Lookup");
manager.collectPlugins();

被标记Lookup类型的@Plugin,必须要实现StrLookup接口。

log4j2提供了很多类型的Lookup实现,方便用户可以在不同的场景使用,现列举一些常用的:

DateLookup:以date开头,按照指定格式输出当前时间

System.out.println(new StrSubstitutor(new Interpolator()).replace("${date:yyyy-MM-dd}"));
logger.error("测试时间:${date:yyyy-MM-dd HH:mm:ss}");
<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}将获得相同的结果。

<File name="app" fileName="application.log">
  <PatternLayout>
    <pattern>%d %p %c{1.} [%t] $${ctx:loginId} %m%n</pattern>
  </PatternLayout>
</File>

MarkerLookup:以marker开头 ,输出marker的name

System.out.println(new StrSubstitutor(new Interpolator()).replace("${marker:}"));
logger.error(MarkerManager.getMarker("marker1"), "测试marker:${marker:}");

JavaLookup:以java开头,支持读取java环境信息

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开头,从计算机环境变量中读取值

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开头,从计算机环境变量中读取值

System.out.println(new StrSubstitutor(new Interpolator()).replace("${env:JAVA_HOME}"));
logger.error("测试环境变量:${env:JAVA_HOME}");

JndiLookup:以jndi开头,允许通过JNDI检索变量

默认情况下,密钥将以java:comp/env/为前缀,但如果密钥包含:,则不会添加前缀。

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;
}
<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配置文件的目录中。

<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

// --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:

<File name="app" fileName="application.log">  
    <PatternLayout header="File: ${main:--file}">    
        <Pattern>%d %m%n</Pattern>  
    </PatternLayout>
</File>

自定义LookUp

log4j2提供不下十种获取所运行环境配置信息的方式,基本能满足实际运行环境中获取各类配置信息的需求。 我们在自定义lookup时,可以根据自身需求自由选择继承自StrLookupAbstractLookupAbstractConfigurationAwareLookup等等来简化我们的代码。

  • 作为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}