使用 packageName 和 Classloader 实例化 JAXBContext 时混淆 ClassNotFoundException

Confusing ClassNotFoundException when instantiating JAXBContext with packageName and Classloader

我正在尝试使用 java 中的 JAXB 将 XML 文件解组为生成的 class 结构。我 运行 遇到了一个令人困惑的问题,我交给 JAXBContext.newInstance(packageName, classLoader) 的 classloader 显然找不到一些必要的 classes 来实例化模式 classes,但是当我手动搜索提供的 classloader 以查找所需的 classes 时,它们就在那里:

URLClassLoader cl = this.getJaxbClassloader();
try 
{
    cl.loadClass("org.postgresql.util.PGInterval");
    Log.error("Found class [" + name + "] in provided classloader");
} 
catch (ClassNotFoundException e) 
{
    Log.error("Unable to find class [" + name + "]  in provided classloader");
}

JAXBContext ctx = JAXBContext.newInstance( "com.comp.gen", cl);

getJaxbClassloader() 方法只是创建一个新的 URLClassLoader,加载生成的 classes 所需的一些特定 jar,然后将系统 classloader 设置为父类。生成的 classes 使用一些我放入 classloader 的 postgresql 库,这是我遇到问题的资源。 JAXB 在提供的包中正确地找到了 ObjectFactory class,问题似乎只是生成的 classes 本身的实例化。

运行 这段代码的结果是 cl.loadClass("org.postgresql.util.PGInterval"); 的手动调用工作正常,它记录了第一条声明它找到 class 的语句,没有抛出异常。但是当 JAXBContext 被实例化时,它会在完全相同的资源上抛出一个 CNFE:

java.lang.ClassNotFoundException: org.postgresql.util.PGInterval
   at java.net.URLClassLoader.findClass(URLClassLoader.java:600)
   at java.lang.ClassLoader.loadClassHelper(ClassLoader.java:772)
   at java.lang.ClassLoader.loadClass(ClassLoader.java:745)
   at java.lang.ClassLoader.loadClass(ClassLoader.java:726)
   ... 78 more

更彻底的堆栈跟踪:

java.lang.NoClassDefFoundError: org.postgresql.util.PGInterval
    at java.lang.Class.getDeclaredFieldsImpl(Native Method)
    at java.lang.Class.getDeclaredFields(Class.java:740)
    at com.sun.xml.bind.v2.model.nav.ReflectionNavigator.getDeclaredFields(ReflectionNavigator.java:249)
    at com.sun.xml.bind.v2.model.nav.ReflectionNavigator.getDeclaredFields(ReflectionNavigator.java:58)
    at com.sun.xml.bind.v2.model.impl.ClassInfoImpl.findFieldProperties(ClassInfoImpl.java:370)
    at com.sun.xml.bind.v2.model.impl.RuntimeClassInfoImpl.getProperties(RuntimeClassInfoImpl.java:176)
    at com.sun.xml.bind.v2.model.impl.ModelBuilder.getClassInfo(ModelBuilder.java:243)
    at com.sun.xml.bind.v2.model.impl.RuntimeModelBuilder.getClassInfo(RuntimeModelBuilder.java:100)
    at com.sun.xml.bind.v2.model.impl.RuntimeModelBuilder.getClassInfo(RuntimeModelBuilder.java:81)
    at com.sun.xml.bind.v2.model.impl.ModelBuilder.getClassInfo(ModelBuilder.java:209)
    at com.sun.xml.bind.v2.model.impl.RuntimeModelBuilder.getClassInfo(RuntimeModelBuilder.java:95)
    at com.sun.xml.bind.v2.model.impl.RuntimeModelBuilder.getClassInfo(RuntimeModelBuilder.java:81)
    at com.sun.xml.bind.v2.model.impl.ModelBuilder.getTypeInfo(ModelBuilder.java:315)
    at com.sun.xml.bind.v2.model.impl.RegistryInfoImpl.<init>(RegistryInfoImpl.java:99)
    at com.sun.xml.bind.v2.model.impl.ModelBuilder.addRegistry(ModelBuilder.java:357)
    at com.sun.xml.bind.v2.model.impl.ModelBuilder.getTypeInfo(ModelBuilder.java:327)
    at com.sun.xml.bind.v2.runtime.JAXBContextImpl.getTypeInfoSet(JAXBContextImpl.java:466)
    at com.sun.xml.bind.v2.runtime.JAXBContextImpl.<init>(JAXBContextImpl.java:302)
    at com.sun.xml.bind.v2.runtime.JAXBContextImpl$JAXBContextBuilder.build(JAXBContextImpl.java:1136)
    at com.sun.xml.bind.v2.ContextFactory.createContext(ContextFactory.java:154)
    at com.sun.xml.bind.v2.ContextFactory.createContext(ContextFactory.java:121)
    at com.sun.xml.bind.v2.ContextFactory.createContext(ContextFactory.java:202)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:95)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:56)
    at java.lang.reflect.Method.invoke(Method.java:620)
    at javax.xml.bind.ContextFinder.newInstance(ContextFinder.java:184)
    at javax.xml.bind.ContextFinder.newInstance(ContextFinder.java:144)
    at javax.xml.bind.ContextFinder.find(ContextFinder.java:346)
    at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:443)
    at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:406)

有人知道这里出了什么问题吗?我的印象是(并且 JAXBContext 文档支持这一点)它会使用提供的 classloader 来查找实例化 classes 所需的实现 classes,因此鉴于资源似乎在我提供的 classloader 中,为什么 JAXB 找不到它?

编辑: 添加使用 PGInterval 资源的生成 class 的相关部分:

import org.postgresql.util.PGInterval;
...
... 
...
@XmlElement(name = "time_to_live", required=false)
protected PGInterval time_to_live;

public PGInterval gettime_to_live()
{
    return time_to_live;
}

public void settime_to_live(PGInterval time_to_live)
{
    this.time_to_live = time_to_live;
}

我想值得注意的是,这是生成的 class 中唯一的导入,它不在 java 的标准库中。

根据 this FAQ,当在应用程序容器和服务器中使用 JAXB 时,可能会发生 class 加载问题。建议的解决方案是在创建 JAXBContext:

时使用当前的 class 加载器
JAXBContext.newInstance( "com.comp.gen", this.getClass().getClassLoader() );

编辑: 通过查看堆栈跟踪中的相关代码,指定的 classloader 用于:

  1. 加载位于指定包com.comp.gen中的ObjectFactoryclass。
  2. 加载 jaxb.index 文件中指定的 classes,该文件也位于包中。

比照。 com.sun.xml.internal.bind.v2.ContextFactory#createContext(String contextPath, ClassLoader classLoader, Map<String,Object> properties):

// look for ObjectFactory and load it
final Class<?> o;
try {
    o = classLoader.loadClass(pkg+".ObjectFactory");
    classes.add(o);
    ...

// look for jaxb.index and load the list of classes
try {
    indexedClasses = loadIndexedClasses(pkg, classLoader);
} catch (IOException e) {
    ...

从那时起,JAXB 似乎使用某种反射从这些已加载的 classes 中加载所有静态可达的 classes。这也是Javadocs of JAXBContext#newInstance(String contextPath, ClassLoader classLoader)中提到的:

Every package listed on the contextPath must meet one or both of the following conditions otherwise a JAXBException will be thrown:

  1. it must contain ObjectFactory.class
  2. it must contain jaxb.index

Format for jaxb.index

The file contains a newline-separated list of class names. Space and tab characters, as well as blank lines, are ignored. The comment character is '#' (0x23); on each line all characters following the first comment character are ignored. The file must be encoded in UTF-8. Classes that are reachable, as defined in newInstance(Class...), from the listed classes are also registered with JAXBContext.

我假设(但这是我不确定的部分...)所有可访问的 classes 也将使用您提供的 classloader 加载。但很明显,在参考路径的某个地方,org.postgresql.util.PGInterval 而不是 被那个 classloader 加载的。如果引用 org.postgresql.util.PGInterval 的 class 本身不是由您的自定义 classloader 加载,而是由父(系统)classloader 加载,则可能是这种情况。这意味着您可能希望确保您的自定义 classloader 能够加载从顶级 class 到 org.postgresql.util.PGInterval class 的所有 classes .

所以我最终弄明白了。在我工作的代码库深处,一位先前的开发人员创建了一个自定义动态 classloader,它使用 ClassLoader.getSystemClassLoader() 作为其父级。这个自定义 classloader 实际上用于从磁盘加载生成的 JAXB class,我使用该实例的 classloader 来加载 JAXB 内容。一旦我看到,我立即知道问题所在。

我 运行 所在的环境是 tomcat 之上的 restapi,因此我的组件级别的系统-classloader 仅包含 bootstrap tomcat.

之前的开发人员只在独立的 JVM 中 运行 他的代码,他在其中提供了一个巨大的 class 路径。因此,尽管这在技术上是一个环境问题,但根本原因是使用 ClassLoader.getSystemClassLoader() 作为新 ClassLoader 的父类。将父级更改为更符合逻辑的东西,即包含 class' classloader 解决了问题。