13.java日志之log4j2插件plugin篇

什么是插件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:

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

java 复制代码
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类定义如下:

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

  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类中的某个方法为工厂方法

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

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

java 复制代码
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特地声明了一个接口用来做这件事:

java 复制代码
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类型的插件都扫描出来:

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类,其字段用于注入属性和子节点。

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

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

插件工厂字段和参数可以在运行时使用受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时,可以根据自身需求自由选择继承自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}
相关推荐
蓝澈112116 分钟前
迪杰斯特拉算法之解决单源最短路径问题
java·数据结构
Kali_0724 分钟前
使用 Mathematical_Expression 从零开始实现数学题目的作答小游戏【可复制代码】
java·人工智能·免费
rzl0235 分钟前
java web5(黑马)
java·开发语言·前端
君爱学习41 分钟前
RocketMQ延迟消息是如何实现的?
后端
guojl1 小时前
深度解读jdk8 HashMap设计与源码
java
Falling421 小时前
使用 CNB 构建并部署maven项目
后端
guojl1 小时前
深度解读jdk8 ConcurrentHashMap设计与源码
java
程序员小假1 小时前
我们来讲一讲 ConcurrentHashMap
后端
爱上语文1 小时前
Redis基础(5):Redis的Java客户端
java·开发语言·数据库·redis·后端