日志框架冲突引发的问题及思考

问题

最近遇到了一个奇怪的问题,在线上发布应用时,有一部分机器能正常运行,有一部分起不来。代码部署的都是同一套,应用是tomcat中部署的SpringBoot war包,应用日志看不到,查看tomcat日志,发现报错信息为:

Exception in thread "main" java.lang.IllegalArgumentException: LoggerFactory is not a Logback LoggerContext but Logback is on the classpath. Either remove Logback or the competing implementation (class org.apache.logging.slf4j.Log4jLoggerFactory loaded from ......WEB-INFO/lib/log4j-slf4j-impl-2.13.3.jar). If you are using WebLogic you will need to add 'org.slf4j' to prefer-application-packages in WEB-INF/weblogic.xml: org.apache.logging.slf4j.Log4jLoggerFactory
    at org.springframework.util.Assert.instanceCheckFailed(Assert.java:699)
    at org.springframework.util.Assert.isInstanceOf(Assert.java:599)
    at org.springframework.boot.logging.logback.LogbackLoggingSystem.getLoggerContext(LogbackLoggingSystem.java:284)
    at org.springframework.boot.logging.logback.LogbackLoggingSystem.beforeInitialize(LogbackLoggingSystem.java:104)
......

看提示信息是日志框架冲突了,我们用的是logback却在新版本中无意中引入了log4j的jar包,排除掉log4j的相关引用后都可以正常发布运行了!

问题虽然找到原因也解决了,但是为什么这个问题会引发异常还不是很清楚,为什么有的机器可以有的不行也很奇怪,正好也学习下日志框架。

观察到有一段SLF4J的日志,提示有多个日志实现框架,报错的机器Actual binding都是log4j,不报错的是logback。
启动成功的日志:

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:../ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:.../org/apache/logging/log4j/log4j-slf4j-impl/2.13.3/log4j-slf4j-impl-2.13.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]

启动失败的日志:

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:.../org/apache/logging/log4j/log4j-slf4j-impl/2.13.3/log4j-slf4j-impl-2.13.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:../ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory]

就先研究下SLF4J。

SLF4J

slf4j是日志接口层,提供了日志的统一接口,可以有不同的日志实现。
slf4j通过LoggerFactory.getLogger方法返回Logger接口供上层使用,我们通过源码来看下是如何实现绑定不同的日志框架的。以下代码来自slf4j-api-1.7.25:

public static Logger getLogger(Class<?> clazz) {
        Logger logger = getLogger(clazz.getName());
        ......下面省略非主流程......
}

public static Logger getLogger(String name) {
        ILoggerFactory iLoggerFactory = getILoggerFactory();
        return iLoggerFactory.getLogger(name);
    }

使用的工厂模式来创建日志对象,那关键就是看如何获取的不同日志工厂了。

public static ILoggerFactory getILoggerFactory() {
        if (INITIALIZATION_STATE == UNINITIALIZED) {
            synchronized (LoggerFactory.class) {
                if (INITIALIZATION_STATE == UNINITIALIZED) {
                    INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                    performInitialization();
                }
            }
        }
        ......下面省略非主流程......
}

private final static void performInitialization() {
        bind();
        if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
            versionSanityCheck();
        }
    }

private final static void bind() {
        try {
            Set<URL> staticLoggerBinderPathSet = null;
            // skip check under android, see also
            // http://jira.qos.ch/browse/SLF4J-328
            if (!isAndroid()) {
                staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
                reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
            }
            // the next line does the binding
            StaticLoggerBinder.getSingleton();
            INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
            reportActualBinding(staticLoggerBinderPathSet);
            ......下面省略非主流程......
        }
}

可以看到,绑定日志实现类的流程在bind方法里面,reportMultipleBindingAmbiguity和reportActualBinding就是打印上面SLF4J日志的地方了。但是具体是如何绑定的呢?

            // the next line does the binding
            StaticLoggerBinder.getSingleton();

纳尼?StaticLoggerBinder这个类的实现不存在slf4j的jar包中啊,为什么调用一下获取的单例方法就能够绑定日志框架了?
这里是通过java的类加载机制,在slf4j框架中不仅定义了绑定接口(LoggerFactoryBinder),还固定了它的实现类必须得是org.slf4j.impl.StaticLoggerBinder,也就是说,需要绑定slf4j的日志实现框架,不仅要实现LoggerFactoryBinder接口,而且还必须得定义org.slf4j.impl.StaticLoggerBinder这个类来实现,这样在slf4j绑定的时候就可以通过类名来加载这个类了,哪个先被类加载器加载到,就绑定的那个实现框架。不知道这里为啥这样设计,一般定义好接口,获取外部可拓展的实现类时,都是通过java SPI机制来实现的,SLF4J这样设计猜测可能是出于性能考虑。

原因

上面看到SLF4J绑定日志框架是看先加载到哪个类,而绑定到logback时应用正常启动,绑定到log4j时应用启动失败。而失败堆栈是springboot打印出来的,猜测应该是springboot适配日志系统的问题。
出错处代码在LogbackLoggingSystem中:

private LoggerContext getLoggerContext() {
        ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory();
        Assert.isInstanceOf(LoggerContext.class, factory,
                String.format(
                        "LoggerFactory is not a Logback LoggerContext but Logback is on "
                                + "the classpath. Either remove Logback or the competing "
                                + "implementation (%s loaded from %s). If you are using "
                                + "WebLogic you will need to add 'org.slf4j' to "
                                + "prefer-application-packages in WEB-INF/weblogic.xml",
                        factory.getClass(), getLocation(factory)));
        return (LoggerContext) factory;
    }

也就是springboot在获得LoggingSystem取到的是LogbackLoggingSystem,所以在LoggerBinder绑定Logback的时候正常,而在绑定Log4j的时候会抛异常。这就是只有一部分机器启动失败的原因了。那么为什么LoggingSystem会是LogbackLoggingSystem呢,再看下获取LoggingSystem的代码(spring-boot-2.3.2.RELEASE)

public static LoggingSystem get(ClassLoader classLoader) {
        String loggingSystem = System.getProperty(SYSTEM_PROPERTY);
        if (StringUtils.hasLength(loggingSystem)) {
            if (NONE.equals(loggingSystem)) {
                return new NoOpLoggingSystem();
            }
            return get(classLoader, loggingSystem);
        }
        return SYSTEMS.entrySet().stream().filter((entry) -> ClassUtils.isPresent(entry.getKey(), classLoader))
                .map((entry) -> get(classLoader, entry.getValue())).findFirst()
                .orElseThrow(() -> new IllegalStateException("No suitable logging system located"));
    }

可以看到是先从系统属性中取loggingSystem,没有的话再从SYSTEMS变量中遍历当前类存在的。SYSTEMS是这样初始化的:

private static final Map<String, String> SYSTEMS;
    static {
        Map<String, String> systems = new LinkedHashMap<>();
        systems.put("ch.qos.logback.core.Appender", "org.springframework.boot.logging.logback.LogbackLoggingSystem");
        systems.put("org.apache.logging.log4j.core.impl.Log4jContextFactory",
                "org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");
        systems.put("java.util.logging.LogManager", "org.springframework.boot.logging.java.JavaLoggingSystem");
        SYSTEMS = Collections.unmodifiableMap(systems);
    }

由于没有设置SYSTEM_PROPERTY的系统属性,而SYSTEMS中第一项是logback,同时logback的jar包也在类路径中,所以LoggingSystem每次取到的都是LogbackLoggingSystem了。至此问题出现的原因已找到,同时也找到了另外一种修改的方式,即通过设置系统属性来让springboot不使用日志系统输出日志:

System.setProperty(LoggingSystem.class.getName(), "none");

经测试果然也能避免异常的发生。

猜测

现在问题的原因基本已找到,但还有一个最根本的问题没有解决,那就是,为什么有的机器是先加载的Log4j的StaticLoggerBinder类,有的先加载的是Logback中的。这个问题暂时还没有找到答案,只能有一些大概的猜测,哪位大佬如果有想法可以留言交流下。
以下分析tomcat版本为8.0.30:
Tomcat中Web应用的类加载器使用的是WebappClassLoader来加载WEB-INF/lib中的jar包,在数据完全一致的时候为什么会出现不一样的结果呢?查看WebappClassLoader加载类的流程,发现是将lib中的jar包封装成WebResourceSet存到WebResourceRoot中,而加载lib中的jar的代码在StandardRoot.processWebInfLib:

protected void processWebInfLib() throws LifecycleException {
        WebResource[] possibleJars = listResources("/WEB-INF/lib", false);

        for (WebResource possibleJar : possibleJars) {
            if (possibleJar.isFile() && possibleJar.getName().endsWith(".jar")) {
                createWebResourceSet(ResourceSetType.CLASSES_JAR,
                        "/WEB-INF/classes", possibleJar.getURL(), "/");
            }
        }
    }

jar包加载的顺序是listResources决定的,里面会调用File.list()方法来获取目录中的所有文件,而java中这个方法的注释有说:

  There is no guarantee that the name strings in the resulting array
     * will appear in any specific order; they are not, in particular,
     * guaranteed to appear in alphabetical order.
      ...还有一些别的注释...
      public String[] list() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkRead(path);
        }
        if (isInvalid()) {
            return null;
        }
        return fs.list(this);
    }

翻译过来也就是说不保证返回的顺序,尤其是不保证安字母顺序返回,我猜测这就是加载类顺序部固定的原因,但是也have no guarantee😂

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,084评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,623评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,450评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,322评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,370评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,274评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,126评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,980评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,414评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,599评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,773评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,470评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,080评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,713评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,852评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,865评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,689评论 2 354

推荐阅读更多精彩内容