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.
As far as i know, you should override the method resolveClass
on ObjectInputStream
Something like that:
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();
}
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.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.