웹사이트 검색

자바 클래스로더


Java ClassLoader는 프로젝트 개발에서 중요하지만 거의 사용되지 않는 구성 요소 중 하나입니다. 내 프로젝트에서 ClassLoader를 확장한 적이 없습니다. 그러나 Java 클래스 로딩을 사용자 정의할 수 있는 나만의 ClassLoader가 있다는 생각은 흥미진진합니다. 이 기사에서는 Java ClassLoader에 대한 개요를 제공하고 Java에서 사용자 정의 클래스 로더를 작성하는 단계로 넘어갑니다.

자바 클래스로더란?

우리는 Java 프로그램이 JVM(Java Virtual Machine)에서 실행된다는 것을 알고 있습니다. Java 클래스를 컴파일할 때 JVM은 플랫폼 및 기계 독립적인 바이트코드를 생성합니다. 바이트코드는 .class 파일에 저장됩니다. 클래스를 사용하려고 하면 ClassLoader가 클래스를 메모리에 로드합니다.

내장 ClassLoader 유형

Java에는 세 가지 유형의 내장 ClassLoader가 있습니다.

  1. 부트스트랩 클래스 로더 - JDK 내부 클래스를 로드합니다. rt.jar 및 기타 핵심 클래스(예: java.lang.* 패키지 클래스)를 로드합니다.
  2. 확장 클래스 로더 - JDK 확장 디렉토리(일반적으로 $JAVA_HOME/lib/ext 디렉토리)에서 클래스를 로드합니다.
  3. 시스템 클래스 로더 – 이 클래스 로더는 현재 클래스 경로에서 클래스를 로드합니다. -cp 또는 -classpath 명령줄 옵션을 사용하여 프로그램을 호출하는 동안 클래스 경로를 설정할 수 있습니다.

클래스로더 계층

ClassLoader는 클래스를 메모리에 로드할 때 계층적입니다. 클래스 로드 요청이 발생할 때마다 상위 클래스 로더에 위임합니다. 이것이 런타임 환경에서 고유성이 유지되는 방식입니다. 상위 클래스 로더가 클래스를 찾지 못하면 클래스 로더 자체가 클래스 로드를 시도합니다. 아래의 자바 프로그램을 실행하여 이를 이해해 보자.

package com.journaldev.classloader;

public class ClassLoaderTest {

    public static void main(String[] args) {

        System.out.println("class loader for HashMap: "
                + java.util.HashMap.class.getClassLoader());
        System.out.println("class loader for DNSNameService: "
                + sun.net.spi.nameservice.dns.DNSNameService.class
                        .getClassLoader());
        System.out.println("class loader for this class: "
                + ClassLoaderTest.class.getClassLoader());

        System.out.println(com.mysql.jdbc.Blob.class.getClassLoader());

    }

}

산출:

class loader for HashMap: null
class loader for DNSNameService: sun.misc.Launcher$ExtClassLoader@7c354093
class loader for this class: sun.misc.Launcher$AppClassLoader@64cbbe37
sun.misc.Launcher$AppClassLoader@64cbbe37

Java ClassLoader는 어떻게 작동합니까?

위의 프로그램 출력에서 클래스 로더의 작동을 이해해 봅시다.

  • java.util.HashMap ClassLoader는 Bootstrap ClassLoader를 반영하는 null로 제공됩니다. DNSNameService 클래스 ClassLoader는 ExtClassLoader입니다. 클래스 자체가 CLASSPATH에 있으므로 System ClassLoader가 이를 로드합니다.
  • HashMap을 로드하려고 할 때 시스템 클래스 로더는 이를 확장 클래스 로더에 위임합니다. 확장 클래스 로더는 이를 Bootstrap ClassLoader에 위임합니다. 부트스트랩 클래스 로더는 HashMap 클래스를 찾아 JVM 메모리에 로드합니다.
  • DNSNameService 클래스에 대해 동일한 프로세스를 따릅니다. 그러나 Bootstrap ClassLoader는 $JAVA_HOME/lib/ext/dnsns.jar에 있기 때문에 찾을 수 없습니다. 따라서 Extensions Classloader에 의해 로드됩니다.
  • Blob 클래스는 프로젝트의 빌드 경로에 있는 MySql JDBC 커넥터 jar(mysql-connector-java-5.0.7-bin.jar)에 포함되어 있습니다. 또한 System Classloader에 의해 로드됩니다.
  • 하위 클래스 로더가 로드한 클래스는 상위 클래스 로더가 로드한 클래스를 볼 수 있습니다. 따라서 System Classloader에 의해 로드된 클래스는 Extensions 및 Bootstrap Classloader에 의해 로드된 클래스를 볼 수 있습니다.
  • 형제 클래스 로더가 있으면 서로 로드된 클래스에 액세스할 수 없습니다.

Java로 Custom ClassLoader를 작성하는 이유는 무엇입니까?

Java 기본 ClassLoader는 로컬 파일 시스템에서 클래스를 로드할 수 있으며 이는 대부분의 경우에 충분합니다. 그러나 클래스를 로드할 때 런타임이나 FTP 서버 또는 타사 웹 서비스를 통해 클래스를 예상하는 경우 기존 클래스 로더를 확장해야 합니다. 예를 들어 AppletViewer는 원격 웹 서버에서 클래스를 로드합니다.

자바 클래스로더 메소드

  • JVM이 클래스를 요청할 때 클래스의 완전히 분류된 이름을 전달하여 ClassLoader의 loadClass() 함수를 호출합니다.
  • loadClass() 함수는 findLoadedClass() 메서드를 호출하여 클래스가 이미 로드되었는지 여부를 확인합니다. 동일한 클래스를 여러 번 로드하지 않으려면 필요합니다.
  • 클래스가 아직 로드되지 않은 경우 클래스를 로드하도록 요청을 상위 ClassLoader에 위임합니다.
  • 부모 ClassLoader가 클래스를 찾지 못하면 findClass() 메서드를 호출하여 파일 시스템에서 클래스를 찾습니다.

Java 사용자 정의 ClassLoader 예제

1. CCLoader.java

이것은 아래 메소드가 있는 커스텀 클래스 로더입니다.

  1. private byte[] loadClassFileData(String name): 이 메서드는 파일 시스템에서 바이트 배열로 클래스 파일을 읽습니다.
  2. private Class getClass(String name): 이 메서드는 loadClassFileData() 함수를 호출하고 상위 defineClass() 메서드를 호출하여 클래스를 생성하고 반환합니다.< /리>
  3. public Class loadClass(String name): 이 메서드는 클래스 로드를 담당합니다. 클래스 이름이 com.journaldev(샘플 클래스)로 시작하면 getClass() 메서드를 사용하여 로드하거나 상위 loadClass() 함수를 호출하여 로드합니다.
  4. public CCLoader(ClassLoader parent): 부모 ClassLoader 설정을 담당하는 생성자입니다.

import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
 
/**
 * Our Custom ClassLoader to load the classes. Any class in the com.journaldev
 * package will be loaded using this ClassLoader. For other classes, it will delegate the request to its Parent ClassLoader.
 *
 */
public class CCLoader extends ClassLoader {
 
    /**
     * This constructor is used to set the parent ClassLoader
     */
    public CCLoader(ClassLoader parent) {
        super(parent);
    }
 
    /**
     * Loads the class from the file system. The class file should be located in
     * the file system. The name should be relative to get the file location
     *
     * @param name
     *            Fully Classified name of the class, for example, com.journaldev.Foo
     */
    private Class getClass(String name) throws ClassNotFoundException {
        String file = name.replace('.', File.separatorChar) + ".class";
        byte[] b = null;
        try {
            // This loads the byte code data from the file
            b = loadClassFileData(file);
            // defineClass is inherited from the ClassLoader class
            // that converts byte array into a Class. defineClass is Final
            // so we cannot override it
            Class c = defineClass(name, b, 0, b.length);
            resolveClass(c);
            return c;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
 
    /**
     * Every request for a class passes through this method. If the class is in
     * com.journaldev package, we will use this classloader or else delegate the
     * request to parent classloader.
     *
     *
     * @param name
     *            Full class name
     */
    @Override
    public Class loadClass(String name) throws ClassNotFoundException {
        System.out.println("Loading Class '" + name + "'");
        if (name.startsWith("com.journaldev")) {
            System.out.println("Loading Class using CCLoader");
            return getClass(name);
        }
        return super.loadClass(name);
    }
 
    /**
     * Reads the file (.class) into a byte array. The file should be
     * accessible as a resource and make sure that it's not in Classpath to avoid
     * any confusion.
     *
     * @param name
     *            Filename
     * @return Byte array read from the file
     * @throws IOException
     *             if an exception comes in reading the file
     */
    private byte[] loadClassFileData(String name) throws IOException {
        InputStream stream = getClass().getClassLoader().getResourceAsStream(
                name);
        int size = stream.available();
        byte buff[] = new byte[size];
        DataInputStream in = new DataInputStream(stream);
        in.readFully(buff);
        in.close();
        return buff;
    }
}

2. CCRun.java

이것은 메서드를 호출하기 위한 Java Reflection API가 있는 테스트 클래스입니다.

import java.lang.reflect.Method;
 
public class CCRun {
 
    public static void main(String args[]) throws Exception {
        String progClass = args[0];
        String progArgs[] = new String[args.length - 1];
        System.arraycopy(args, 1, progArgs, 0, progArgs.length);

        CCLoader ccl = new CCLoader(CCRun.class.getClassLoader());
        Class clas = ccl.loadClass(progClass);
        Class mainArgType[] = { (new String[0]).getClass() };
        Method main = clas.getMethod("main", mainArgType);
        Object argsArray[] = { progArgs };
        main.invoke(null, argsArray);

        // Below method is used to check that the Foo is getting loaded
        // by our custom class loader i.e CCLoader
        Method printCL = clas.getMethod("printCL", null);
        printCL.invoke(null, new Object[0]);
    }
 
}

3. Foo.java 및 Bar.java

이들은 사용자 지정 클래스 로더에 의해 로드되는 테스트 클래스입니다. ClassLoader 정보를 인쇄하기 위해 호출되는 printCL() 메서드가 있습니다. Foo 클래스는 커스텀 클래스 로더에 의해 로드됩니다. Foo는 Bar 클래스를 사용하므로 Bar 클래스도 사용자 정의 클래스 로더에 의해 로드됩니다.

package com.journaldev.cl;
 
public class Foo {
    static public void main(String args[]) throws Exception {
        System.out.println("Foo Constructor >>> " + args[0] + " " + args[1]);
        Bar bar = new Bar(args[0], args[1]);
        bar.printCL();
    }
 
    public static void printCL() {
        System.out.println("Foo ClassLoader: "+Foo.class.getClassLoader());
    }
}
package com.journaldev.cl;
 
public class Bar {
 
    public Bar(String a, String b) {
        System.out.println("Bar Constructor >>> " + a + " " + b);
    }
 
    public void printCL() {
        System.out.println("Bar ClassLoader: "+Bar.class.getClassLoader());
    }
}

4. Java 사용자 정의 ClassLoader 실행 단계

먼저 명령줄을 통해 모든 클래스를 컴파일합니다. 그런 다음 세 개의 인수를 전달하여 CCRun 클래스를 실행합니다. 첫 번째 인수는 클래스 로더에 의해 로드될 Foo 클래스의 완전히 분류된 이름입니다. 다른 두 인수는 Foo 클래스 기본 함수와 Bar 생성자에 전달됩니다. 실행 단계와 출력은 아래와 같습니다.

$ javac -cp . com/journaldev/cl/Foo.java
$ javac -cp . com/journaldev/cl/Bar.java
$ javac CCLoader.java
$ javac CCRun.java
CCRun.java:18: warning: non-varargs call of varargs method with inexact argument type for last parameter;
cast to java.lang.Class<?> for a varargs call
cast to java.lang.Class<?>[] for a non-varargs call and to suppress this warning
Method printCL = clas.getMethod("printCL", null);
^
1 warning
$ java CCRun com.journaldev.cl.Foo 1212 1313
Loading Class 'com.journaldev.cl.Foo'
Loading Class using CCLoader
Loading Class 'java.lang.Object'
Loading Class 'java.lang.String'
Loading Class 'java.lang.Exception'
Loading Class 'java.lang.System'
Loading Class 'java.lang.StringBuilder'
Loading Class 'java.io.PrintStream'
Foo Constructor >>> 1212 1313
Loading Class 'com.journaldev.cl.Bar'
Loading Class using CCLoader
Bar Constructor >>> 1212 1313
Loading Class 'java.lang.Class'
Bar ClassLoader: CCLoader@71f6f0bf
Foo ClassLoader: CCLoader@71f6f0bf
$

출력을 보면 com.journaldev.cl.Foo 클래스를 로드하려고 합니다. java.lang.Object 클래스를 확장하고 있기 때문에 먼저 Object 클래스를 로드하려고 합니다. 따라서 요청은 부모 클래스에 위임하는 CCLoader loadClass 메서드로 전달됩니다. 따라서 상위 클래스 로더는 Object, String 및 기타 Java 클래스를 로드합니다. ClassLoader는 파일 시스템에서 Foo 및 Bar 클래스만 로드합니다. printCL() 함수의 출력에서 명확합니다. 우리는 loadClassFileData() 기능을 변경하여 FTP 서버에서 바이트 배열을 읽거나 제3자 서비스를 호출하여 즉시 클래스 바이트 배열을 가져올 수 있습니다. 이 기사가 Java ClassLoader 작동을 이해하고 파일 시스템에서 가져오는 것보다 더 많은 작업을 수행하도록 확장하는 방법을 이해하는 데 유용하기를 바랍니다.

커스텀 ClassLoader를 기본 ClassLoader로 만들기

Java 옵션을 사용하여 JVM이 시작될 때 사용자 정의 클래스 로더를 기본 클래스 로더로 만들 수 있습니다. 예를 들어 java classloader 옵션을 제공한 후 ClassLoaderTest 프로그램을 다시 한 번 실행하겠습니다.

$ javac -cp .:../lib/mysql-connector-java-5.0.7-bin.jar com/journaldev/classloader/ClassLoaderTest.java
$ java -cp .:../lib/mysql-connector-java-5.0.7-bin.jar -Djava.system.class.loader=CCLoader com.journaldev.classloader.ClassLoaderTest
Loading Class 'com.journaldev.classloader.ClassLoaderTest'
Loading Class using CCLoader
Loading Class 'java.lang.Object'
Loading Class 'java.lang.String'
Loading Class 'java.lang.System'
Loading Class 'java.lang.StringBuilder'
Loading Class 'java.util.HashMap'
Loading Class 'java.lang.Class'
Loading Class 'java.io.PrintStream'
class loader for HashMap: null
Loading Class 'sun.net.spi.nameservice.dns.DNSNameService'
class loader for DNSNameService: sun.misc.Launcher$ExtClassLoader@24480457
class loader for this class: CCLoader@38503429
Loading Class 'com.mysql.jdbc.Blob'
sun.misc.Launcher$AppClassLoader@2f94ca6c
$

CCLoader는 com.journaldev 패키지에 있기 때문에 ClassLoaderTest 클래스를 로드하고 있습니다.

GitHub 리포지토리에서 ClassLoader 예제 코드를 다운로드할 수 있습니다.