2017-02-19 1 views
1

Я пытаюсь загрузить класс из JAR, представленного как массив byte [] во время выполнения.
Я знаю две вещи класса нагрузки:

1. он реализует «RequiredInterface»
2. Я знаю, что это квалифицированное имя: «sample.TestJarLoadingClass»

Я нашел solution, в котором я должен продлить ClassLoader но он выбрасывает:ClassNotFoundException при попытке загрузить класс из внешнего JAR во время выполнения

Exception in thread "main" java.lang.ClassNotFoundException: sample.TestJarLoadingClass 
    at java.lang.ClassLoader.findClass(ClassLoader.java:530) 
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424) 
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357) 
    at java.lang.Class.forName0(Native Method) 
    at java.lang.Class.forName(Class.java:348) 
    at tasks.Main.main(Main.java:12) 
    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 com.intellij.rt.execution.application.AppMain.main(AppMain.java:144) 

всякий раз, когда я хочу загрузить класс.

Что может быть причиной этой ситуации и как я могу избавиться от этого?
Любая помощь высоко оценили

Основной метод:

public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException { 
    Path path = Paths.get("src/main/java/tasks/sample.jar"); 
    RequiredInterface requiredInterface = (RequiredInterface) Class.forName("sample.TestJarLoadingClass", true, new ByteClassLoader(Files.readAllBytes(path))).newInstance(); 
} 

Пользовательский класс погрузчик:

import java.io.ByteArrayInputStream; 
    import java.io.IOException; 
    import java.io.InputStream; 
    import java.util.Collections; 
    import java.util.HashSet; 
    import java.util.Set; 
    import java.util.zip.ZipEntry; 
    import java.util.zip.ZipInputStream; 

    public class ByteClassLoader extends ClassLoader { 
     private final byte[] jarBytes; 
     private final Set<String> names; 

     public ByteClassLoader(byte[] jarBytes) throws IOException { 
      this.jarBytes = jarBytes; 
      this.names = loadNames(jarBytes); 
     } 

     private Set<String> loadNames(byte[] jarBytes) throws IOException { 
      Set<String> set = new HashSet<>(); 
      try (ZipInputStream jis = new ZipInputStream(new ByteArrayInputStream(jarBytes))) { 
       ZipEntry entry; 
       while ((entry = jis.getNextEntry()) != null) { 
        set.add(entry.getName()); 
       } 
      } 
      return Collections.unmodifiableSet(set); 
     } 

     @Override 
     public InputStream getResourceAsStream(String resourceName) { 
      if (!names.contains(resourceName)) { 
       return null; 
      } 
      boolean found = false; 
      ZipInputStream zipInputStream = null; 
      try { 
       zipInputStream = new ZipInputStream(new ByteArrayInputStream(jarBytes)); 
       ZipEntry entry; 
       while ((entry = zipInputStream.getNextEntry()) != null) { 
        if (entry.getName().equals(resourceName)) { 
         found = true; 
         return zipInputStream; 
        } 
       } 
      } catch (IOException e) {; 
       e.printStackTrace(); 
      } finally { 
       if (zipInputStream != null && !found) { 
        try { 
         zipInputStream.close(); 
        } catch (IOException e) {; 
         e.printStackTrace(); 
        } 
       } 
      } 
      return null; 
     } 
} 

RequiredInterface:

public interface RequiredInterface { 
    String method(); 
} 

класс в JAR-файле:

package sample; 
public class TestJarLoadingClass implements RequiredInterface { 
    @Override 
    public String method() { 
     return "works!"; 
    } 
} 
+0

Я использовал код, который вы разместили на моей машине, и все сработало. Не могли бы вы опубликовать точное исключение? Также проверьте, действительно ли путь, который вы используете, **, ** указывает на банку, которая содержит класс, который вы хотите загрузить? –

+0

Хорошо, я отправил все сообщение об исключении. Я проверил все возможные «глупые» ошибки, все должно работать. Убедитесь, что у вас нет класса, требуемого для загрузки в диапазоне вашего загрузчика классов. –

+1

Да, вы правы, по ошибке я добавил зависимость от jar с классом вместо jar с интерфейсом. Извините –

ответ

1

На мой взгляд, у нас есть две проблемы здесь:

Прежде всего вы должны переопределить findClass метод, который содержит фактическую логику загрузки класса. Основная задача здесь состоит в том, чтобы найти часть массива байтов, которая содержит ваш класс - поскольку у вас есть целая банка как массив байтов, вам нужно будет использовать JarInputStream для сканирования массива байтов для вашего класса.

Но это может быть не достаточно, потому что ваш RequiredInterface неизвестен для ByteClassLoader - так что вы будете иметь возможность читать сам класс, а определение класса содержит информацию, которую он реализует RequiredInterface, которая является проблемой для вашего загрузчика классов. Это легко исправить, вам просто нужно передать обычный загрузчик классов в качестве параметра конструктора в свой и использовать super(parentClassLoader).

Вот моя версия:

public class ByteClassLoader extends ClassLoader { 
    private final byte[] jarBytes; 

    public ByteClassLoader(ClassLoader parent, byte[] jarBytes) throws IOException { 
     super(parent); 
     this.jarBytes = jarBytes; 
    } 

    @Override 
    protected Class<?> findClass(String name) throws ClassNotFoundException { 

     // read byte array with JarInputStream 
     try (JarInputStream jis = new JarInputStream(new ByteArrayInputStream(jarBytes))) { 
      JarEntry nextJarEntry; 

      // finding JarEntry will "move" JarInputStream at the begining of entry, so no need to create new input stream 
      while ((nextJarEntry = jis.getNextJarEntry()) != null) { 

       if (entryNameEqualsClassName(name, nextJarEntry)) { 

        // we need to know length of class to know how many bytes we should read 
        int classSize = (int) nextJarEntry.getSize(); 

        // our buffer for class bytes 
        byte[] nextClass = new byte[classSize]; 

        // actual reading 
        jis.read(nextClass, 0, classSize); 

        // create class from bytes 
        return defineClass(name, nextClass, 0, classSize, null); 
       } 
      } 
      throw new ClassNotFoundException(String.format("Cannot find %s class", name)); 
     } catch (IOException e) { 
      throw new ClassNotFoundException("Cannot read from jar input stream", e); 
     } 
    } 


    private boolean entryNameEqualsClassName(String name, JarEntry nextJarEntry) { 

     // removing .class suffix 
     String entryName = nextJarEntry.getName().split("\\.")[0]; 

     // "convert" fully qualified name into path 
     String className = name.replace(".", "/"); 

     return entryName.equals(className); 
    } 
} 

А использование

RequiredInterface requiredInterface = (RequiredInterface)Class.forName("com.sample.TestJarLoadingClass", true, new ByteClassLoader(ByteClassLoader.class.getClassLoader(), Files.readAllBytes(path))).newInstance(); 
    System.out.println(requiredInterface.method()); 

Пожалуйста, обратите внимание, что моя реализация предполагает, что имя файла = имя класса, поэтому, например, классы, которые не являются верхнего уровня не будет быть найденным. И, конечно, некоторые детали могут быть более полированными (например, обработка исключений).

+0

Я пытаюсь запустить ваше решение, но каждый раз, когда я получаю за имя «TestJarLoadingClass» исключение: Исключение в потоке «main» java.lang.NoClassDefFoundError: TestJarLoadingClass (неправильное имя: sample/TestJarLoadingClass) –

+0

@adsqew это, вероятно, потому что я тестировал его на 'TestJarLoadingClass', который я поместил в пакет по умолчанию, поэтому мой пример не принял его во внимание. Я обновил пример, и теперь он учитывает разные пакеты (посмотрите на новый 'entryNameEqualsClassName'). Также попробуйте поставить точку останова в начале метода findClass и посмотреть, как экземпляры JarEntry будут выглядеть на каждой итерации. Вероятно, это даст вам больше знаний о том, как классы хранятся внутри файла Jar –

+0

Отлично, теперь он работает как ожидалось. Я хотел бы спросить вас в конце, если вы знаете, почему nextJarEntry.getSize() каждый раз дает результат -1 (значение по умолчанию)? Я должен передать размер .class вручную. –

Смежные вопросы