ObjectInputStream # readObject вызывает ClassNotFoundException с классами внешнего Jar - PullRequest
3 голосов
/ 05 апреля 2020

Итак, у меня есть приложение Spring Boot, которое загружает внешние файлы jar по указанным ниже путям:

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

Основной файл jar не знает внешних файлов jar во время компиляции. Внешние банки загружаются как «плагины» или «дополнения», указав

-Dloader.path=...

Все внешние банки зависят от интерфейса из main-0.0.1-SNAPSHOT.jar, и они должны делать сериализацию объектов более или менее. Интерфейс называется 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, и это то, что я пытаюсь выяснить.

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)

После некоторого тестирования мне кажется, что ClassNotFoundException выбрасывается Class.forName ( ) потому что по умолчанию ClassLoader трудно найти moe.ofs.addon.navdata.domain.Navaid, это класс, который я пытаюсь десериализовать.

Navaid implements Serializable, и он также имеет static final long serialVersionUID.

Я надеялся, что смогу решить эту проблему, установив загрузчик класса контекста для текущего потока, так что ObjectInputStream будет использовать загрузчик класса Spring Boot для разрешения Navaid class:

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

Это при печати out, дает что-то вроде

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

За исключением того, что ObjectInputStream#readObject все еще выбрасывает ClassNotFoundException. Если я явно сделаю вызов для загрузки класса Navaid из загрузчика Spring Boot, например:

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

Он возвращает Navaid экземпляр без каких-либо проблем.

И, как и ожидалось, при непосредственном вызове

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

a ClassNotFoundException выбрасывается, даже если загрузчик контекста потока был явно установлен на LaunchedURLClassLoader; ObjectInputStream#readObject всегда пытается разрешить класс, вызывая системный загрузчик классов по умолчанию для загрузки класса.

Затем я пытался загрузить ObjectInputStream, используя LaunchedURLClassLoader, но экземпляр все еще использовал 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();  // throws ClassNotFoundException

Любой ввод приветствуется. Заранее спасибо.

1 Ответ

1 голос
/ 05 апреля 2020

Насколько я знаю, вы должны переопределить метод resolveClass на ObjectInputStream

Примерно так:

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();
}

Никогда не пробовал сам, но это стоит того shot.

Существует также http://commons.apache.org/proper/commons-io/javadocs/api-2.4/org/apache/commons/io/input/ClassLoaderObjectInputStream.html, если у вас уже есть commons-io в вашем проекте.

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

...