使用 Jakarta Commons Logging 时遇到的类加载器问题的分类

之前在 《类加载器 ClassLoader 实现 jar 文件版本加载隔离验证样例》 一文中讲到过,我正在为 ETL 项目的某些模块做动态加载分离,主要是为了隔离五花八门的第三方依赖库冲突,并且可以按需加载。

但是最近在支持 apache hadoop 相关的仓库的时候,遇到了加载器父子混乱的问题。

我们知道 JDK 默认的父类加载器优先委派机制,但是在 Java 周边的产品组件中,诸如 JDBC、JNDI 等都是使用自身的加载器去加载包和资源的,并没有将只能委派给父类。这种单纯的例外还是很好解决的。

apache 的组件库可以说是良莠不齐吧,本人一直不喜欢使用 common-logging (JCL),一直在使用的是 slf4j 接口,但是 apache 内部组件库都是使用 JCL 的,它有个致命的缺陷就是类加载器的问题,如果你使用过 OSGi,并且使用了 JCL,你肯定遇到过类似这样的错误:

Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/logging/LogFactory
	at org.springframework.context.support.AbstractApplicationContext.<init>(AbstractApplicationContext.java:159)
	at org.springframework.context.support.AbstractApplicationContext.<init>(AbstractApplicationContext.java:223)
	at org.springframework.context.support.AbstractRefreshableApplicationContext.<init>(AbstractRefreshableApplicationContext.java:88)
	at org.springframework.context.support.AbstractRefreshableConfigApplicationContext.<init>(AbstractRefreshableConfigApplicationContext.java:58)
	at org.springframework.context.support.AbstractXmlApplicationContext.<init>(AbstractXmlApplicationContext.java:61)
	at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:136)
	at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83)
	at com.liu.UserTest.main(UserTest.java:14)
Caused by: java.lang.ClassNotFoundException: org.apache.commons.logging.LogFactory
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 8 more

但是你明明就不缺任何 jar 包,JVM 就是像故意的一样一遍又一遍的给你抛出这样的错误。

原因在于我们的子加载器加载 JCL 接口,JCL 接口使用当前或父类加载器加载接口实现,这就导致我们的子加载器的类不能访问 JCL 实现。这样的场景和原因有很多,这只是其中一种。

正好看到 QOS 上对 JCL 类加载器问题的分析,应该对大家在类加载这块有所帮助,所以花点时间翻译了一下。

下面是原文:

原文作者:Ceki Gülcü
原文地址:https://articles.qos.ch/classloader.html

致谢

我要感谢 Jacob Kjome 审阅本文件的早期草稿。 他的评论有助于澄清几个重点。 Jake 还在 log4j-dev 邮件列表中提醒我们,子父组委托模型并不是唯一的模型,parent-first 委托模型还很好的存在着。 Niclas Hedhman 和 Yoav Shapira 也提出了非常有价值的建议。 Scott Deboy,Paul Delano,Robert Burrell Donkin,Frank-Olaf Lohmann,Dugald Morrow,Nain Hwu,Toni Price,Peter Royal,Richard Lawrence 和 Henning Schmiedehausen 都善意地指出了印刷和语法性质的错误。

错误报告

我试图让这个文件没有错误; 但是,我很乐意向任何错误的初始报告者提供 The complete log4j manual 的副本,无论是技术,印刷还是其他方面。 我们强烈建议您将评论或建议发送至 ceki@qos.ch

如果您对 JCL的错误不感兴趣,但正在寻找当前问题的快速解决方案,您可以跳到标题为短期解决方案的部分。

JCL 版本

本文使用 Jakarta Commons Logging 1.0.4 版进行了测试。 提出了几个建议来纠正 JCL 代码中发现的问题。 但是,到目前为止还没有取得任何实际进展。

介绍

本文档试图系统地分类使用 Jakarta Commons Logging 时遇到的问题类型,此后缩写为 JCL。 在阅读了 Richard Sitze 关于 JCL 发现机制的类加载器问题的分析之后,我发现虽然大多数人都同意 JCL 的动态发现机制存在问题,但要掌握它们并不容易。 希望本文档附带的示例代码可以让读者轻松地重现至少基本的错误。 正如 Donald Knuth 曾经指出的那样,我们都学到了我们自己发现的东西。 我希望你能轻松复制 JCL 的错误条件,为广大公众更好地理解问题领域铺平道路。

为方便读者,运行本文档中示例所需的所有源代码以及类和 jar 文件都打包在一个名为 cl-problems.zip 的文件中。

如果你有时间,我建议你花一个下午的时间来完成实例。 仅通过阅读文章就不可能对这个主题有所了解。

假设读者对 Java 类加载器的工作原理有基本的了解。 为了唤醒你的这些记忆,我建议您阅读以下文章。(译注:这几篇文章都很不错,基本涉及了 Java 类加载器的基础知识、分类和实际使用场景,如果大家感兴趣,我后面可以翻译过来。)

问题类型

使用 JCL 时遇到的类加载问题分为三大类:

  • Type-I:当一个类对父类加载器不可见,即使对子类加载器可见,抛出 java.lang.NoClassDefFoundError
  • Type-II:非兼容性赋值,即两类虽然相同或存在父子关系,但是由于 ClassLoader 不一样而导致不可以相互赋值。
  • Type-III:保持对给定类加载器的引用将阻止该类加载器加载的资源被垃圾收集。

至少在表面层次上,Type-I 问题相对容易理解,而 Type-II 问题乍一看有点难以理解。 因此,我们将首先说明 Type-I 的问题。 鉴于它们的不同性质,将在本文件末尾简要讨论第三类问题。

类加载器树

父类优先委托模型

Parent-first delegation model

您可能知道,类加载器通常具有对其父类的引用。 因此,类加载器可以被安排到类加载器的树中。为了检索资源,类加载器通常首先委托给它的父类。Classloader 类的 javadoc 文档说明:

ClassLoader 类使用委派模型来搜索类和资源。 ClassLoader 的每个实例都有一个关联的父类加载器。当调用查找类或资源时,ClassLoader 实例会在尝试查找类或资源本身之前将对类或资源的搜索委托给其父类加载器。虚拟机的内置类加载器(称为引导类加载器)没有父级,但可以作为 ClassLoader 实例的父级。

因此,父类优先委托是 Java 中的默认和推荐委托模型。

子类优先委托模型

Child-first delegation model

但是,在某些情况下,子类加载器可能更愿意首先尝试自己加载资源,然后才能委托给它的父级。 我们将这种类加载器称为子类优先委托模型。 Java Servlet 规范版本 2.4 声明如下:

SRV.9.7.2 Web Application Classloader

容器用于在 WAR 中加载 servlet 的类加载器必须允许开发人员使用 getResource 在正常的 J2SE 语义之后加载 WAR 中的 JAR 库中包含的任何资源。它不能允许 WAR 覆盖 J2SE 或 Java servlet API 类。 进一步建议加载器不允许 WAR 中的 servlet 访问 Web 容器的实现类。建议实现应用程序类加载器,以便将 WAR 中打包的类和资源加载到驻留在容器范围的 JAR 库中的类和资源中。

请注意,Servlet 规范仅建议使用子优先授权顺序。 实际上,并非所有 Servlet 容器都实现了子优先授权模型。 两种代表团模型在实践中都相当普遍。

两种模型的类加载器简单实现

为了编写再现类加载器问题的小测试用例,我们需要为父优先委托模型实现一个类加载器实现,并为子优先模型实现另一个实现。 幸运的是,父优先委托模型的类加载器随时可用。java.net.URLClassLoader 不仅实现了父优先模型,使用起来也非常方便。

据我所知,没有类加载器来实现 JDK 附带的子优先模型。 但是,写我们自己并不需要太多代码。

/**
 * An almost trivial no fuss implementation of a class loader 
 * following the child-first delegation model.
 * 
 * @author Ceki Gülcü
 */
public class ChildFirstClassLoader extends URLClassLoader {

  public ChildFirstClassLoader(URL[] urls) {
    super(urls);
  }

  public ChildFirstClassLoader(URL[] urls, ClassLoader parent) {
    super(urls, parent);
  }

  public void addURL(URL url) {
    super.addURL(url);
  }
  
  public Class loadClass(String name) throws ClassNotFoundException {
  	return loadClass(name, false);
  }

  /**
   * We override the parent-first behavior established by 
   * java.lang.Classloader.
   * 
   * The implementation is surprisingly straightforward.
   */
  protected Class loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
  	
    // First, check if the class has already been loaded
    Class c = findLoadedClass(name);

    // if not loaded, search the local (child) resources
    if (c == null) {
    	try {
        c = findClass(name);
    	} catch(ClassNotFoundException cnfe) {
    	  // ignore
    	}
    }

    // if we could not find it, delegate to parent
    // Note that we don't attempt to catch any ClassNotFoundException
    if (c == null) {
      if (getParent() != null) {
        c = getParent().loadClass(name);
      } else {
        c = getSystemClassLoader().loadClass(name);
      }
    }

    if (resolve) {
      resolveClass(c);
    }

    return c;
  }
}

简而言之,ChildFirstClassLoader 扩展了 java.net.URLClassLoader 并覆盖了其 loadClass() 方法,以便只有在尝试在本地加载类失败后才会发生父委派。

现在我们已经有了类加载器实现,我们还需要一个或两个类来加载和使用。 我们将使用下面显示的各种版本的 Box 接口。

package box;

/*
 * Box is a small two method interface. We don't really care about 
 * what implementations of the Box interface do, as long as they
 * implement its two methods.
 * 
 * @author Ceki Gülcü
 */
public interface Box {

  /**
  * Returns the version number of the implementing class.
  */
  public int get();

  /**
   * Perform some operation. We don't really care what it is.
   */
  public void doOp();
  
}

这是 Box 的一个简单的实现 BoxImpl

package box;

/**
 * A no brainer implementation of Box returning a number. This
 * number is set at compilation time by Ant. 
 *  
 * @author Ceki Gülcü
 */
public class BoxImpl implements Box  {

  /**
   * Create an instance of BoxImpl and print it.
   */
  static public void main(String[] args) {
    Box box = new BoxImpl();
    System.out.println("BoxImpl version is "+box.get());
  }

  /**
   * The appropriate Ant task replaces @V@ with an integer constant.
   */
  public int get() {
    return @V@;
  }

  /**
   * Print this instance as a string.
   */
  public void doOp() {
  	System.out.println(this);
  }
  
  /**
   * Returns this instance's string representation.
   */
  public String toString() {
   return "BoxImpl("+get()+")"; 
  }
}

在随附的本文档发行版中,您将找到 3 个 jar 文件。 文件 boxAPI.jar 包含 Box.class,没有别的。 文件 box0.jar 包含一个 BoxImpl 类的版本,它在 get() 方法中返回零(0)。 文件 box1.jar 包含一个 BoxImpl 类的版本,它在 get() 方法中返回一(1)。

执行命令:

java -cp boxAPI.jar;box0.jar box.BoxImpl

执行结果:

Box version is 0

而执行命令:

java -cp boxAPI.jar;box1.jar box.BoxImpl

执行结果:

Box version is 1

这表明 jar 文件 box0.jarbox1.jar 包含标有不同版本号的实现。

子应用程序 ChildFirstTestParentFirstTest 包含在 ChildFirstClassLoaderURLClassLoader 分别实现子优先和父优先委托机制的分发中。

它们可以运行如下:

java -cp classes;boxAPI.jar;box0.jar ch.qos.test.ChildFirstTest

java -cp classes;boxAPI.jar;box0.jar ch.qos.test.ParentFirstTest

JCL 在父类优先加载器树中的应用

在父类优先类加载器树中,JCL 遇到 Type-I 的问题。要了解原因,必须仔细审查几个事实。

JCL 依靠动态发现来确定用户首选的日志 API。 理论上,支持 log4j,java.util.logging 和 Avalon logkit。 为了定位类和资源,JCL 的发现机制完全依赖于线程上下文类加载器,它是由当前线程的 getContextClassLoader() 方法返回的类加载器的定义。 只要没有显式定义线程上下文类加载器(TCCL),它就默认为系统类加载器,即用于加载应用程序的类加载器。

在以下三个示例中,系统类加载器将尝试访问 boxAPI.jarcommons-logging.jarclasses/ 目录。 URLClassLoader(父类优先委托)的实例将使用系统类加载器作为其父级。 它也将尝试访问 box1.jarcommons-logging.jarlog4j.jar

Example-1

第一个示例 ParentFirstTestJCL0 不会显式设置 TCCL。 因此,TCCL 将默认为系统类加载器。 请注意,此示例并未说明 JCL 中的错误,而是作为其余示例的热身。

public class ParentFirstTestJCL0 {
  
  public static void main(String[] args) throws Exception {
  
    URLClassLoader childClassLoader =
      new URLClassLoader(
        new URL[] {
          new URL("file:box1.jar"), 
          new URL("file:lib/commons-logging.jar"),
          new URL("file:lib/log4j.jar")
        });

    Log log = LogFactory.getLog("logger.name.not.important.here");
    log.info("a message");

    Class boxClass = childClassLoader.loadClass("box.BoxImplWithJCL");
    Box box = (Box) boxClass.newInstance();
    System.out.println(box);
    box.doOp();
  }
}

ParentFirstTestJCL0 应用程序创建 URLClassLoader 类型的子类加载器,并将 box1.jar,commons-logging.jar 和 log4j.jar 添加到加载器实例。 在接下来的两行中,JCL Log 实例由 LogFactory 检索并用来记录一个消息。 然后使用子类加载器创建 box.BoxImplWithJCL 的实例。 然后我们调用新创建的 BoxImplWithJCL 实例的 doOp 方法。 BoxImplWithJCL 中的 doOp() 方法调用 LogFactory.getLog 方法来检索 Log 实例,然后使用它来记录简单消息。

下图以图形方式更详细地说明了此配置。

cl-example-1.gif

执行命令:

java -cp boxAPI.jar;classes;lib/commons-logging.jar ch.qos.test.ParentFirstTestJCL0

执行结果:

Feb 6, 2005 3:44:45 PM ch.qos.test.ParentFirstTestJCL0 main
INFO: a message
Feb 6, 2005 3:44:45 PM box.BoxImplWithJCL doOp
INFO: hello  

等一下,这是 java.util.logging 的输出,而不是 log4j! 那是怎么发生的?

虽然我们可以使用子类加载器加载 box.BoxImplWithJCL(只有它可以访问 box1.jar),但是 JCL 忽略了子类加载器中的 log4j.jar,因为没有设置线程上下文类加载器。 JCL 随后发现了 java.util.logging,它提供了一个默认配置,将 INFO 级别以上的消息定向到控制台。

在下一个示例中,我们将显式设置线程上下文类加载器(TCCL),然后遇到一组全新的问题。

Example-2

ParentFirstTestJCL1 应用程序类似于 ParentFirstTestJCL0,除了它将子类加载器显式设置为 TCCL。 然后它加载一个新的 BoxImplWithJCL 实例并尝试在该实例上调用 doOp 方法。

package ch.qos;

public class ParentFirstTestJCL1 {

  public static void main(String[] args) throws Exception {
  
    URLClassLoader childClassLoader = new URLClassLoader(new URL[] {
        new URL("file:box1.jar"),
        new URL("file:lib/commons-logging.jar"),
        new URL("file:lib/log4j.jar")});
  		
    Thread.currentThread().setContextClassLoader(childClassLoader);
  		
    Class boxClass = childClassLoader.loadClass("box.BoxImplWithJCL");
    Box box = (Box) boxClass.newInstance();
    box.doOp();
  }
}

Example-2 的类加载器配置如下图所示。

cl-example-2.gif

执行下面的命令:

java -cp boxAPI.jar;classes;lib/commons-logging.jar ch.qos.test.ParentFirstTestJCL1

执行结果:

Exception in thread "main" org.apache.commons.logging.LogConfigurationException: \
org.apache.commons.logging.LogConfigurationException: 
  No suitable Log constructor [Ljava.lang.Class;@12b66 for org.apache.commons.logging.impl.Log4JLogger 
Caused by java.lang.NoClassDefFoundError: org/apache/log4j/Category) 
Caused by org .apache.commons.logging.LogConfigurationException: 
   No suitable Log constructor [Ljava.lang.Class;@12b6651 for org.apache.commons.logging.impl.Log4JLogger 
(Caused by java.lang.NoClassDefFoundError: org/apache/log4j/Category))
        at org.apache.commons.logging.impl.LogFactoryImpl.newInstance(LogFactoryImpl.java:543)
        at org.apache.commons.logging.impl.LogFactoryImpl.getInstance(LogFactoryImpl.java:235)
        at org.apache.commons.logging.LogFactory.getLog(LogFactory.java:370)
        at box.BoxImplWithJCL.doOp(BoxImplWithJCL.java:23)
        at ch.qos.test.ParentFirstTestJCL1.main(ParentFirstTestJCL1.java:44)
Caused by: org.apache.commons.logging.LogConfigurationException: 
  No suitable Log constructor [Ljava.lang.Class;@12b6651 for org.apache.commons.logging.impl.Log4J
Logger (Caused by java.lang.NoClassDefFoundError: org/apache/log4j/Category)
        at org.apache.commons.logging.impl.LogFactoryImpl.getLogConstructor(LogFactoryImpl.java:413)
        at org.apache.commons.logging.impl.LogFactoryImpl.newInstance(LogFactoryImpl.java:529)
        ... 4 more
Caused by: java.lang.NoClassDefFoundError: org/apache/log4j/Category
        at java.lang.Class.getDeclaredConstructors0(Native Method)
        at java.lang.Class.privateGetDeclaredConstructors(Class.java:1590)
        at java.lang.Class.getConstructor0(Class.java:1762)
        at java.lang.Class.getConstructor(Class.java:1002)
        at org.apache.commons.logging.impl.LogFactoryImpl.getLogConstructor(LogFactoryImpl.java:410)
        ... 5 more

出了什么问题?

当我们尝试从 box.BoxImplWithJCL 中的 doOp() 方法进行日志记录时,抛出 LogConfigurationExceptionBoxImplWithJCL 类由子类加载器加载和实例化。 子类加载器将 LogFactory 类的加载委托给其父类。 为了返回 Log 实例,LogFactory 在某个时间点调用其 getLogConstructor() 方法。 从此方法中抛出 LogConfigurationException

在 LogFactoryImpl.java 的第 368 行,getLogConstructor() 调用 getLogClassName()。 假定通过 TCCL(定义为子类加载器)可以看到类 org.apache.log4j.Loggerorg.apache.commons.logging.impl.Log4JLogger,则 getLogClassName() 方法返回字符串值 “org.apache.commons.logging.impl.Log4JLogger”。

在第 373 行到第 398 行,Log4jLogger 类由父类加载器加载到内存中。 TCCL,即在这个例子中,子类加载器委托给它的父系统类加载器,它能够找到 JCL 的 Log4jLogger 类。 将此 Log4jLogger 副本与 org.apache.commons.logging.Log 类的副本进行比较,该副本也由同一个类加载器加载。 比较成功,我们继续下一步。

在第 410 行,在 Log4jLogger 类上调用 Class.getConstructor() 方法 - 由父类加载器加载。 这反过来会触发 log4j 的 Logger 类的加载。 这是使用加载 Log4jLogger 类的类加载器完成的,即 父类加载器,它看不到 log4j.jar,因此未定义 org.apache.log4j.Category 的根 java.lang.NoClassDefFoundError 异常。 换句话说,JCL 使用 TCCL 加载日志记录 API 实现类,但是将获取实现类的构造函数方法的工作交给当前的类加载器。 这种差异是 JCL 的类加载器问题 Type-I 的核心。

Example-2B: 从 commons-logging.jar 中排除 Log4jLogger

随 JCL 发行版一起提供的文件 commons-logging-api.jar 与 commons-logging.jar 相同,只是前者不包含 org.apache.commons.logging.impl.Log4JLoggerorg.apache.commons.logging.impl.AvalonLogger 类。

有趣的是,在类路径上使用 commons-logging-api.jar 运行 ParentFirstTestJCL1 会产生更好的结果。 Example-2B 的类加载器配置如下图所示。

cl-example-2B.gif

在类路径上使用 commons-logging-api.jar 运行 ParentFirstTestJCL1,如下所示:

java -cp boxAPI.jar;classes;lib/commons-logging-api.jar ch.qos.test.ParentFirstTestJCL1

将加载 log4j 类并且没有错误。 实际上,Log4JLogger 类将由子类加载器而不是无法看到 Log4JLogger 的父类加载。 子类加载器也可以加载 log4j 类。

遗憾的是,即使 log4j 类对父类加载器可见,这种方法也明显不便于阻止使用由父类加载器加载的类的 log4j(或驻留在 JDK 之外的任何其他API)。

将 commons-logging-api.jar 放在父类加载器中是安全的,但是不能通过父类加载器直接加载的类来使用 log4。 在讨论 child-first 类加载器树时,我们将回到这一点。 如 Example-2 所示,当子类加载器可以看到 log4j 类时,将 commons-logging.jar 放在父类加载器中是不安全的,仍然被定义为 TCCL。

Example-3

ParentFirstTestJCL2 应用程序与 ParentFirstTestJCL1 非常相似。 它说明了一旦将线程上下文类加载器设置为子类加载器,通过 JCL 进行日志记录就会失败。

package ch.qos;

public static void main(String[] args) throws Exception {
  URLClassLoader childClassLoader =
    new URLClassLoader(
      new URL[] {
        new URL("file:box1.jar"), 
        new URL("file:lib/commons-logging.jar"),
        new URL("file:lib/log4j.jar")
      });

  Thread.currentThread().setContextClassLoader(childClassLoader);

  // this will throw a NoClassDefFoundError exception
  Log log = LogFactory.getLog("logger.name.not.important.here");

} 

执行

java -cp boxAPI.jar;classes;lib/commons-logging.jar ch.qos.test.ParentFirstTestJCL2

将会输出和上一个示例一样的错误。

JCL 在子类优先加载器树中的应用

在子类优先的加载器树中,JCL 同时遇到 Type-I 和 Type-II 的问题。 我们将以一个重现 Type-II 问题的例子开始本节。

Example-4

下一个示例 ChildFirstTestJCL0 演示了当父类和子类加载器都可以直接访问 commons-logging.jar 并且 TCCL 设置在子类加载器中时,在由父类加载器直接加载的类中第一次调用 JCL 的 LogFactory.getLog() 方法,将直接将抛出异常。

package ch.qos.test;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import ch.qos.ChildFirstClassLoader;

public class ChildFirstTestJCL0 {
  public static void main(String[] args) throws Exception {
    ChildFirstClassLoader child = new ChildFirstClassLoader( 
        new URL[] { new URL("file:lib/commons-logging.jar") });

    Thread.currentThread().setContextClassLoader(child);

    // JCL will throw an exception as soon as LogFactory is called after the 
    // TCCL is set to a child class loader, and both the parent and child
    // have direct access to commons-logging.jar
    Log log = LogFactory.getLog("logger.name.not.important.here");
  }
} 

Example-4 的类加载器配置如下图所示。

cl-example-4.gif

使用以下命令行运行应用程序 ChildFirstTestJCL0

java -cp classes;lib/commons-logging.jar ch.qos.test.ChildFirstTestJCL0

将导致以下异常

Exception in thread "main" org.apache.commons.logging.LogConfigurationException: \
  org.apache.commons.logging.LogConfigurationException: \
    org.apache.commons.logging.LogConfigurationException: Invalid class loader hierarchy.  \
  You have more than one version of 'org.apache.commons.logging.Log' visible, which is not allowed. 
(Caused by org.apache.commons.logging.LogConfigurationException: Invalid class loader hierarchy.  
 You have more than one version of 'org.apache.commons.logging.Log' visible, which is not allowed.) 
(Caused by org.apache.commons.logging.LogConfigurationException: \
org.apache.commons.logging.LogConfigurationException: Invalidclass loader hierarchy.  \
You have more than one version of 'org.apache.commons.logging.Log' visible, which is not allowed. 
(Caused by org.apache.commons.logging.LogConfigurationException: Invalid class loader hierarchy.  \
You have more than one version of 'org.apache.commons.logging.Log' visible, which is not allowed.))
        at org.apache.commons.logging.impl.LogFactoryImpl.newInstance(LogFactoryImpl.java:543)
        at org.apache.commons.logging.impl.LogFactoryImpl.getInstance(LogFactoryImpl.java:235)
        at org.apache.commons.logging.LogFactory.getLog(LogFactory.java:370)
        at ch.qos.test.ChildFirstTestJCL0.main(ChildFirstTestJCL0.java:43)
Caused by: org.apache.commons.logging.LogConfigurationException: \
org.apache.commons.logging.LogConfigurationException: Invalid class loader hierarchy.  \
You have more than one version of 'org.apache.commons.logging.Log' visible, which is not allowed. 
(Caused by org.apache.commons.logging.LogConfigurationException: Invalid class loader hierarchy. \
You have more than one version of 'org.apache.commons.logging.Log' visible, which is not allowed.)
        at org.apache.commons.logging.impl.LogFactoryImpl.getLogConstructor(LogFactoryImpl.java:397)
        at org.apache.commons.logging.impl.LogFactoryImpl.newInstance(LogFactoryImpl.java:529)
        ... 3 more
Caused by: org.apache.commons.logging.LogConfigurationException: Invalid class loader hierarchy.  \
You have more than one version of 'org.apache.commons.logging.Log' visible, which is not allowed.
        at org.apache.commons.logging.impl.LogFactoryImpl.getLogConstructor(LogFactoryImpl.java:385)
        ... 4 more

抛出异常是因为 TCCL 加载了 Log 实现,即子类加载器与 LogFactory 中使用的 Log 接口(由父类加载器加载)不同。 根据 Java 语言规范的 第 4.3.2 节,由不同类加载器加载的两个类被认为是不同的,因此是不兼容的。

Example-4B

在前面的示例中,LogFactoryLog 类由父加载器加载,而其实现由 TCCL 加载。 Example-4B 演示了如果 TCCL 遵循子类优先委托,那么在 TCCL 加载 LogFactoryLog 类的情况下,Log 实现将是兼容的,因为它将由相同的类加载器加载。

package ch.qos.test;

import box.Box;
import ch.qos.ChildFirstClassLoader;

import java.net.URL;

/**
 * Usage:
 *
 *   java -cp classes;lib/commons-logging.jar ch.qos.test.ChildFirstTestJCL1
 *
 * @author Ceki Gülcü
 */
public class ChildFirstTestJCL1 {
  public static void main(String[] args) throws Exception {
    ChildFirstClassLoader child = new ChildFirstClassLoader( 
        new URL[] { 
            new URL("file:lib/commons-logging.jar"),
            new URL("file:box1.jar")});

    Thread.currentThread().setContextClassLoader(child);

    Class boxClass = child.loadClass("box.BoxImplWithJCL");
    Box box = (Box) boxClass.newInstance();
    box.doOp();
   }
}

执行

java -cp boxAPI.jar;classes;lib/commons-logging.jar ch.qos.test.ChildFirstTestJCL1

将使用 java.util.logging 输出一个日志消息。

Example-5

前面第四个例子中,我们已经看到在父类和子类加载器中都有一个 commons-logging.jar 的副本,一旦父类加载的类尝试使用 JCL,就会引起问题。 在第五个名为 ChildFirstTestJCL2 的示例中,我们将从子类加载器中删除commons-logging.jar,并使用一个 log4j.jar 的副本代替。 它演示了当父类加载器可以看到 commons-logging.jar 并且子类加载器(也设置为 TCCL)可以看到 log4j.jar 时,那么就像在第二个和第三个例子中一样,JCL 将抛出 LogConfigurationException,因为它无法找到 log4j 类。

package ch.qos.test;

import java.net.URL;
import box.Box;

import ch.qos.ChildFirstClassLoader;

/**
 * Usage:
 *
 *   java -cp classes;boxAPI.jar;lib/commons-logging.jar ch.qos.test.ChildFirstTestJCL1
 * 
 * @author Ceki Gülcü
 */
public class ChildFirstTestJCL2 {

  public static void main(String[] args) throws Exception {
    
    ChildFirstClassLoader child = new ChildFirstClassLoader(new URL[] {
        new URL("file:box1.jar"), 
        new URL("file:lib/log4j.jar") });

    Thread.currentThread().setContextClassLoader(child);
    
    Class boxClass = child.loadClass("box.BoxImplWithJCL");
    Box box = (Box) boxClass.newInstance();
    box.doOp();
  }
}

Example-5 的类加载器配置如下图所示。

cl-example-5.gif

执行命令:

java -cp classes;boxAPI.jar;lib/commons-logging.jar ch.qos.test.ChildFirstTestJCL2

将导致抛出以下异常。

Exception in thread "main" org.apache.commons.logging.LogConfigurationException: \
org.apache.commons.logging.LogConfigurationException: \ 
No suitable Log constructor [Ljava.lang.Class;@4a5ab2 for org.apache.commons.logging.impl.Log4JLogger\
(Caused by java.lang.NoClassDefFoundError: org/apache/log4j/Category) 
(Caused by org.apache.commons.logging.LogConfigurationException: \
No suitable Log constructor [Ljava.lang.Class;@4a5ab2 for org.apache.commons.logging.impl.Log4JLogger 
(Caused by java.lang.NoClassDefFoundError: org/apache/log4j/Category))
        at org.apache.commons.logging.impl.LogFactoryImpl.newInstance(LogFactoryImpl.java:543)
        at org.apache.commons.logging.impl.LogFactoryImpl.getInstance(LogFactoryImpl.java:235)
        at org.apache.commons.logging.LogFactory.getLog(LogFactory.java:370)
        at box.BoxImplWithJCL.doOp(BoxImplWithJCL.java:23)
        at ch.qos.test.ChildFirstTestJCL2.main(ChildFirstTestJCL1.java:42)
Caused by: org.apache.commons.logging.LogConfigurationException: \ 
 No suitable Log constructor [Ljava.lang.Class;@4a5ab2 for org.apache.commons.logging.impl.Log4JLogger
(Caused by java.lang.NoClassDefFoundError: org/apache/log4j/Category)
        at org.apache.commons.logging.impl.LogFactoryImpl.getLogConstructor(LogFactoryImpl.java:413)
        at org.apache.commons.logging.impl.LogFactoryImpl.newInstance(LogFactoryImpl.java:529)
        ... 4 more
Caused by: java.lang.NoClassDefFoundError: org/apache/log4j/Category
        at java.lang.Class.getDeclaredConstructors0(Native Method)
        at java.lang.Class.privateGetDeclaredConstructors(Class.java:1590)
        at java.lang.Class.getConstructor0(Class.java:1762)
        at java.lang.Class.getConstructor(Class.java:1002)
        at org.apache.commons.logging.impl.LogFactoryImpl.getLogConstructor(LogFactoryImpl.java:410)
        ... 5 more

此异常的原因与第二个示例中讨论的非常相似。

Example-6

ChildFirstTestJCL3 应用程序与前面的示例非常相似。 它说明了一旦将线程上下文类加载器设置到子类加载器,使用 JCL 通过父类加载器加载的任何类进行日志记录将失败。

Example-7

假设您正在使用 Tomcat 并希望在 Web 应用程序中使用 log4j。 在 Tomcat 中,每个 Web 应用程序都有自己的类加载器,遵循子类优先委托模型。 假设您在 web 应用程序的文件中添加了 log4j.jar,并在 Tomcat 的 common/lib/ 目录中添加了 commons-logging.api。那么,在这种情况下,您将重演 Example-5 和 Example 中描述的问题,并且你会得到同样的异常。

对于对此模拟的真实性持怀疑态度的,本文档附带的 分发 包含一个小而完整的 Web 应用程序,称为 “Hello”,可以在 Tomcat 中重现该问题。

保持对类加载器的引用

为了跟踪各种日志记录实现及其包装器,JCL 内部管理了一个由类加载器作为键的哈希表。 这导致 Type-III 类型的问题。 实际上,JCL 阻止了应用程序容器垃圾回收器收集回收 Web 应用程序的垃圾。 将在 JCL 版本 1.0.5 中解决此限制,通过使用 WeakHashTable 替换常规哈希表。

与静态绑定相比,例如 SLF4J

直到最近,JCL 是提供各种日志 API 抽象的唯一解决方案。 事后看来,log4j 项目开发了一种替代解决方案,简称为 “Simple Logging Facade for Java” 或 SLF4J。

SLF4J 支持多种实现,即 NOP,Simple,log4j,JDK1.4 日志记录和 logback。 SLF4J 发行版附带了四个 jar 文件 slf4j-nop.jar,slf4j-simple.jar,slf4j-log4j12.jar 和 slf4j-jdk14.jar。 每个这些 jar 文件在编译时都是硬连线的,只使用一个实现,分别是 NOP,Simple,log4j12 和 JDK1.4 日志记录。 logback 发行版附带 logback-classic.jar,它本身实现了 SLF4J API。

除非 slf4j-xxx.jar 或 logback-classic.jar 文件损坏,否则静态链接永远不会导致 Type-I,Type-II 或 Type-III 的错误,原因很简单,所有的 API 绑定都是在编译时完成的,slf4j-xxx.jar 或 logback-classic.jar 所有的依赖文件都打包在了一起。

您可以借助 BoxImplWithSLF4J 以及分发中找到的相关测试用例来自行验证。 简而言之,SLF4J 永远不会出现意外行为或崩溃您的应用程序。

短期解决方案

鉴于 JCL 问题的严重性,我们建议您切换到 SLF4J,它提供更好的功能,没有任何错误。 但是,在短期内,如果您完全遵循以下方案,则可以减轻将 JCL 与 log4j 结合使用的一些痛苦。

在所有服务器中,确保您使用的是 JCL 1.0.4 或更高版本。早期版本的 JCL 带来的问题超出了本文档中讨论的问题。

对于Tomcat 5.0.27及更高版本,

  1. 确保您使用的是 JCL 1.0.4 或更高版本。 Tomcat 5.0.x 版附带了过时的 JCL 版本。
  2. 保留文件 TOMCAT_HOME/bin/commons-logging-api.jar,即不要删除它。
  3. 将文件 commons-logging.jar 和 log4j.jar 放在目录 TOMCAT_HOME/common/lib/ 中。
  4. 不要在 web 应用程序的 WEB-INF/lib/ 目录中包含 commons-logging.jar 和 log4j.jar 的任何其他副本。
  5. 不要设置系统属性 org.apache.commons.logging.LogFactoryorg.apache.commons.logging.Log

对于Resin 2.0.x,

  1. 确保您使用的是 JCL 1.0.4 或更高版本。
  2. 将文件 commons-logging.jar 和 log4j.jar 放在 RESIN_HOME/lib/ 目录中。
  3. 不要在 web 应用程序的 WEB-INF/lib/ 目录中包含 commons-logging.jar 和 log4j.jar 的任何其他副本。
  4. 不要设置系统属性 org.apache.commons.logging.LogFactoryorg.apache.commons.logging.Log

Jetty,

我很高兴地报告,Jetty 的未来版本预计会放弃使用 JCL,转而支持 SLF4J。 但是,对于旧版本的 Jetty,以下说明可以帮助您。

  1. 确保您使用的是 JCL 1.0.4 或更高版本。
  2. 将文件 commons-logging.jar 和 log4j.jar 放在 JETTY_HOME/ext/ 目录中。
  3. 不要在 web 应用程序的 WEB-INF/lib/ 目录中包含 commons-logging.jar 和 log4j.jar 的任何其他副本。
  4. 将系统属性 org.apache.commons.logging.LogFactory 设置为 org.apache.commons.logging.impl.LogFactoryImpl。 这可以通过在命令行上按如下方式启动 Jetty 来完成:java -Dorg.apache.commons.logging.LogFactory=org.apache.commons.logging.impl.LogFactoryImpl -jar start.jar
  5. 不要设置 org.apache.commons.logging.Log 系统属性。

总结

如上所演示的,JCL 的发现机制发明了用脚射击自己的新的和原始的方式。 例如,使用 JCL,你可以在瞄准天空的同时射击自己的脚。 感谢 JCL 在没有下雨的时候,你能在沙漠中间被闪电击中。 如果您的计算生活过于沉闷而且您正在寻找麻烦,那么 JCL 就是您的选择。

为了使 JCL 可靠地发现日志记录 API,此 API 必须位于类加载器树中的相同级别或更高级别。 因此,实际上,JCL 安全桥接的唯一日志 API 是 java.util.logging API。 与静态绑定的桥接机制相比,例如由 SLF4J 实现,JCL 的动态发现机制具有严格的零增值。 总之,静态绑定发现提供了更好的功能,没有与 JCL 动态发现相关的痛苦错误。

解决更广泛的现象

由于许多 Apache 组件都依赖于 JCL,因此 Apache 内外的许多开发人员都误以为使用 JCL 是安全且无害的。 这样的开发人员认为,由于他们使用过 JCL,因此不会遇到任何严重的错误,因为这些错误会被报告并且可能会被纠正。 正如埃里克·雷蒙德(Eric S. Raymond)所说的那样,“给予足够的眼球,所有的错误都很浅。”

毫无疑问,随着眼球数量的增加,捕获 BUG 的可能性也会增加。 但是,我们隐含地将眼球与软件用户等同起来。 不幸的是,任何给定软件的绝大多数用户都不会查看他们正在使用的软件,至少不是以严肃而有意义的方式。 实际上,捕获错误的改进仅与用户数量成对数。

许多人遭受错误影响的事实并不意味着他们会报告它,更不用说研究它了。

BUG 的性质在其鉴定中也起着至关重要的作用。 人们善于捕捉他们能看到的 BUG。 然而,人们在精神上没有能力识别依赖于被检查代码外部因素的类加载器错误。 即使是经验丰富且能干的开发人员也可以盯着类加载器 bug 几个小时,但仍然没有意识到它存在。 它需要 JVM 级别的精确性和很多耐心才能找到可能在可预见的未来仍然难以捉摸的类加载器错误。

如果觉得这对你有用,请随意赞赏,给与作者支持
评论 0
最新评论