ObjectInputStream#readObject在使用外部Jar类时引发ClassNotFoundException。

huangapple go评论78阅读模式
英文:

ObjectInputStream#readObject raises ClassNotFoundException with External Jar classes

问题

我有一个Spring Boot应用程序,从以下路径加载外部jar包:

主要的jar包在编译时不知道外部jar包。外部jar包通过指定 -Dloader.path=... 类似于“插件”或“附加组件”的方式进行加载。

所有外部jar包都依赖于“main-0.0.1-SNAPSHOT.jar”中的一个名为Configurable的接口,它们大致上用于对象序列化。
这个接口名为 Configurable,并且提供了两个默认方法,如下所示:

default <T extends Serializable> void writeFile(T object, String fileName) throws IOException {
    Path configFilePath = configPath.resolve(fileName + ".data");
    FileOutputStream fileOutputStream = new FileOutputStream(configFilePath.toFile());
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
    objectOutputStream.writeObject(object);
    objectOutputStream.close();
}

default <T extends Serializable> T readFile(String fileName) throws IOException, ClassNotFoundException {
    Path configFilePath = configPath.resolve(fileName + ".data");
    FileInputStream fileInputStream = new FileInputStream(configFilePath.toFile());
    ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
    return (T) objectInputStream.readObject();
}

外部jar包中的类实现了这个接口,并调用了readFile()writeFile()方法。

writeFile()方法运行正常,似乎没有引起任何问题;然而,readFile()方法抛出了一个ClassNotFoundException异常,这就是我正在尝试解决的问题。

经过一些测试,我认为ClassNotFoundException是由于Class.forName()抛出的,因为默认的类加载器在查找moe.ofs.addon.navdata.domain.Navaid类时遇到了困难,而这正是我正在尝试反序列化的类。

Navaid实现了Serializable接口,它还有一个static final long serialVersionUID

我曾希望通过为当前线程设置上下文类加载器来解决这个问题,这样ObjectInputStream将使用Spring Boot类加载器来解析Navaid类:

Thread.currentThread().setContextClassLoader(getClass().getClassLoader());

然而,当我打印出来时,结果是类似这样的:

Thread.currentThread().getContextClassLoader() = org.springframework.boot.loader.LaunchedURLClassLoader@7a0b4753

但是ObjectInputStream#readObject仍然会抛出ClassNotFoundException异常。
如果我明确地调用Spring Boot加载器来加载Navaid类,比如:

getClass().getClassLoader().loadClass("moe.ofs.addon.navdata.domain.Navaid");

它会毫无问题地返回一个Navaid实例。

并且正如预期的那样,当直接调用:

Class.forName("moe.ofs.addon.navdata.domain.Navaid")

始终会抛出ClassNotFoundException异常,即使线程上下文加载器已经被明确设置为LaunchedURLClassLoaderObjectInputStream#readObject始终尝试通过调用系统默认类加载器来加载类。

然后我尝试使用LaunchedURLClassLoader加载ObjectInputStream,但该实例仍然使用系统默认类加载器中的Class.forName()方法。

ClassLoader cl = getClass().getClassLoader();

Thread.currentThread().setContextClassLoader(cl);

System.out.println("Thread.currentThread().getContextClassLoader() = " + Thread.currentThread().getContextClassLoader());

Class<?> tClass = getClass().getClassLoader().loadClass("java.io.ObjectInputStream");
System.out.println("tClass = " + tClass);

Path configFilePath = configPath.resolve(fileName + ".data");
FileInputStream fileInputStream = new FileInputStream(configFilePath.toFile());

Constructor<?> constructor = tClass.getConstructor(InputStream.class);

ObjectInputStream objectInputStream = (ObjectInputStream) constructor.newInstance(fileInputStream);

objectInputStream.readObject();  // 抛出ClassNotFoundException异常

非常感谢您的任何意见。提前致谢。

英文:

So I have a Spring Boot application that loads external jars from the paths below:

java -cp "main-0.0.1-SNAPSHOT.jar" -Dloader.path="%USERPROFILE%\Addons\" -Dloader.main=moe.ofs.backend.BackendApplication org.springframework.boot.loader.PropertiesLauncher

The main jar doesn't know external jars at compile time. External jars are loaded like "plugins" or "addons" by specifying

-Dloader.path=...

All of external jars depend on an interface from "main-0.0.1-SNAPSHOT.jar", and they are supposed to do object serializations more or less.
The interface is called Configurable, and it provides two default methods like these:

default <T extends Serializable> void writeFile(T object, String fileName) throws IOException {
    Path configFilePath = configPath.resolve(fileName + ".data");
    FileOutputStream fileOutputStream = new FileOutputStream(configFilePath.toFile());
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
    objectOutputStream.writeObject(object);
    objectOutputStream.close();
}

default <T extends Serializable> T readFile(String fileName) throws IOException, ClassNotFoundException {
    Path configFilePath = configPath.resolve(fileName + ".data");
    FileInputStream fileInputStream = new FileInputStream(configFilePath.toFile());
    ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
    return (T) objectInputStream.readObject();
}

Classes in external jars implement this interface, and they call readFile() and writeFile().

writeFile() works perfectly fine and doesn't seem to cause any problem; readFile(), however, throws a ClassNotFoundException, and that's what I'm trying to figure out.

java.lang.ClassNotFoundException: moe.ofs.addon.navdata.domain.Navaid
	at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:348)
	at java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:719)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1922)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1805)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2096)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1624)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:464)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
	at java.util.ArrayList.readObject(ArrayList.java:797)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1170)
	at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2232)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2123)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1624)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:464)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
	at moe.ofs.backend.Configurable.lambda$readFile$0(Configurable.java:186)
	at java.lang.Thread.run(Thread.java:748)

After some testing it seems to me that ClassNotFoundException is thrown by Class.forName() because the default ClassLoader has a hard time looking for moe.ofs.addon.navdata.domain.Navaid, which is the class I'm trying to deserialize.

Navaid implements Serializable, and it also has a static final long serialVersionUID.

I had hoped that I could solve this by setting a context class loader for current thread, so that ObjectInputStream will use Spring Boot class loader to resolve Navaid class:

Thread.currentThread().setContextClassLoader(getClass().getClassLoader());

This, when printed out, gives something like

Thread.currentThread().getContextClassLoader() = org.springframework.boot.loader.LaunchedURLClassLoader@7a0b4753

Except that ObjectInputStream#readObject still throws ClassNotFoundException.
If I explicitly make a call to load Navaid class from Spring Boot loader such as:

getClass().getClassLoader().loadClass("moe.ofs.addon.navdata.domain.Navaid");

It returns a Navaid instance without any issue.

And as expected, when directly calling

Class.forName("moe.ofs.addon.navdata.domain.Navaid")

a ClassNotFoundException is thrown, even if the thread context loader has been explicitly set to LaunchedURLClassLoader; ObjectInputStream#readObject always tries to resolve the class by making a call to system default classloader to load the class.

Then I tried to load an ObjectInputStream using LaunchedURLClassLoader, but the instance still used Class.forName() from system default class loader.

ClassLoader cl = getClass().getClassLoader();

Thread.currentThread().setContextClassLoader(cl);

System.out.println("Thread.currentThread().getContextClassLoader() = " + Thread.currentThread().getContextClassLoader());

Class<?> tClass = getClass().getClassLoader().loadClass("java.io.ObjectInputStream");
System.out.println("tClass = " + tClass);

Path configFilePath = configPath.resolve(fileName + ".data");
FileInputStream fileInputStream = new FileInputStream(configFilePath.toFile());

Constructor<?> constructor = tClass.getConstructor(InputStream.class);

ObjectInputStream objectInputStream = (ObjectInputStream) constructor.newInstance(fileInputStream);

objectInputStream.readObject();  // throws ClassNotFoundException

Any input is appreciated. Thanks in advance.

答案1

得分: 1

据我所知,您应该在ObjectInputStream上覆盖方法resolveClass

类似这样:

default <T extends Serializable> T readFile(String fileName, ClassLoader loader) throws IOException, ClassNotFoundException {
    Path configFilePath = configPath.resolve(fileName + ".data");
    FileInputStream fileInputStream = new FileInputStream(configFilePath.toFile());
    ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream){
       protected Class<?> resolveClass(ObjectStreamClass desc)
                     throws IOException, ClassNotFoundException {
          try {
              return Class.forName(desc.getName(), false, loader);
          } catch(ClassNotFoundException cnfe) {
              return super.resolveClass(desc);
          }
       }
   };
   return (T) objectInputStream.readObject();
}

我自己从未尝试过,但值得一试。

如果您的项目中已经包含了 commons-io,还有另外一个选项:http://commons.apache.org/proper/commons-io/javadocs/api-2.4/org/apache/commons/io/input/ClassLoaderObjectInputStream.html。

另外,您可以在这里找到源代码:https://github.com/apache/commons-io/blob/master/src/main/java/org/apache/commons/io/input/ClassLoaderObjectInputStream.java。

英文:

As far as i know, you should override the method resolveClass on ObjectInputStream

Something like that:

default &lt;T extends Serializable&gt; T readFile(String fileName, ClassLoader loader) throws IOException, ClassNotFoundException {
    Path configFilePath = configPath.resolve(fileName + &quot;.data&quot;);
    FileInputStream fileInputStream = new FileInputStream(configFilePath.toFile());
    ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream){
       protected Class&lt;?&gt; resolveClass(ObjectStreamClass desc)
                     throws IOException, ClassNotFoundException {
          try {
              return Class.forName(desc.getName(), false, loader);
          } catch(ClassNotFoundException cnfe) {
              return super.resolveClass(desc);
          }
       }
   };
   return (T) objectInputStream.readObject();
}

Never tried it myself, but it is worth a shot.

There is also http://commons.apache.org/proper/commons-io/javadocs/api-2.4/org/apache/commons/io/input/ClassLoaderObjectInputStream.html if you aready have commons-io in your project.

https://github.com/apache/commons-io/blob/master/src/main/java/org/apache/commons/io/input/ClassLoaderObjectInputStream.java

huangapple
  • 本文由 发表于 2020年4月6日 02:42:47
  • 转载请务必保留本文链接:https://go.coder-hub.com/61047670.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定