错误而不是警告 logback 资源 [logback.xml] 在类路径上多次出现

Error instead of warn on logback Resource [logback.xml] occurs multiple times on the classpath

为了在多个项目之间共享 logback 配置,我们将 logback.xml 文件嵌入到一个公共 jar 中。例如mylogger.jar。项目依赖于此 jar 进行日志记录,因此它始终位于类路径中。这意味着 logback.xml 将在

处找到

https://logback.qos.ch/manual/configuration.html#auto_configuration

但是如果另一个罐子,例如otherlib.jar,还嵌入了一个 logback.xml 文件,我们会看到一个警告

09:27:03,122 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy]
09:27:03,122 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml]
09:27:03,122 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback.xml] at [jar:file:/WEB-INF/lib/mylogger.jar/logback.xml]
09:27:03,123 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs multiple times on the classpath.
09:27:03,123 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [jar:file:/WEB-INF/lib/mylogger.jar/logback.xml]
09:27:03,123 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [jar:file:/WEB-INF/lib/otherlib.jar/logback.xml]

更糟糕的是,有时它不会选择正确的 logback.xml,因为根据 Controlling the classpath in a servlet,这种行为是不确定的。

是否有任何机制可以强制警告使构建失败?这将提醒我们注意上述情况,而可以忽略警告。

在初始化期间,Logback 发出 Status 事件来描述正在发生的事情。这些...

09:27:03,123 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs multiple times on the classpath.
09:27:03,123 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [jar:file:/WEB-INF/lib/mylogger.jar/logback.xml]
09:27:03,123 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [jar:file:/WEB-INF/lib/otherlib.jar/logback.xml]

... 是某些 Status 事件的日志语句。这些 Status 事件由 Logback 的 ContextInitializer ...

发出
if (urlSet != null && urlSet.size() > 1) {
    sm.add(new WarnStatus("Resource [" + resourceName + "] occurs multiple times on the classpath.", loggerContext));
    for (URL url : urlSet) {
        sm.add(new WarnStatus("Resource [" + resourceName + "] occurs at [" + url.toString() + "]", loggerContext));
    }
} 

您可能会看到这些事件被记录下来,因为您已经使用 <configuration debug="true"> 配置了 Logback。使用 debug=true 等同于安装 OnConsoleStatusListener.

您可以注册一个自定义 StatusListener,它对这些 Status 事件的反应不同。鉴于您想要 "force the warning to fail a build" 那么当您的 StatusListener 遇到 "Resource ... occurs multiple times on the classpath." 事件时您可以抛出异常。

这是一个(未经测试的)示例:

import ch.qos.logback.core.status.Status;
import ch.qos.logback.core.status.StatusListener;

public class StrictConfigurationWarningStatusListener implements StatusListener {
    @Override
    public void addStatusEvent(Status status) {
        if (status.getEffectiveLevel() == Status.WARN) {
            // you might want to consider how best to evaluate whether this is the message you are interested in
            // this approach is bound to a string and hence will no longer work if Logback changes this message
            if (status.getMessage().endsWith("occurs multiple times on the classpath.")) {
                throw new LogbackException(status.getMessage());
            }
        }
    }
}

您可以在logback.xml中注册您的监听器,如下所示:

<statusListener class="some.package.StrictConfigurationWarningStatusListener" />

通过上述注册和侦听器,您将能够拦截 "Resource ... occurs multiple times on the classpath." 事件并向它们提供您自己的 action/response。

我的用例:运行 测试时 class 路径上有多个 logback-test.xml 文件

我查看了@glytching 在 中建议的方法,但它有两个问题:

  1. 创建了一些事件(在我的特定用例中) Logback 实例化我的statusListener 之前。就我而言,这就是我们想要在此处做出反应的特定事件的情况。
  2. 从状态侦听器中抛出异常实际上不起作用(它不会传播到调用代码,但会被 Logback 本身捕获和处理)。

基于Logback中的ch.qos.logback.core.status.OnPrintStreamStatusListenerBase class,我想出了解决问题1的方法。

为了解决问题 2,我添加了一个我填充的静态 EVENTS 字段,然后在侦听器中添加了一个 ensureEventsIsEmpty 方法。然后我可以从静态初始化程序调用它(在我的例子中是单元测试 class),如下所示:

public class SomeTestClass {
    private static final Logger logger = LoggerFactory.getLogger( SomeTestClass.class );

    static {
        StrictConfigurationWarningStatusListener.ensureNoEventsReceived();
    }

    // ...test methods goes here.
}

调整后的StrictConfigurationWarningStatusListener实施

package some.package;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.spi.ContextAwareBase;
import ch.qos.logback.core.spi.LifeCycle;
import ch.qos.logback.core.status.Status;
import ch.qos.logback.core.status.StatusListener;
import ch.qos.logback.core.status.StatusManager;

public class StrictConfigurationWarningStatusListener extends ContextAwareBase implements StatusListener, LifeCycle {
    private static final List<String> EVENTS = new CopyOnWriteArrayList<>();

    private boolean isStarted = false;

    public static void ensureNoEventsReceived() {
        if ( !EVENTS.isEmpty() ) {
            for ( String event : EVENTS ) {
                System.err.println( event );
            }

            throw new IllegalStateException( "Multiple logback-test.xml files found on the classpath." );
        }
    }

    @Override
    public void addStatusEvent( Status status ) {
        if ( !isStarted ) {
            // Events being posted at this stage will be retrieved from the StatusManager in the
            // retrospectivelyHandleEvents() method.
            return;
        }

        handleEvent( status );
    }

    @Override
    public void start() {
        isStarted = true;

        retrospectivelyHandleEvents();
    }

    @Override
    public void stop() {
        isStarted = false;
    }

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

    private void handleEvent( Status status ) {
        if ( status.getEffectiveLevel() == Status.WARN ) {
            if ( status.getOrigin() instanceof LoggerContext &&
                    ( status.getMessage().contains( "occurs multiple times" ) ||
                            status.getMessage().contains( "occurs at" ) ) ) {

                EVENTS.add( status.getMessage() );
            }
        }
    }

    private void retrospectivelyHandleEvents() {
        StatusManager statusManager = context.getStatusManager();
        List<Status> statusList = statusManager.getCopyOfStatusList();

        for ( Status status : statusList ) {
            handleEvent( status );
        }
    }
}