2017-02-19 13 views
1

런타임시 byte [] 배열로 표시된 JAR에서 클래스를로드하려고합니다.
나는 부하에 클래스에 대한 두 가지를 알고
는 "RequiredInterface"나는 자격이 이름을 알고
2. 구현
1. "sample.TestJarLoadingClass"

내가 ClassLoader를 확장해야하는 solution 발견을 하지만 던지기 :런타임시 외부 JAR 클래스를로드하는 중 ClassNotFoundException 발생

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 파일의 클래스 :

0,123,516
는 어떤 도움을 매우

홈페이지 방법을 평가 당신로드 클래스의 실제 로직을 포함 findClass 메소드를 오버라이드 (override) 모든

첫째 : 제 생각에는

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

내 컴퓨터에 게시 한 코드를 사용했는데 모두 작동했습니다. 당신은 정확한 예외를 게시 할 수 있겠습니까? 또한 사용중인 경로가 ** 실제로 **로드하려는 클래스가 들어있는 jar를 가리키는 지 확인하십시오. –

+0

그래, 전체 예외 메시지를 게시했습니다. 나는 가능한 모든 "어리석은"실수를 모두 확인했다. 클래스 로딩을 요구하는 클래스가 클래스 로더 범위 내에 있지 않은지 확인하십시오. –

+1

네, 맞습니다. 실수로 인터페이스가있는 jar 대신 클래스를 사용하여 jar에 종속성을 추가했습니다. 죄송합니다 –

답변

1

우리는 여기에 두 가지 문제가있다. 가장 큰 문제는 클래스를 포함하는 바이트 배열의 일부를 찾는 것입니다. 전체 배열을 바이트 배열로 사용하기 때문에 JarInputStream을 사용하여 클래스의 바이트 배열을 스캔해야합니다.

하지만 당신의 RequiredInterfaceByteClassLoader에 대해 알 수 없기 때문에이 충분하지 않을 수 있습니다 - 그래서 당신은 클래스 자체를 읽을 수 있지만, 그것은 당신의 클래스 로더에 대한 문제가 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"라는 예외를받을 때마다 예외가 발생합니다 : java.lang.NoClassDefFoundError : TestJarLoadingClass (잘못된 이름 : sample/TestJarLoadingClass) –

+0

@adsqew 왜냐하면 필자는 기본 패키지에 넣어 둔'TestJarLoadingClass'에서 테스트했기 때문에 예제에서 고려하지 않았습니다. 나는 예제를 업데이트했고 이제는 다른 패키지를 고려한다 (새로운'entryNameEqualsClassName'을 살펴 보라). 또한 findClass 메소드의 시작 부분에 중단 점을 넣고 각 반복에서 JarEntry 인스턴스가 어떻게 보이는지 살펴보십시오. 아마도 Jar 파일에 클래스가 내부적으로 저장되는 방법에 대해 더 많은 지식을 제공 할 것입니다. –

+0

좋아, 이제 예상대로 작동합니다. nextJarEntry.getSize()가 매번 결과 -1 (기본값)을주는 이유를 아는 경우 끝에 묻고 싶습니다. .class 크기를 수동으로 전달해야합니다. –