Spring Boot中從自定義Logback訪問Spring Bean三種方法

banq發表於2024-06-12

討論了在 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 進行所有內部日誌記錄,因此我們更容易選擇其他日誌記錄框架。其他選項包括

  • Log4J2
  • Java Util Logging


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 <font>"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=<font>"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(<font>"DELEGATOR");
        delegate.setDelegateAndReplayBuffer(this);
    }
}


值得一提的是,與第二種方法不同,我們不會將其新增NotificationAppender到 LoggerContext 的根記錄器。
這種方法是最複雜的,但它在附加器中提供了最大的靈活性和日誌覆蓋率。

最後的思考
我們討論了在 Spring Boot 應用程式中從自定義 Logback 應用程式訪問 Spring Bean 所面臨的挑戰,並提供了三種解決方案來解決這一問題。第二種方法通常會奏效。

相關文章