Log4j2分析与实践-扩展

LoggerContextFactory

    LoggerContextFactory将Log4j2 API绑定到了它的实现上面。LogManager通过META-INF/log4j-provider.properties来定位LoggerContextFactory,然后逐个验证Log4jAPIVersion的值与LogManager要求的值是一致的。如果有效的实现超过了一个,FactoryPriority的值将会用于识别最高优先级的工厂。最后,LoggerContextFactory属性的值将会用于定位LoggerContextFactory,在Log4j2中即为Log4jContextFactory。
    应用可以通过以下方式修改LoggerContextFactory:
(1)实现一个新的LoggerContextFactory ,在log4j-provider.properties对其进行引用,确保为最高优先级。
(2)创建一个新的log4j-provider.xml,然后配置LoggerContextFactory,确保为最高优先级。
(3)将系统属性log4j2.loggerContextFactory设置为类LoggerContextFactory。
(4)在log4j2.logManager.properties文件中设置属性log4j2.loggerContextFactory为LoggerContextFactory类。

    以上的所有配置文件均需要放到classpath下面。

ContextSelector

    Log4jContextFactory中会调用ContextSelector。ContextSelector的职责就是定位和创建一个LoggerContext,LoggerContext是Logger和Configuration的锚点。ContextSelector可以自由地实现对LoggerContext的管理。默认的Log4jContextFactory会验证系统属性Log4jContextSelector是否存在,如果存在,那么就会使用它指定的ContextSelector。
    Log4j提供了五种ContextSelect:
BasicContextSelector
    使用ThreadLocal中存储的LoggerContext,或者一个公共的LoggerContext。
ClassLoaderContextSelector
    将LoggerContext与调用getLogger那个类的Classloader关联起来,这是默认的ContextSelector。
JndiContextSelector
    通过查询JNDI来定位LoggerContext。
AsyncLoggerContextSelector
    创建一个LoggerContext,确保全部logger都是AsyncLoggers。
BundleContextSelector
    将LoggerContext与调用getLogger那个bundle的Classloader关联起来。在OSGi环境下是默认启用的。

ConfigurationFactory

    通常,我们会对修改日志的配置方式产生兴趣,而一个主要的方法就是实现或者扩展一个ConfigurationFactory。Log4j提供了新增ConfigurationFactory的两种方式:第一个就是配置系统变量log4j.configurationFactory;第二个就是将ConfigurationFactory定义为一个插件。
所有的ConfigurationFactory都是按顺序执行的,通过调用它的getSupportedTypes方法来决定它所支持的文件扩展名。如果定位到了一个指定文件扩展名的配置文件,那么ConfigurationFactory就会加载配置,并创建Configuration对象。
    大部分Configuration都是继承自BaseConfiguration类的,BaseConfiguration的子类需要加载处理配置文件,并创建一系列需要的节点对象。

LoggerConfig

    Logger就是在LoggerConfig对象中创建的。Log4j实现要求所有的LoggerConfig必须基于LoggerConfig类,所以,如果应用需要做一些修改,就要继承LoggerConfig类。为了定义新的LoggerConfig,将它定义为一个插件,将type指定为Core,然后提供一个名称(这个名称会在配置文件中被用作元素名称)。LoggerConfig也该定义一个@PluginFactory标注的方法,用于创建一个LoggerConfig实例。
    以下示例说明了根LoggerConfig是如何继承LoggerConfig的:
@Plugin(name = "root", category = "Core", printObject = true)
public static class RootLogger extends LoggerConfig {
    @PluginFactory
    public static LoggerConfig createLogger(@PluginAttribute(value = "additivity", defaultBooleanValue = true) boolean additivity,
                                            @PluginAttribute(value = "level", defaultStringValue = "ERROR") Level level,
                                            @PluginElement("AppenderRef") AppenderRef[] refs,
                                            @PluginElement("Filters") Filter filter) {
        List<AppenderRef> appenderRefs = Arrays.asList(refs);
        return new LoggerConfig(LogManager.ROOT_LOGGER_NAME, appenderRefs, filter, level, additivity);
    }
}

LogEventFactory

    LogEventFactory用于生成LogEvent。应用中可能会使用自定义的LogEventFactory类来替换标准的LogEventFactory,替换方式为设置系统属性Log4jLogEventFactory,值为自定义LogEventFactory的类名。
    注意:如果Log4j被配置为所有Logger都是异步的,那么会在环形缓冲中预分配日志事件,LogEventFactory就不再起作用了。

MessageFactory

    MessageFactory被用来生成Message对象。应用中可以使用自定义的MessageFactory来替换标准的ParameterizedMessageFactory,用于设置的系统属性为log4j2.messageFactory。
    Logger.entry()和logger.exit()方法的流式消息有个独立的FlowMessageFactory,可以使用自定义的FlowMessageFactory来替换DefaultFlowMessageFactory,设置的系统属性为log4j2.flowMessageFactory。

Lookups

    Lookups即为执行参数替换的方法,在配置初始化期间,一个"替换器"会被创建,它会定位并注册所有的Lookups,然后在需要的时候就会替换对应的变量。替换器会根据变量的前缀来匹配一个Lookup,然后解析这个变量。
    必须在Lookup上使用@Plugin注解,且设置类型为"Lookup",在@Plugin上面设置的name将会用于匹配变量的前缀。跟其它插件不同,Lookup不使用PluginFactory,而是需要提供一个无参的构造方法。

Filters

    当日志事件经过日志系统时,过滤器就会被用来拒绝或者接受它们。定义过滤器时,需要在类上面使用@Plugin注解,且type设置为"Core",elementType设置为"filter"。@Plugin注解的name属性被用于指定配置文件中对应的元素名称。如果将printObject指定为true,那么意味着一旦在配置执行过程中遇到对toString的调用(即配置的属性值需要调用toString方法来进行渲染),就会将参数进行格式化并且传递给过滤器。过滤器也需要指定一个@PluginFactory标注的方法,用于创建过滤器。

Appenders

    Appender接收到一个事件,然后调用一个Layout来对事件进行格式化,最后再按照设定的规则发布事件。需要在Appender的@Plugin注解上设置type为"Core",将elementType设置为"appender",name名称用于在配置文件中作为元素标签名使用。如果使用了toString方法来渲染传递给Appender的属性值,那么需要将printObject设置为true。
    Appender需要定义一个@PluginFactory标注的方法,用于创建Appender。
    大部分Appender使用Manager,Manager通常拥有各类资源,比如一个OutputStream或者Socket。当配置文件被更新时,将会创建一个新的Appender。然而,如果之前的Manager没有发生重大变化的话,新的Appender也只是引用一下已有的Manager,而不会创建一个新的,这就确保了在配置发生变化的时候,日志事件不会丢失。

Layouts

    Layout的任务就是将Appender需要写的事件格式化成可以打印的文本。所有的Layout均需实现Layout接口。如果要将事件格式化为String,Layout需要继承AbstractStringLayout,它会将String转换为需要的字节数组。
    每个Layout都要将自己定义为一个插件,使用@Plugin注解。type设置为"Core",elementType设置为"layout",如果插件的toString方法会用于对象或者参数的呈现,那么需要将printObject设置为true。name参数为配置文件中元素标签的名称。插件还需要一个@PluginFactory标注的静态方法,参数使用@PluginAttr或者@PluginElement标注。

PatternConverters

    PatternConverter被PatternLayout用来将日志事件格式化为可打印的字符串。每个Converter只负责一个简单的操作,然而,Converter的组合可以自由地以复杂的方式来格式化事件。
    一个PatternConverter需要定义为一个插件,使用@Plugin注解,指定type为"Converter"。还需要指定ConverterKeys属性,用来定义可以在pattern中唯一标识一个Converter的字符(即%之后的字符)。
    跟大多数其它插件不同,Converter不使用@PluginFactory注解,而是每个Converter需要有个静态的newInstance方法,仅接收一个字符串数组作为参数。字符串数组的值即为ConvertKey之后的花括号中的值。

Plugin Builders

    一些插件有很多可选的配置项,为了提升可维护性,通常会使用一个builder类来代替一个工厂方法。这是一个插件工厂的示例:
@PluginFactory
public static ListAppender createAppender(
        @PluginAttribute("name") @Required(message = "No name provided for ListAppender") final String name,
        @PluginAttribute("entryPerNewLine") final boolean newLine,
        @PluginAttribute("raw") final boolean raw,
        @PluginElement("Layout") final Layout<? extends Serializable> layout,
        @PluginElement("Filter") final Filter filter) {
    return new ListAppender(name, filter, layout, newLine, raw);
}

    将上面的工厂方法改为使用builer:

@PluginBuilderFactory
public static Builder newBuilder() {
    return new Builder();
}
 
public static class Builder implements org.apache.logging.log4j.core.util.Builder<ListAppender> {
 
    @PluginBuilderAttribute
    @Required(message = "No name provided for ListAppender")
    private String name;
 
    @PluginBuilderAttribute
    private boolean entryPerNewLine;
 
    @PluginBuilderAttribute
    private boolean raw;
 
    @PluginElement("Layout")
    private Layout<? extends Serializable> layout;
 
    @PluginElement("Filter")
    private Filter filter;
 
    public Builder setName(final String name) {
        this.name = name;
        return this;
    }
 
    public Builder setEntryPerNewLine(final boolean entryPerNewLine) {
        this.entryPerNewLine = entryPerNewLine;
        return this;
    }
 
    public Builder setRaw(final boolean raw) {
        this.raw = raw;
        return this;
    }
 
    public Builder setLayout(final Layout<? extends Serializable> layout) {
        this.layout = layout;
        return this;
    }
 
    public Builder setFilter(final Filter filter) {
        this.filter = filter;
        return this;
    }
 
    @Override
    public ListAppender build() {
        return new ListAppender(name, filter, layout, entryPerNewLine, raw);
    }
}
    唯一的不同点就是,builder使用的是@PluginBuilderAttribute,而不是@PluginAttribute。其实,两个注解都可以使用,前者更加适合字段注入,后者更加适合参数注入。而@PluginConfiguration, @PluginElement, @PluginNode, @PluginValue均可正常使用。需要注意的是,仍然需要一个工厂方法来提供builder,这个工厂方法需要使用注解@PluginBuilderFactory。

自定义ContextDataInjector

    ContextDataInjector(Log4j 2.7引入的)负责使用键值对来填充LogEvent的上下文数据,或者完全替换它。默认的实现是ThreadContextDataInjector,它从ThreadContext获得上下文属性。
应用可以设置系统属性log4j2.ContextDataInjector来替换默认的ContextDataInjector。

自定义ThreadContextMap实现

    通过设置系统属性log4j2.garbagefree.threadContextMap=true,就可以安装基于无垃圾StringMap的上下文Map。
可以设置log4j2.threadContextMap来指定任何自定义的ThreadContextMap实现。