log4j2自定义appender插件源码、配置及采坑说明

本文介绍如何扩展Log4j2的Appender插件,包括自定义变量、引入原生插件等内容,并解决了本地测试有效但服务器失效等问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本篇为扩展appender标签,如果需要扩展filer、marker等其他的log4j2提供的扩展方式,可以查看相关的文档:
中文文档:https://www.docs4dev.com/docs/zh/log4j2/2.x/all/javadoc.html
英文文档:http://logging.apache.org/log4j/2.x/
api:http://logging.apache.org/log4j/2.x/log4j-api/apidocs/index.html
通过本篇,你可以看到我在扩展插件时所遇到的问题以及解决方案,根据ApplicationContext获取bean的工具类源码等。

不再是千篇一律的抄袭,真正将解决自己所遇到的问题的代码分享出来。

目录

自定义Appender插件类

关键代码

代码说明:

1.引入log4j2的插件注解,并声明相关属性的值

2.如何自定义变量

3.如何引入原生的插件,如rollingfile和console

4.为何本地自测有效,一到服务器就不生效?

5.我的bean注入是null?明明配置都对

log4j2.xml配置


 

自定义Appender插件类

关键代码

package com.xxx.util;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.*;
import org.apache.logging.log4j.core.appender.*;
import org.apache.logging.log4j.core.config.AppenderControl;
import org.apache.logging.log4j.core.config.AppenderRef;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.Property;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.layout.PatternLayout;
import java.io.Serializable;

@Slf4j
@Plugin(name = "MyAppender", category = "Core", elementType = "appender", printObject = true)
public class MyAppender extends AbstractAppender {
    private  Configuration configuration;
    private Property[] properties;

    protected MyAppender(String name, Filter filter, Layout<? extends Serializable> layout,
                                  boolean ignoreExceptions, Property[] properties, AppenderRef[] appenderRefs) {
        super(name, filter, layout, ignoreExceptions, properties);
        this.appenderRefs = appenderRefs;
        this.properties = properties;
    }

    @Override
    public void append(LogEvent event) {
        try {
            // 可以增加处理业务日志逻辑
            try {
                callAppender(event);
            } catch (Exception e) {
                log.error("自定义异常信息", e);
            }
            try {
                // 可以根据日志级别处理日志-如写入数据库
                if (event.getLevel().intLevel() <= Level.ERROR.intLevel()) {}
            } catch (Exception e) {
                log.error("自定义异常信息", e);
            }
        } catch (Exception ex) {
            if (!ignoreExceptions()) {
                throw new AppenderLoggingException(ex);
            }
        }
    }

    @PluginFactory
    public static MyAppender createAppender(@PluginAttribute("name") String name,
                                            @PluginElement("Filter") Filter filter,
                                            @PluginElement("Layout") Layout<? extends Serializable> layout,
                                            @PluginAttribute("ignoreExceptions") boolean ignoreExceptions,
                                            @PluginElement("property") Property[] properties,
                                            @PluginElement("AppenderRef") AppenderRef[] appenderRefs) {
        if (name == null) {
            log.error("No name provided for MyAppenderImpl");
            return null;
        }
        if (layout == null) {
            layout = PatternLayout.createDefaultLayout();
        }
        return new MyAppender(name, filter, layout, ignoreExceptions, properties, appenderRefs);
    }

    private void callAppender(LogEvent event) {
        if (null == configuration) {
            refreshLogcontext();
        }
        for (AppenderRef appenderRef : appenderRefs) {
            if (null == configuration.getAppender(appenderRef.getRef())) {
                log.info("appenderRef null {}", appenderRef.getRef());
                continue;
            }
            recursion(event, appenderRef.getRef());
        }
    }

    private void refreshLogcontext() {
        LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
        this.configuration = loggerContext.getConfiguration();
    }

    private void recursion(LogEvent event, String ref) {
        if (configuration.getAppender(ref) instanceof RollingFileAppender) {
            Appender appender = configuration.getAppender(ref);
            AppenderControl control = new AppenderControl(appender, null, null);
            control.callAppender(event);
        } else if (configuration.getAppender(ref) instanceof AsyncAppender) {
            AsyncAppender asyncAppender = configuration.getAppender(ref);
            if (null != asyncAppender.getAppenderRefStrings()) {
                for (int j = 0; j < asyncAppender.getAppenderRefStrings().length; j++) {
                    // 递归
                    recursion(event, asyncAppender.getAppenderRefStrings()[j]);
                }
            }
        } else if (configuration.getAppender(ref) instanceof ConsoleAppender) {
            Appender appender = configuration.getAppender(ref);
            AppenderControl control = new AppenderControl(appender, null, null);
            control.callAppender(event);
        }
    }
}

代码说明:

1.引入log4j2的插件注解,并声明相关属性的值

@Plugin(name = "MyAppender", category = "Core", elementType = "appender", printObject = true)

  • name:插件的名称。请注意,此名称不区分大小写
  • category:用于放置插件的类别。类别名称区分大小写
  • elementType:此插件所属元素的相应类别的名称,当前扩展所属元素的类别是appender。
  • printObject:指示插件类是否实现Object.toString()方法,消息中使用。

2.如何自定义变量

createAppender方法,用于log4j2在扫描到插件之后根据配置文件中的配置穿件自定义的插件对象。

  • @PluginAttribute:是指插件的属性,如@PluginAttribute("name") String name

       <MyAppender name="MyAppenderTest"></Realtimeval>会取标签内name的值

  • @PluginElement:是指插件的子元素,如@PluginElement("AppenderRef") AppenderRef[] appenderRefs

     <MyAppender name="MyAppenderTest">
        <AppenderRef ref="AsyncMqLog"/>
        <AppenderRef ref="AsyncCONSOLE"/>
    </MyAppender>

     会获取标签下AppenderRef 元素的值,如果是多个AppenderRef 子元素,将会获取都一个数组
可以根据业务需要自定义元素或者属性。


3.如何引入原生的插件,如rollingfile和console

本篇中实现了自定义的标签引入log4j2原生的插件rollingfile和console,并控制原生的rollingfile和console向文件写入和控制台打印。
这一点区分于网上大部分的给出的代码示例,记住生产业务系统不能使用System.out.print,而且要注重高效,最好能用到log4j2自己实现的异步机制来完成我们想要的效果。
其实引入原生的插件rollingfile和console,并使它们发挥作用很简单,只需要call一下就可以,关键代码:

Appender appender = configuration.getAppender(ref);
AppenderControl control = new AppenderControl(appender, null, null);
control.callAppender(event);

4.为何本地自测有效,一到服务器就不生效?

本功能在实现的时候遇到一个难以预料的问题,就是本地怎么测试都好用,但是一部署到服务器就不好用,查过很多资料,不得不说国内的资料千篇一律的居多,参考意义不大。无奈自己搭建了xx谷歌了一下,发现如果你的自定义插件失效,可以从三个方面排查

  • 配置文件configuration的packages属性没有设置或者没有设置成该插件所在的包,注意可以设置多个,用逗号隔开。<configuration monitorInterval="5" status="WARN" name="${app_name}"    packages="com.xxx.log4j2.pattern,com.xxx.util">

  • log4j2的插件是在编译期生成的缓存,而不是启动期间,所以,我们打包的插件需要讲生成的缓存插件.dat打包到jar包中才能生效。关于打包的问题可以自行百度一下,这个问题范围就小多了,一般来说,maven插件都会将插件缓存打包到jar中。用解压缩文件就可以打开jar并查看jar中的文件,该插件缓存在xx.jar\META-INF\org\apache\logging\log4j\core\config\plugins路径下。可以打开文件看到自定义的插件信息:

  • 如果上两个问题都排查过了,而且你也和我一样在代码中用到了logcontext,那么可能的原因是本地的logcontext和服务器上的logcontext加载的顺序不一致,导致服务器上的插件类在加载的时候所获取到的logcontext中的信息是最基础的,只有一个DefaultConsole插件。我的解决思路是调用的时候再去取一次logcontext。

5.我的bean注入是null?明明配置都对

再说一个上下文的问题,log4j2在dao层service层初始化结束之前就已经初始化了,如果采用@Resource这种依赖注入的方式构建bean是行不通的,获取到的只能是null,但是ApplicationContext已经加载(已经扫描哪些带有注解的类),我们可以通过ApplicationContext手动获取bean。
SpringBeanUtil源码:

package com.xxx.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
@Component
@Configuration
public class SpringBeanUtils implements ApplicationContextAware {
    private static ApplicationContext applicationContext;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (SpringBeanUtils.applicationContext == null) {
            SpringBeanUtils.applicationContext = applicationContext;
        }
    }
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }
    public static Object getBean(String name){
        return getApplicationContext().getBean(name);
    }
    public static <T> T getBean(Class<T> clazz){
        return getApplicationContext().getBean(clazz);
    }
    public static <T> T getBean(String name, Class<T> clazz){
        return getApplicationContext().getBean(name, clazz);
    }
    public static <T> T getBean(Class<? extends T> clazz, Class<T> targetClazz){
        return (T)getBean(clazz);
    }
}


用法:
注意第一个入参如果没有指定,默认是小写,这个应该都懂吧,spring的东西了
SpringBeanUtils.getBean("testAppenderDao", TestAppenderDao.class);

log4j2.xml配置

<?xml version="1.0" encoding="utf-8"?>
<configuration monitorInterval="5" status="WARN" name="${app_name}"
               packages="com.xxx.log4j2.pattern,com.xxx.util">
    <properties>
        <property name="LOG_HOME" value="log" />
        <property name="LOG_LEVEL" value="info" />
        <property name="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} |-%-5level [%thread] %c [%L] -| %msg%n</property>
        <property name="FILE_PATTERN">his/app-%d{MM-dd-yyyy}-%i.log</property>
        <property name="FILE_DEADLINE" value="7d" />
    </properties>
    <appenders>
        <Console name="CONSOLE" target="system_out">
            <PatternLayout pattern="${LOG_PATTERN}" />
        </Console>
        <!-- 滚动文件生成 fileName日志文件名称,filePattern压缩文件路径 -->
        <RollingFile name="MqLog" fileName="${LOG_HOME}/logs/rabbitmq/rabbitmq.log"
            <!-- 过滤日志级别 info及以上 -->
            <Filters>
                <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY" />
            </Filters>
            <!-- 指定日志输出格式 -->
            <PatternLayout pattern="${LOG_PATTERN}" />
            <!-- 新文件生成策略,每100M生成一个新文件 -->
            <Policies>
                <TimeBasedTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="100 MB" />
            </Policies>
            <!-- 默认过度策略,不设置,则默认为最多同一文件夹下7个文件 大于此值会删除旧的日志文件 -->
            <DefaultRolloverStrategy>
                <!-- 文件删除策略,basePath扫描路径,maxDepth目录的最大级别数,IfFileName文件名称,IfLastModified指定持续时间(7天前)-->
                <Delete basePath="${LOG_HOME}/logs/rabbitmq" maxDepth="2">
                    <IfFileName glob="*.log" />
                    <IfLastModified age="${FILE_DEADLINE}" />
                </Delete>
                <Delete basePath="${LOG_HOME}/logs/rabbitmq" maxDepth="2">
                    <IfFileName glob="*.log" />
                    <IfAccumulatedFileSize exceeds="${FILE_MAX_SIZE}" />
                </Delete>
            </DefaultRolloverStrategy>
        </RollingFile>
        <Async name="AsyncCONSOLE">
            <AppenderRef ref="CONSOLE"/>
        </Async>
        <Async name="AsyncMqLog">
            <AppenderRef ref="MqLog"/>
        </Async>
        <MyAppender name="MyAppenderTest">
            <AppenderRef ref="AsyncMqLog"/>
            <AppenderRef ref="AsyncCONSOLE"/>
            <property name="logType" value="1"/>
        </MyAppender >
    </appenders>

    <loggers>
        <AsyncLogger name="com.xxx.mq" level="info" additivity="false">
            <AppenderRef ref="MyAppenderTest" />
        </AsyncLogger>
        <root level="${LOG_LEVEL}">
            <AppenderRef ref="AsyncCONSOLE" />
        </root>
    </loggers>
</configuration>


配置说明:

  • 定义了两个appender:CONSOLE、MqLog
  • 异步:AsyncCONSOLE、AsyncMqLog
  • 引入到我们自己的标签中:

        <MyAppender name="MyAppenderTest">
            <AppenderRef ref="AsyncMqLog"/>
            <AppenderRef ref="AsyncCONSOLE"/>
            <property name="logType" value="1"/>
        </MyAppender >

  • 引入到异步Loggers标签中,只对com.xxx.mq这里路径下的日志起作用:

        <AsyncLogger name="com.xxx.mq" level="info" additivity="false">
            <AppenderRef ref="MyAppenderTest" />
        </AsyncLogger>


我们可以使用自己扩展的标签引入不同的console和rollingfile等标签,可以配置不同的属性和元素,也可以监控不同的项目路径以达到业务需要的效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值