Spring Boot中从自定义Logback访问Spring Bean三种方法

讨论了在 Spring Boot 应用程序中从自定义 Logback 应用程序访问 Spring Bean 所面临的挑战,并提供了三种解决方案来解决这一问题。

什么是 Logback?
Logback是一个用于 Java 应用程序的日志框架,旨在比其前身 Log4j 1.x 更快、功能更丰富。
通过提供新的通用架构,Logback 适用于广泛的用例。
它以其性能和灵活性而闻名。以下是 Logback 的一些主要优点:

  • 配置灵活:Logback 配置可以是XML或Groovy配置文件。它还支持配置文件的自动重新加载。
  • 强大的过滤功能:Logback 事件过滤允许我们控制捕获和处理的日志消息。
  • 模块化和可扩展性:Logback 是模块化和可扩展的,因此可以轻松添加自定义附加程序、过滤器和其他组件。
  • 支持多种日志记录 API:Logback 支持 Java 中的多种日志记录 API,包括SLF4J、Commons Logging和Java Util Logging API。

以上所有特性使得 Logback 成为 Java 生态系统中流行的框架,Spring Boot 也不例外。

Spring Boot 中默认日志记录选项?
前面提到过,Logback 是 Spring Boot 中默认的日志框架,我们不需要进行任何配置就可以使用它。通常情况下,默认配置就可以了。

Spring Boot 中日志记录的另一个重要方面是,SLF4J 被用作 Logback 或我们希望在 Spring Boot 中使用的任何其他日志记录框架之上的门面层。

因此,在不同日志框架之间切换非常容易。此外,由于 Spring Boot 使用 Commons Logging API 进行所有内部日志记录,因此我们更容易选择其他日志记录框架。其他选项包括


Spring Boot 如何初始化和配置 Logback?
由于 Logback在配置文件中同时支持XML或Groovy语法,因此 Spring Boot将从类路径的根目录或 Spring 配置指定的位置选择具有这些名称( logback-spring.xml、logback-spring.groovy、logback.xml或)的文件。

Logback 配置不能指定为Spring Boot 文档中所述的 Spring 配置的原因:

  • 由于日志记录是在创建 ApplicationContext 之前初始化的,因此无法通过 Spring @Configuration 文件中的 @PropertySources 控制日志记录。
  • 更改日志记录系统或完全禁用日志记录系统的唯一方法是通过系统属性。

Spring Boot 中其他日志框架的文件名是

  • 用于 Log4J2 的 log4j2-spring.xml 或 log4j2.xml
  • 用于 Java Util Logging 的 logging.properties


什么是 Logback Appender?
Logback 架构中的三个主要组件是:Logger , Appender和Layout 。简而言之:

  • Logger是用于创建日志消息的接口。Appender 将日志消息发送到目标,并且一个Logger可以有多个附加器。最后,事实证明,Layout负责在将日志消息发送到目标之前对其进行格式化。

Logback 提供了几种现成的附加程序,例如ConsoleAppender,FileAppender或RollingFileAppender。我们还可以轻松实现自己的自定义附加程序,并在配置文件中将其注册到 Logback。

问题
Logback 无法访问ApplicationContext:

出现类似:

10:41:05,660 |-ERROR in com.saeed.springlogbackappender.NotificationAppender[NOTIFY] - Appender [NOTIFY] failed to append. java.lang.NullPointerException: Cannot invoke "com.saeed.springlogbackappender.Notifier.notify(String)" because "this.notifier" is null
 at java.lang.NullPointerException: Cannot invoke "com.saeed.springlogbackappender.Notifier.notify(String)" because "this.notifier" is null

或错误:

10:53:57,887 |-ERROR in ch.qos.logback.core.model.processor.AppenderModelHandler - Could not create an Appender of type [com.saeed.springlogbackappender.NotificationAppender]. ch.qos.logback.core.util.DynamicClassLoadingException: Failed to instantiate type com.saeed.springlogbackappender.NotificationAppender
Caused by: java.lang.NoSuchMethodException: com.saeed.springlogbackappender.NotificationAppender.<init>()

这是因为:
Logback 需要一个默认构造函数来初始化 自定义Appender。

如果我们将默认构造函数添加到bean中,我们就在获得这个 Bean 遭遇 NullPointerException,因为现在我们的应用程序中有两个 自定义Appender 实例:

  • 一个由 Logback 实例化和管理
  • 另一个由 ApplicationContext 实例化和管理!

现在,我们想通过提供三种解决方案来解决这个问题。我在GitHub:spring-logback-appender中创建了一个名为的 Spring Boot 项目,并为每个解决方案创建了单独的提交。

1- Spring Boot 创建 bean 并在 @PostConstruct 中动态将其添加为 Logback 附加器
在这种方法中,我们将 定义NotificationAppender为 Spring bean,因此我们可以毫无问题地将每个 Spring bean 注入其中。但是正如我们之前在问题陈述中看到的那样,我们如何将这个 Spring bean 作为附加器引入到 Logback?我们将使用 以编程方式执行此操作LoggerContext:

@Component
public class NotificationAppender extends AppenderBase<ILoggingEvent> {

    private final Notifier notifier;

    public NotificationAppender(Notifier notifier) {
        this.notifier = notifier;
    }

    @Override
    protected void append(ILoggingEvent loggingEvent) {
        notifier.notify(loggingEvent.getFormattedMessage());
    }

    @PostConstruct
    public void init() {
        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
        Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
        rootLogger.addAppender(this);
        setContext(context);
        start();
    }
}


这将起作用,并且如果我们调用/helloAPI,我们将看到Notifier将使用附加程序进行通知。
对我来说,这种方法有一些缺点:

  • 由于附加器无法在文件中配置,因此灵活性较logback-spring.xml差。
  • 在Spring boot启动初期我们会遗漏一些日志。

2- Logback 创建附加器,然后使用 ApplicationContexAware 在自定义附加器中填充 bean 依赖项
在这种方法中,为了解决第一种方法的一个重要缺陷,我们将以标准方式注册 Logback 附加器,将其添加到 logback-spring.xml 文件中。

<configuration debug="true">
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml" />

    <appender name="NOTIFY" class="com.saeed.springlogbackappender.NotificationAppender"/>

    <logger name="org.springframework.web" level="DEBUG"/>

    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="NOTIFY" />
    </root>
</configuration>

我们需要做的另一个改动是让 Notifier 字段成为静态,并让 NotificationAppender 实现 ApplicationContextAware:

@Component
public class NotificationAppender extends AppenderBase<ILoggingEvent> implements ApplicationContextAware {

    private static Notifier notifier;

    @Override
    protected void append(ILoggingEvent loggingEvent) {
        if (notifier != null)
            notifier.notify(loggingEvent.getFormattedMessage());
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        notifier = applicationContext.getAutowireCapableBeanFactory().getBean(Notifier.class);
    }

}


这种方法的结果与第一种方法类似,但现在可以使用 Logback 中的标准方法配置附加器。
这种方法仍有一些缺点:

  • 我们需要检查通知器是否为空,并使其成为静态。
  • 由于注入的类不会在 Spring ApplicationContext 完全加载之前出现,因此我们会在应用程序启动的早期阶段错过一些日志。

3-如上所述,您可能希望不要丢失 Spring Boot 启动期间记录的事件
在第三种也是最后一种方法中,我们将集中精力解决应用程序启动初期丢失日志的问题。对于这种方法,我受到了StackOverflow 上这个问题的启发,将之前的方法混合起来并创建一个新AppenderDelegator类。

在这种方法中,我们将定义两个附加器:
AppenderDelegator:在 Logback 配置文件中注册为附加器 ( logback-spring.xml)。此附加器是我们的主要附加器,充当委托人,并有一个缓冲区来存储日志事件,以备实际记录器尚未准备好记录时使用。

public class AppenderDelegator<E> extends UnsynchronizedAppenderBase<E> {

    private final ArrayList<E> logBuffer = new ArrayList<>(1024);
    private Appender<E> delegate;

    @Override
    protected void append(E event) {
        synchronized (logBuffer) {
            if (delegate != null) {
                delegate.doAppend(event);
            } else {
                logBuffer.add(event);
            }
        }
    }

    public void setDelegateAndReplayBuffer(Appender<E> delegate) {
        synchronized (logBuffer) {
            this.delegate = delegate;
            for (E event : this.logBuffer) {
                delegate.doAppend(event);
            }
            this.logBuffer.clear();
        }
    }
}


NotificationAppender:这是我们实际使用的 appender,它通过编程方式进行配置,并使用 Spring SmartLifecycle 对其生命周期进行更多控制。我们将在组件启动生命周期中将此应用程序连接到委托程序:

@Component
public class NotificationAppender extends AppenderBase<ILoggingEvent> implements SmartLifecycle {

    private final Notifier notifier;

    public NotificationAppender(Notifier notifier) {
        this.notifier = notifier;
    }

    @Override
    protected void append(ILoggingEvent loggingEvent) {
        notifier.notify(loggingEvent.getFormattedMessage());
    }

    @Override
    public boolean isRunning() {
        return isStarted();
    }

    @Override
    public void start() {
        super.start();
        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
        Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
        AppenderDelegator<ILoggingEvent> delegate = (AppenderDelegator<ILoggingEvent>) rootLogger.getAppender("DELEGATOR");
        delegate.setDelegateAndReplayBuffer(this);
    }
}


值得一提的是,与第二种方法不同,我们不会将其添加NotificationAppender到 LoggerContext 的根记录器。
这种方法是最复杂的,但它在附加器中提供了最大的灵活性和日志覆盖率。

最后的思考
我们讨论了在 Spring Boot 应用程序中从自定义 Logback 应用程序访问 Spring Bean 所面临的挑战,并提供了三种解决方案来解决这一问题。第二种方法通常会奏效。