클래스 로더는 Java™에서 매우 중요한 개념입니다. 클래스 로더는 Java 클래스의 바이트 코드를 Java 가상 머신에 로드하는 역할을 합니다. 이 글에서는 먼저 프록시 모드, 클래스 및 스레드 컨텍스트 클래스 로더를 로드하는 구체적인 프로세스 등을 포함하여 Java 클래스 로더의 기본 개념을 자세히 소개하고, 그 방법을 소개합니다. 자신만의 클래스 로더를 개발하고, 마지막으로 웹 컨테이너와 OSGi™에 클래스 로더를 적용하는 방법을 소개합니다.
IBM Bluemix 클라우드 플랫폼에서 다음 앱을 개발하고 배포하세요.
평가판 시작
클래스 로더는 Java 언어의 혁신이며 Java 언어 인기의 중요한 이유 중 하나입니다. 이를 통해 Java 클래스를 JVM(Java Virtual Machine)에 동적으로 로드하고 실행할 수 있습니다. 클래스 로더는 JDK 1.0부터 사용되었으며 원래 Java 애플릿의 요구 사항을 충족하기 위해 개발되었습니다. Java Applet은 Java 클래스 파일을 원격에서 브라우저로 다운로드하여 실행해야 합니다. 클래스 로더는 이제 웹 컨테이너와 OSGi에서 널리 사용됩니다. 일반적으로 Java 애플리케이션 개발자는 클래스 로더와 직접 상호 작용할 필요가 없습니다. Java Virtual Machine의 기본 동작은 대부분의 상황 요구 사항을 충족하기에 충분합니다. 하지만 클래스 로더와 상호 작용해야 하는 상황에 직면하고 클래스 로더의 메커니즘에 대해 잘 알지 못하는 경우 디버깅 ClassNotFound예외 및 NoClassDefFoundError 및 기타 예외. 이 기사에서는 독자가 Java 언어에서 이 중요한 개념을 깊이 이해할 수 있도록 Java의 클래스 로더를 자세히 소개합니다. 아래에서는 먼저 몇 가지 관련 기본 개념을 소개합니다.
클래스 로더의 기본 개념이름에서 알 수 있듯이 클래스 로더는 Java 클래스를 Java 가상 머신에 로드하는 데 사용됩니다. 일반적으로 Java 가상 머신은 다음과 같이 Java 클래스를 사용합니다. Java 소스 프로그램(.java 파일)은 Java 컴파일러에 의해 컴파일된 후 Java 바이트 코드(.class 파일)로 변환됩니다. 클래스 로더는 Java 바이트 코드를 읽고 이를 java.lang.Class 클래스의 인스턴스로 변환하는 일을 담당합니다. 이러한 각 인스턴스는 Java 클래스를 나타냅니다. 이 클래스의
객체는 이 인스턴스의 newInstance() 메서드를 통해 생성할 수 있습니다. 실제 상황은 더 복잡할 수 있습니다. 예를 들어 Java 바이트 코드는 도구를 통해 동적으로 생성되거나 네트워크를 통해 다운로드될 수 있습니다. 기본적으로 모든 클래스 로더는 java.lang.ClassLoader 클래스의 인스턴스입니다. 이 Java 클래스는 아래에 자세히 설명되어 있습니다.
java.lang.ClassLoader**클래스 소개**java.lang.ClassLoader 클래스의 기본 역할은 지정된 클래스의 이름을 기반으로 해당 바이트 코드를 찾거나 생성하는 것입니다. class. 그런 다음 이러한 바이트 코드에서 Java 클래스, 즉 java.lang.Class 클래스의 인스턴스를 정의합니다. 또한 ClassLoader는 이미지 파일,
구성 파일 등 Java 애플리케이션에 필요한 리소스를 로드하는 역할도 합니다. 그러나 이 문서에서는 클래스 로드 기능에 대해서만 설명합니다. 클래스 로딩 책임을 완수하기 위해 ClassLoader는 일련의 메소드를 제공하며, 더 중요한 메소드는 표 1에 나와 있습니다. 이러한 방법에 대한 자세한 내용은 아래에 설명되어 있습니다.
표 1. ClassLoader의 클래스 로딩 관련 메소드
메소드 설명
getParent()이 클래스 로더의 상위 클래스 로더를 반환합니다. loadClass(String name)
name이라는 클래스를 로드하고 반환된 결과는 java.lang.Class 클래스의 인스턴스입니다. findClass(String name)name이라는 클래스를 찾으면 반환된 결과는 java.lang.Class 클래스의 인스턴스입니다. findLoadedClass(String name)name이라는 이름의 로드된 클래스를 찾으면 반환된 결과는 java.lang.Class 클래스의 인스턴스입니다. defineClass(String name, byte[] b, int off, int len)바이트array b의 내용을 Java 클래스로 변환하고 결과를 반환합니다. java.lang.Class 클래스의 인스턴스입니다. 이 메소드는 최종으로 선언됩니다.
resolveClass(Class> c)지정된 Java 클래스를 연결합니다. 표 1에 제시된 메소드의 경우 클래스 이름을 나타내는 name 매개변수의 값은 클래스의 바이너리 이름입니다. 주목해야 할 것은 com.example.Sample$1 및 com.example.Sample$Inner와 같은 내부 클래스의 표현입니다. 이러한 메소드는 클래스 로더의 작동 메커니즘이 소개될 때 아래에서 더 자세히 설명됩니다. 다음은 클래스 로더의 트리형 조직 구조를 설명합니다.클래스 로더의 나무형 조직 구조
Java의 클래스 로더는 크게 두 가지 범주로 나눌 수 있습니다. 하나는 시스템에서 제공하고 다른 하나는 Java 애플리케이션 개발자가 작성합니다. 시스템에서 제공하는 세 가지 주요 클래스 로더가 있습니다:
부트스트랩 클래스 로더): Java의 핵심 라이브러리를 로드하는 데 사용되며 java.lang의 상속 네이티브 코드로 구현됩니다. .클래스로더. 확장 클래스 로더: Java 확장 라이브러리를 로드하는 데 사용됩니다. JVM(Java Virtual Machine)의 구현은 확장 라이브러리 디렉토리를 제공합니다. 클래스 로더는 이 디렉토리에서 Java 클래스를 찾아 로드합니다.
시스템 클래스 로더: Java 애플리케이션의 클래스 경로(CLASSPATH)에 따라 Java 클래스를 로드합니다. 일반적으로 Java 애플리케이션 클래스는 이를 통해 로드됩니다. ClassLoader.getSystemClassLoader()를 통해 얻을 수 있습니다.
부트스트랩 클래스 로더를 제외한 모든 클래스 로더에는 상위 클래스 로더가 있습니다. 표 1에 주어진 getParent() 메소드를 통해 얻을 수 있다. 시스템에서 제공하는 클래스 로더의 경우, 시스템 클래스 로더의 상위 클래스 로더는 확장 클래스 로더이고, 확장 클래스 로더의 상위 클래스 로더는 개발자가 작성한 클래스 로더의 부트 클래스 로더입니다. 클래스 로더는 이 클래스 로더의 Java 클래스를 로드하는 클래스 로더입니다.
다른 Java 클래스와 마찬가지로 클래스 로더 Java 클래스도 클래스 로더에 의해 로드되기 때문입니다. 일반적으로 개발자가 작성한 클래스 로더의 상위 클래스 로더는 시스템 클래스 로더입니다. 클래스 로더는 이러한 방식으로 구성되어 트리 구조를 형성합니다. 트리의 루트 노드는 부트 클래스 로더입니다. 그림 1은 화살표가 상위 클래스 로더를 가리키는 일반적인 클래스 로더 트리 조직 구조 다이어그램을 보여줍니다.
그림 1. 클래스 로더 트리 구성 구조 도식
코드 목록 1은 클래스 로더의 트리형 조직 구조를 보여줍니다.
목록 1. 클래스 로더의 트리 구성 시연
public class ClassLoaderTree { public static void main(String[] args) { ClassLoader loader = ClassLoaderTree.class.getClassLoader(); while (loader != null) { System.out.println(loader.toString()); loader = loader.getParent(); } } }
참조 , getClassLoader() 메소드를 통해 얻을 수 있습니다. 코드 목록 1에서는 getParent() 메서드가 재귀적으로 호출되어 모든 상위 클래스 로더를 출력합니다. 코드 목록 1의 실행 결과는 코드 목록 2에 표시됩니다.
목록 2. 클래스 로더의 트리 조직 구조를 보여주는 실행 결과 sun.misc.Launcher$AppClassLoader@9304b1
sun.misc.Launcher$ExtClassLoader@190d11
코드 목록 2에 표시된 것처럼 첫 번째 출력은 ClassLoaderTree 클래스의 클래스 로더, 즉 시스템 클래스 로더입니다. 이는 sun.misc.Launcher$AppClassLoader 클래스의 인스턴스입니다. 두 번째 출력은 sun.misc.Launcher$ExtClassLoader 클래스의 인스턴스인 확장 클래스 로더입니다. 부트 클래스 로더는 여기서 출력되지 않습니다. 이는 상위 클래스 로더가 부트 클래스 로더일 때 일부 JDK 구현이 null을 반환하기 때문입니다.
클래스 로더의 트리형 조직 구조를 이해한 후 클래스 로더의 프록시 모드를 소개합니다.
클래스 로더의 프록시 모드클래스 로더가 클래스의 바이트 코드를 스스로 찾아 정의하려고 하면 먼저 상위 클래스 로더로 프록시를 수행하고 상위 클래스는 로더는 클래스 로더가 먼저 이 클래스를 로드하려고 시도하며 계속해서 시도합니다. 프록시 패턴의 동기를 소개하기 전에 먼저 Java 가상 머신이 두 Java 클래스가 동일하다고 판단하는 방법을 설명해야 합니다. JVM(Java Virtual Machine)은 클래스의 전체 이름이 동일한지 여부뿐만 아니라 이 클래스를 로드하는 클래스 로더가 동일한지도 확인합니다. 두 클래스는 둘 다 동일한 경우에만 동일한 것으로 간주됩니다. 동일한 바이트 코드가 다른 클래스 로더에 의해 로드되더라도 획득되는 클래스는 다릅니다. 예를 들어, Java 클래스 com.example.Sample은 컴파일 후에 바이트 코드 파일 Sample.class를 생성합니다. 두 개의 서로 다른 클래스 로더인 ClassLoaderA와 ClassLoaderB는 각각 이 Sample.class 파일을 읽고 이 클래스를 나타내기 위해 java.lang.Class 클래스의 두 인스턴스를 정의합니다. 이 두 인스턴스는 동일하지 않습니다. Java 가상 머신에서는 서로 다른 클래스입니다. 이 두 클래스의 개체를 서로 할당하려고 하면 런타임 예외 ClassCastException이 발생합니다. 다음은 예시를 통해 자세히 설명합니다. Java 클래스 com.example.Sample은 코드 목록 3에 나와 있습니다.
목록 3. com.example.Sample 클래스
package com.example; public class Sample { private Sample instance; public void setSample(Object instance) { this.instance = (Sample) instance; } }
如 代码清单 3所示,com.example.Sample类的方法 setSample接受一个 java.lang.Object类型的参数,并且会把该参数强制转换成com.example.Sample类型。测试 Java 类是否相同的代码如 代码清单 4所示。
清单 4. 测试 Java 类是否相同
public void testClassIdentity() { String classDataRootPath = "C:\workspace\Classloader\classData"; File SystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath); FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath); String className = "com.example.Sample"; try { Class<?> class1 = fscl1.loadClass(className); Object obj1 = class1.newInstance(); Class<?> class2 = fscl2.loadClass(className); Object obj2 = class2.newInstance(); Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class); setSampleMethod.invoke(obj1, obj2); } catch (Exception e) { e.printStackTrace(); } }
代码清单 4中使用了类 FileSystemClassLoader的两个不同实例来分别加载类 com.example.Sample,得到了两个不同的java.lang.Class的实例,接着通过 newInstance()方法分别生成了两个类的对象 obj1和 obj2,最后通过 Java 的反射 API 在对象 obj1上调用方法 setSample,试图把对象 obj2赋值给 obj1内部的 instance对象。代码清单 4的运行结果如 代码清单 5所示。
清单 5. 测试 Java 类是否相同的运行结果
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at classloader.ClassIdentity.testClassIdentity(ClassIdentity.java:26)
at classloader.ClassIdentity.main(ClassIdentity.java:9)
Caused by: java.lang.ClassCastException: com.example.Sample
cannot be cast to com.example.Sample
at com.example.Sample.setSample(Sample.java:7)
... 6 more
从 代码清单 5给出的运行结果可以看到,运行时抛出了 java.lang.ClassCastException异常。虽然两个对象 obj1和 obj2的类的名字相同,但是这两个类是由不同的类加载器实例来加载的,因此不被 Java 虚拟机认为是相同的。
了解了这一点之后,就可以理解代理模式的设计动机了。代理模式是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。
不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。这种技术在许多框架中都被用到,后面会详细介绍。
下面具体介绍类加载器加载类的详细过程。
加载类的过程
在前面介绍类加载器的代理模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用 defineClass来实现的;而启动类的加载过程是通过调用 loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。
方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是java.lang.NoClassDefFoundError异常。
类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。
下面讨论另外一种类加载器:线程上下文类加载器。
线程上下文类加载器
스레드 컨텍스트 클래스 로더(컨텍스트 클래스 로더)는 JDK 1.2부터 도입되었습니다. 클래스 java.lang.Thread의 getContextClassLoader() 및 setContextClassLoader(ClassLoader cl) 메소드는 스레드의 컨텍스트 클래스 로더를 획득하고 설정하는 데 사용됩니다. setContextClassLoader(ClassLoader cl) 메소드를 통해 설정되지 않은 경우 스레드는 상위 스레드의 컨텍스트 클래스 로더를 상속합니다. Java 애플리케이션이 실행되는 초기 스레드에 대한 컨텍스트 클래스 로더는 시스템 클래스 로더입니다. 스레드에서 실행되는 코드는 이러한 유형의 로더를 통해 클래스와 리소스를 로드할 수 있습니다.
위에서 언급한 클래스 로더의 프록시 모드는 Java 애플리케이션 개발에서 직면하게 되는 클래스 로더의 모든 문제를 해결할 수는 없습니다. Java는 다양한 서비스 공급자 인터페이스(SPI)를 제공하므로 제3자가 이러한 인터페이스에 대한 구현을 제공할 수 있습니다. 일반적인 SPI에는 JDBC, JCE, JNDI, JAXP 및 JBI가 포함됩니다. 이러한 SPI 인터페이스는 Java 코어 라이브러리에서 제공됩니다. 예를 들어 JAXP의 SPI 인터페이스 정의는 javax.xml.parsers 패키지에 포함되어 있습니다. 이러한 SPI 구현 코드는 Java 애플리케이션이 의존하는 jar 패키지로 포함될 가능성이 높으며 JAXP SPI를 구현하는 Apache Xerces에 포함된 jar 패키지와 같은 클래스 경로(CLASSPATH)를 통해 찾을 수 있습니다. SPI 인터페이스의 코드는 특정 구현 클래스를 로드해야 하는 경우가 많습니다. 예를 들어 JAXP의 javax.xml.parsers.DocumentBuilderFactory 클래스에 있는 newInstance() 메서드는 DocumentBuilderFactory의 새 인스턴스를 생성하는 데 사용됩니다. 여기서 인스턴스의 실제 클래스는 SPI 구현에서 제공하는 javax.xml.parsers.DocumentBuilderFactory에서 상속됩니다. 예를 들어 Apache Xerces에서 구현된 클래스는 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl입니다. 문제는 SPI 인터페이스가 Java 코어 라이브러리의 일부이고 부트 클래스 로더에 의해 로드된다는 것입니다. SPI에 의해 구현된 Java 클래스는 일반적으로 시스템 클래스 로더에 의해 로드됩니다. 부트 클래스 로더는 Java 코어 라이브러리만 로드하기 때문에 SPI 구현 클래스를 찾을 수 없습니다. 또한 시스템 클래스 로더의 조상 클래스 로더이기 때문에 시스템 클래스 로더로 프록시할 수 없습니다. 즉, 클래스 로더의 프록시 모드는 이 문제를 해결할 수 없습니다. 스레드 컨텍스트 클래스 로더는 이 문제를 정확하게 해결합니다. 설정이 이루어지지 않은 경우 Java 애플리케이션 스레드의 컨텍스트 클래스 로더는 기본적으로 시스템 컨텍스트 클래스 로더로 설정됩니다. SPI 인터페이스 코드에서 스레드 컨텍스트 클래스 로더를 사용하면 SPI 구현 클래스를 성공적으로 로드할 수 있습니다. 스레드 컨텍스트 클래스 로더는 많은 SPI 구현에서 사용됩니다. 다음은 클래스를 로드하는 또 다른 방법인 Class.forName을 소개합니다.
Class.forName
Class.forName은 클래스를 로드하는 데에도 사용할 수 있는 정적
메서드입니다. 이 메소드에는 Class.forName(문자열 이름, 부울 초기화, ClassLoader 로더) 및 Class.forName(String className)의 두 가지 형식이 있습니다. 매개변수 이름의 첫 번째 형식은 클래스의 전체 이름을 나타내며, 초기화는 클래스를 초기화할지 여부를 나타냅니다. 두 번째 형식은 초기화 매개변수의 값을 true로 설정하고, loader의 값을 현재 클래스의 클래스 로더로 설정하는 것과 동일합니다. Class.forName의 매우 일반적인 용도는 데이터베이스 드라이버 를 로드할 때입니다. 예를 들어, Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()는 Apache Derby 데이터베이스의 드라이버를 로드하는 데 사용됩니다. 클래스 로더와 관련된 기본 개념을 소개한 후, 자신만의 클래스 로더를 개발하는 방법을 소개합니다.
대부분의 경우 시스템에서 기본적으로 제공하는 클래스 로더 구현이 요구 사항을 충족할 수 있습니다. 그러나 어떤 경우에는 여전히 애플리케이션을 위한 자체 클래스 로더를 개발해야 합니다. 예를 들어, 애플리케이션은 보안을 보장하기 위해 Java 클래스 바이트 코드를 암호화합니다. 이때, 특정 네트워크 주소에서 암호화된 바이트코드를 읽어와 이를 복호화하고 검증한 뒤, 최종적으로 자바 가상 머신에서 실행될 클래스를 정의하기 위해서는 자신만의 클래스 로더가 필요하다. 다음은 두 가지 구체적인 예를 통해 클래스 로더의 개발을 보여줍니다.
파일 시스템클래스 로더첫 번째 클래스 로더는 파일 시스템에 저장된 Java 바이트 코드를 로드하는 데 사용됩니다. 전체 구현은 코드 목록 6에 나와 있습니다.
public class FileSystemClassLoader extends ClassLoader { private String rootDir; public FileSystemClassLoader(String rootDir) { this.rootDir = rootDir; } protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] getClassData(String className) { String path = classNameToPath(className); try { InputStream ins = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; } }
如 代码清单 6所示,类 FileSystemClassLoader继承自类 java.lang.ClassLoader。在 表 1中列出的 java.lang.ClassLoader类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可。java.lang.ClassLoader类的方法loadClass()封装了前面提到的代理模式的实现。该方法会首先调用 findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的 loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用 findClass()方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写 findClass()方法。
类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。
网络类加载器
下面将通过一个网络类加载器来说明如何通过类加载器来实现组件的动态更新。即基本的场景是:Java 字节代码(.class)文件存放在服务器上,客户端通过网络的方式获取字节代码并执行。当有版本更新的时候,只需要替换掉服务器上保存的文件即可。通过类加载器可以比较简单的实现这种需求。
类 NetworkClassLoader负责通过网络下载 Java 类字节代码并定义出 Java 类。它的实现与 FileSystemClassLoader类似。在通过NetworkClassLoader加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用 Java 反射 API。另外一种做法是使用接口。需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为客户端代码的类加载器找不到这些类。使用 Java 反射 API 可以直接调用 Java 类的方法。而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类。在客户端通过相同的接口来使用这些实现类。网络类加载器的具体代码见 下载。
在介绍完如何开发自己的类加载器之后,下面说明类加载器和 Web 容器的关系。
类加载器与 Web 容器
对于运行在 Java EE™容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。
绝大多数情况下,Web 应用的开发人员不需要考虑与类加载器相关的细节。下面给出几条简单的原则:
每个 Web 应用自己的 Java 类文件和使用的库的 jar 包,分别放在 WEB-INF/classes和 WEB-INF/lib目录下面。
多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面。
当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。
在介绍完类加载器与 Web 容器的关系之后,下面介绍它与 OSGi 的关系。
클래스 로더 및 OSGi
OSGi™는 Java의 동적 모듈 시스템입니다. 이는 개발자에게 서비스 지향 및 구성 요소 기반 런타임 환경을 제공하고 소프트웨어의 라이프 사이클을 관리하는 표준 방법을 제공합니다. OSGi는 많은 제품에 구현 및 배포되었으며 오픈 소스 커뮤니티에서 광범위한 지원을 받았습니다. Eclipse는 OSGi 기술을 기반으로 제작되었습니다.
OSGi의 모든 모듈(번들)에는 Java 패키지와 클래스가 포함되어 있습니다. 모듈은 가져와야 하는 다른 모듈의 Java 패키지 및 클래스를 선언하거나(Import-Package를 통해) 다른 모듈에서 사용할 수 있도록 자체 패키지와 클래스를 선언하고 내보낼 수 있습니다(Export를 통해). -패키지) . 이는 모듈 내에서 특정 Java 패키지와 클래스를 숨기고 공유할 수 있어야 함을 의미합니다. 이는 OSGi의 고유한 클래스 로더 메커니즘을 통해 달성됩니다. OSGi의 각 모듈에는 해당 클래스 로더가 있습니다. 모듈 자체에 포함된 Java 패키지와 클래스를 로드하는 일을 담당합니다. Java 코어 라이브러리(java로 시작하는 패키지 및 클래스)에서 클래스를 로드해야 하는 경우 이를 완료하기 위해 상위 클래스 로더(일반적으로 시작 클래스 로더)로 프록시됩니다. 가져온 Java 클래스를 로드해야 하는 경우 Java 클래스를 내보내는 모듈에 위임하여 로드를 완료합니다. 모듈은 상위 클래스 로더가 로드해야 하는 특정 Java 패키지 및 클래스를 명시적으로 선언할 수도 있습니다. 시스템 속성 org.osgi.framework.bootdelegation의 값을 설정하기만 하면 됩니다.
두 개의 모듈 BundleA와 BundleB가 있고 두 모듈 모두 해당 클래스 로더인 classLoaderA와 classLoaderB를 갖고 있다고 가정해 보겠습니다. BundleA에는 com.bundleA.Sample 클래스가 포함되어 있으며 클래스는 내보내기로 선언되었습니다. 즉, 다른 모듈에서 사용할 수 있습니다. BundleB는 BundleA에서 제공하는 com.bundleA.Sample 클래스를 가져오고 com.bundleA.Sample에서 상속되는 com.bundleB.NewSample 클래스를 포함한다고 선언합니다. BundleB가 시작되면 해당 클래스 로더 classLoaderB는 com.bundleB.NewSample 클래스를 로드해야 하며, 이 클래스는 com.bundleA.Sample 클래스를 로드해야 합니다. BundleB는 com.bundleA.Sample 클래스를 가져올 것으로 선언하므로 classLoaderB는 com.bundleA.Sample 클래스를 로드하는 작업을 이 클래스를 내보내는 BundleA의 클래스 로더 classLoaderA에 위임합니다. classLoaderA는 모듈 내에서 com.bundleA.Sample 클래스를 찾아 정의합니다. 결과 클래스 com.bundleA.Sample 인스턴스는 이 클래스를 선언하는 모든 모듈에서 사용할 수 있습니다. Java로 시작하는 클래스의 경우 상위 클래스 로더에 의해 로드됩니다. 시스템 속성 org.osgi.framework.bootdelegation=com.example.core.*가 선언되면 상위 클래스 로더는 com.example.core 패키지에서 클래스 로드를 완료합니다.
OSGi 모듈의 클래스 로더 구조를 사용하면 다양한 버전의 클래스가 Java 가상 머신에 공존할 수 있어 유연성이 뛰어납니다. 그러나 이러한 차이는 특히 모듈이 제3자가 제공하는 라이브러리를 사용해야 하는 경우 개발자에게 몇 가지 문제를 가져올 수도 있습니다. 다음은 좋은 제안입니다.
클래스 라이브러리가 하나의 모듈에서만 사용되는 경우 클래스 라이브러리의 jar 패키지를 모듈에 넣고 Bundle-ClassPath에 지정합니다.
클래스 라이브러리가 여러 모듈에서 공유되는 경우 이 클래스 라이브러리에 대해 별도의 모듈을 생성하고 다른 모듈이 사용해야 하는 Java 패키지를 내보낸 것으로 선언할 수 있습니다. 다른 모듈 선언은 이러한 클래스를 가져옵니다.
클래스 라이브러리가 SPI 인터페이스를 제공하고 스레드 컨텍스트 클래스 로더를 사용하여 SPI로 구현된 Java 클래스를 로드하는 경우 Java 클래스를 찾지 못할 수 있습니다. NoClassDefFoundError 예외가 발생하면 먼저 현재 스레드의 컨텍스트 클래스 로더가 올바른지 확인하십시오. 클래스 로더는 Thread.currentThread().getContextClassLoader()를 통해 얻을 수 있습니다. 이 클래스 로더는 이 모듈에 해당하는 클래스 로더여야 합니다. 그렇지 않은 경우에는 class.getClassLoader()를 통해 모듈에 해당하는 클래스 로더를 먼저 가져온 후 Thread.currentThread().setContextClassLoader()를 통해 현재 스레드의 컨텍스트 클래스 로더를 설정할 수 있습니다.
요약
클래스 로더는 Java 언어의 혁신입니다. 이를 통해 소프트웨어 구성 요소를 동적으로 설치하고 업데이트할 수 있습니다. 이 기사에서는 기본 개념, 프록시 모드, 스레드 컨텍스트 클래스 로더, 웹 컨테이너 및 OSGi와의 관계 등을 포함하여 클래스 로더 관련 주제를 자세히 소개합니다. 개발자가 ClassNotFoundException 및 NoClassDefFoundError와 같은 예외를 발견하면 예외를 발생시킨 클래스의 클래스 로더와 현재 스레드의 컨텍스트 클래스 로더를 확인하여 문제를 발견해야 합니다. 자신만의 클래스 로더를 개발할 때 기존 클래스 로더 조직 구조와의 조화에 주의해야 합니다.
위 내용은 Java 클래스 로더에 대한 심층적인 살펴보기의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!