教你打印自己的日志 -- 如何自定义 log4j2 各组件

2022-06-27 16:27:38 浏览数 (1)

1. 引言

上一篇文章中,我们介绍了 log4j2 的组件构成:

你知道 log4j2 各项配置的全部含义吗?带你了解 log4j2 的全部组件

可以看到,log4j2 框架为我们提供了非常多的组件,在这些各类功能的 appender 与 layout 以及 filter 的组合下,我们能够实现各种使用场景的处理。

然而,复杂的现实情况是不可能做到完全覆盖的,此时,我们就要考虑自定义一些属于自己的组件来实现相应的功能了。

2. 声明插件 -- @Plugin 注解

要自定义 log4j2 注解,需要在实现类上标记 @Plugin 注解,用来声明插件名、插件类型、节点类型等信息。

代码语言:javascript复制
 @Plugin(name = "ThreadFilter", category = Node.CATEGORY, elementType = Filter.ELEMENT_TYPE, printObject = true)

无论我们要自定义的是哪一个类型的插件,这个注解都是必须的,name 就是我们在 log4j2.xml 中配置时的节点名,elementType 则表示这个类是哪种类型的插件。

3. 自定义 appender

在 log4j2 中,appender 用来定义日志要往哪里打。

如果你需要向一个特殊的位置打印日志,而这个“特殊位置”的访问方法必须由你编写一系列代码来实现,此时,你就可以通过自定义 Appender 来方便的实现。

当然,实际上,在 appender 决定日志打印之前,你也可以对 logEvent 进行一些处理,不过 log4j2 提供了 RewriteAppender 来实现这一功能,你只需要添加处理策略即可。

学习如何自定义 Appender 最好的方法是学习已有的 Appender 是如何实现的,然后只需依葫芦画瓢就可以实现你自己的 Appender 了。

3.1 实例 -- RewriteAppender

下面是 log4j2 提供的 RewriteAppender 的源码:

代码语言:javascript复制
 /*
  * Licensed to the Apache Software Foundation (ASF) under one or more
  * contributor license agreements. See the NOTICE file distributed with
  * this work for additional information regarding copyright ownership.
  * The ASF licenses this file to You under the Apache license, Version 2.0
  * (the "License"); you may not use this file except in compliance with
  * the License. You may obtain a copy of the License at
  *
  *      http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
 package org.apache.logging.log4j.core.appender.rewrite;
 
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 
 import org.apache.logging.log4j.core.Appender;
 import org.apache.logging.log4j.core.Core;
 import org.apache.logging.log4j.core.Filter;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.appender.AbstractAppender;
 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.plugins.Plugin;
 import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
 import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
 import org.apache.logging.log4j.core.config.plugins.PluginElement;
 import org.apache.logging.log4j.core.config.plugins.PluginFactory;
 import org.apache.logging.log4j.core.util.Booleans;
 
 /**
  * This Appender allows the logging event to be manipulated before it is processed by other Appenders.
  */
 @Plugin(name = "Rewrite", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE, printObject = true)
 public final class RewriteAppender extends AbstractAppender {
 
     private final Configuration config;
     private final ConcurrentMap<String, AppenderControl> appenders = new ConcurrentHashMap<>();
     private final RewritePolicy rewritePolicy;
     private final AppenderRef[] appenderRefs;
 
     private RewriteAppender(final String name, final Filter filter, final boolean ignoreExceptions,
                             final AppenderRef[] appenderRefs, final RewritePolicy rewritePolicy,
                             final Configuration config) {
         super(name, filter, null, ignoreExceptions);
         this.config = config;
         this.rewritePolicy = rewritePolicy;
         this.appenderRefs = appenderRefs;
     }
 
     @Override
     public void start() {
         for (final AppenderRef ref : appenderRefs) {
             final String name = ref.getRef();
             final Appender appender = config.getAppender(name);
             if (appender != null) {
                 final Filter filter = appender instanceof AbstractAppender ?
                     ((AbstractAppender) appender).getFilter() : null;
                 appenders.put(name, new AppenderControl(appender, ref.getLevel(), filter));
             } else {
                 LOGGER.error("Appender "   ref   " cannot be located. Reference ignored");
             }
         }
         super.start();
     }
 
     /**
      * Modifies the event and pass to the subordinate Appenders.
      * @param event The LogEvent.
      */
     @Override
     public void append(LogEvent event) {
         if (rewritePolicy != null) {
             event = rewritePolicy.rewrite(event);
         }
         for (final AppenderControl control : appenders.values()) {
             control.callAppender(event);
         }
     }
 
     /**
      * Creates a RewriteAppender.
      * @param name The name of the Appender.
      * @param ignore If {@code "true"} (default) exceptions encountered when appending events are logged; otherwise
      *               they are propagated to the caller.
      * @param appenderRefs An array of Appender names to call.
      * @param config The Configuration.
      * @param rewritePolicy The policy to use to modify the event.
      * @param filter A Filter to filter events.
      * @return The created RewriteAppender.
      */
     @PluginFactory
     public static RewriteAppender createAppender(
             @PluginAttribute("name") final String name,
             @PluginAttribute("ignoreExceptions") final String ignore,
             @PluginElement("AppenderRef") final AppenderRef[] appenderRefs,
             @PluginConfiguration final Configuration config,
             @PluginElement("RewritePolicy") final RewritePolicy rewritePolicy,
             @PluginElement("Filter") final Filter filter) {
 
         final boolean ignoreExceptions = Booleans.parseBoolean(ignore, true);
         if (name == null) {
             LOGGER.error("No name provided for RewriteAppender");
             return null;
         }
         if (appenderRefs == null) {
             LOGGER.error("No appender references defined for RewriteAppender");
             return null;
         }
         return new RewriteAppender(name, filter, ignoreExceptions, appenderRefs, rewritePolicy, config);
     }
 }

3.2 源码解析

Appender 继承自 AbstractAppender 类,主要包含以下方法:

  1. void start() -- 初始化方法。
  2. void append(LogEvent event) -- 实现 appender 功能的核心方法。
  3. RewriteAppender createAppender -- 用于创建 Appender 实例的工厂方法。

通过继承 AbstractAppender 或其他 AbstractAppender 的子类,就可以实现一个 Appender 的创建。

通过 createAppender 的 @PluginAttribute 注解参数,就可以实现自定义参数的传递了,而 @PluginElement 则用来接收 xml 子元素作为配置。

3.3 让 Appender 使用 Spring 维护的 bean

当我们在 Spring 框架中使用 log4j2 框架时,可能你想要让 Appender 接收外部的 spring bean。

事实上,一个类只要是在 spring 框架中执行,他势必是可以通过 Spring 上下文获取到所有已经被 spring 纳入管理的 bean,最简单的方法就是让这个类实现 ApplicationContextAware 接口,例如:

代码语言:javascript复制
 @Component
 public class SslErrorSecurityAppender extends AppenderSkeleton implements ApplicationContextAware {
 
     private static SecurityLogger securityLogger;
 
     @Override
     protected void append(LoggingEvent event) {
         securityLogger.log(new SslExceptionSecurityEvent(SecurityEventType.AUTHENTICATION_FAILED, event.getThrowableInformation().getThrowable(), "Unexpected SSL error"));
     }
 
     @Override
     public boolean requiresLayout() {
         return false;
     }
 
     @Override
     public synchronized void close() {
         this.closed = true;
     }
 
 
     @Override
     public void setApplicationContext(ApplicationContext applicationContext) {
         if (applicationContext.getAutowireCapableBeanFactory().getBean("securityLogger") != null) {
             securityLogger = (SecurityLogger) applicationContext.getAutowireCapableBeanFactory().getBean("securityLogger");
         }
     }
 }

这个示例中,appender 获取了 spring 容器中名为 securityLogger 的 bean。

4. 自定义 Layout

与 Appender 非常类似,Layout 我们也可以进行自定义。

Log4j2 包中也有丰富的 layout 供我们使用和参考,他们继承自 AbstractStringLayout 类。

4.1 Layout 创建的工厂方法 -- @PluginFactory

通过 @PluginFactory 注解一个返回当前 Layout 对象实例的 static 方法,我们可以实现一个用来创建当前 Layout 对象的工厂方法:

代码语言:javascript复制
 @PluginFactory
 public static XmlLayout createLayout(
   // @formatter:off
   @PluginAttribute(value = "locationInfo") final boolean locationInfo,
   @PluginAttribute(value = "properties") final boolean properties,
   @PluginAttribute(value = "complete") final boolean complete,
   @PluginAttribute(value = "compact") final boolean compact,
   @PluginAttribute(value = "charset", defaultString = "UTF-8") final Charset charset,
   @PluginAttribute(value = "includeStacktrace", defaultBoolean = true) final boolean includeStacktrace)
   // @formatter:on
 {
   return new XmlLayout(locationInfo, properties, complete, compact, charset, includeStacktrace);
 }

这是 XmlLayout 的一个示例。

4.2 更推荐使用的 Layout 创建方法 -- 通过 Builder

通过 Builder 创建 Layout 是一个更为推荐的方法,他是 Builder 设计模式的应用,使用 Builder 模式创建对象有几个好处:

  1. 相比于构造方法,Builder 模式更加灵活,你可以选择初始化任何参数,而不是必须传递构造方法指定的参数。
  2. 易于扩展,你可以随时为 Builder 类加入需要的字段,而不需要创建新的构造方法来初始化这些新加入的字段。
  3. 由于构造器的独立性,你可以轻易控制构建出来的对象的生命周期。

通过在 Layout 中创建 static 的内部 Builder 类就可以实现构建者模式了,通过 @PluginBuilderFactory 注解标记 Builder 实例化的工厂类:

代码语言:javascript复制
 @PluginBuilderFactory
 public static Builder newBuilder() {
   return new Builder();
 }

static Builder 类需要实现 log4j 的 Builder 接口,复写 build 方法,用来实现对 Layout 对象进行实例化。

4.3 LogEvent 序列化

Layout 的功能就是对日志内容进行格式化,因此 Layout 的核心方法就是 LogEvent 序列化方法 toSerializable:

代码语言:javascript复制
 T toSerializable(LogEvent event);

通过解析 event,然后按照预定格式输出为 String 即可,例如:

代码语言:javascript复制
 @Override
 public String toSerializable(LogEvent event) {
   return "hello world"   event.getMessage().getFormattedMessage();
 }

5. 自定义 filter

很多时候,我们并不是所有的日志都需要进行打印,同时,我们也可能需要根据日志内容需要使用不同的 appender 进行分类打印,此时 Filter 组件就派上了用场。

虽然 log4j2 已经提供了丰富的 Filter 实现,我们仍然有可能有自己定制 Filter 的需要。

要自定义 Filter,我们需要继承 AbstractFilter 类。

对于一个 Filter 来说,至少要接收两个参数:

  1. onMatch -- 命中 filter 条件时执行的动作。
  2. onMisMatch -- 未命中 filter 条件时执行的动作。

5.1 Filter 实例化 -- @PluginFactory

与上述两个组件一样,Filter 也需要使用工厂方法来进行实例化,只需要在这个 public static 方法上标记 @PluginFactory 注解即可:

代码语言:javascript复制
 @PluginFactory
 public static MapFilter createFilter(
   @PluginElement("Pairs") final KeyValuePair[] pairs,
   @PluginAttribute("operator") final String oper,
   @PluginAttribute("onMatch") final Result match,
   @PluginAttribute("onMismatch") final Result mismatch) {
   if (pairs == null || pairs.length == 0) {
     LOGGER.error("keys and values must be specified for the MapFilter");
     return null;
   }
   return new MapFilter(map, isAnd, match, mismatch);
 }

5.2 过滤方法 -- filter

AbstractFilter 类声明了 14 个各种参数重载的 filter 方法,并且提供了默认实现。

除了 Result filter(LogEvent event) 方法外,其他方法的默认实现都是调用 Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t),因此我们可以只复写这两个方法,就可以实现所有 filter 方法的实现了。

filter 方法以 LogEvent 对象作为参数,返回 Result 枚举类型对象,通常我们在命中规则后返回经由外部传入的 onMatch 参数,在未命中规则后返回外部传入的另一参数 onMisMatch 即可,但也可以指定具体的 Result 值。

Result 包含三个枚举值:

  1. ACCEPT -- 允许打印
  2. DENY -- 不允许打印
  3. NEUTRAL -- 如果级联多个 Filter,并且当前不是最后一个 Filter,那么传递给下一 Filter 继续判断,否则允许打印。

下面是一个根据线程名判断是否允许打印的例子:

代码语言:javascript复制
 @Plugin(name = "ThreadFilter", category = Node.CATEGORY, elementType = Filter.ELEMENT_TYPE, printObject = true)
 public class ThreadFilter extends AbstractFilter {
 
     private String threadName;
     private Result onMatch;
     private Result onMisMatch;
     private Level level;
 
     public ThreadFilter(String threadName, Result onMatch, Result onMisMatch, Level level) {
         super();
         this.threadName = threadName;
         this.onMatch = onMatch;
         this.onMisMatch = onMisMatch;
         this.level = level;
     }
 
     @PluginFactory
     public static ThreadFilter createFilter(@PluginAttribute("threadName") final String threadName,
                                             @PluginAttribute("onMatch") final Result match,
                                            @PluginAttribute("onMismatch") final Result mismatch,                                        @PluginAttribute("level") final Level level) {
         assert StringUtils.isNotBlank(threadName);
         final Result onMatch = match == null ? Result.NEUTRAL : match;
         final Result onMismatch = mismatch == null ? Result.DENY : mismatch;
         return new ThreadFilter(threadName, onMatch, onMismatch, level);
     }
 
     @Override
     public Result filter(LogEvent event) {
         if (event.getLevel().name().equals(this.level.name()) && event.getThreadName().startsWith(threadName)) {
             return this.onMatch;
         }
         return this.onMisMatch;
     }
 
     @Override
     public Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t) {
         return super.filter(logger, level, marker, msg, t);
     }
 }

0 人点赞