自定义类加载器案例
目标: 自定义类加载器,加载指定路径在D盘下的lib文件夹下的类。
步骤:
1.新建一个需要被加载的类Test.java
2.编译Test.java 到指定lib目录
3.自定义类加载器 ClassLoader 继承 ClassLoader :重写 findClass()方法 调用 defineClass()方法
4.测试自定义类加载器
创建一个自定义类加载器,以加载指定路径下的类文件:
步骤 1:创建需要被加载的类 Test.java
并编译到指定的 lib
目录下。
假设您的 Test.java
内容如下:
java
public class Test {
public void printMessage() {
System.out.println("Hello from the custom class!");
}
}
然后,将其编译到 D:\lib
目录下。
步骤 2:创建自定义类加载器 TestClassLoader
继承 ClassLoader
并重写 findClass()
方法。
java
import java.io.*;
public class TestClassLoader extends ClassLoader {
private String classPath;
public TestClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 读取类文件的字节码
String fileName = name.replace(".", File.separator) + ".class";
byte[] bytes = loadClassData(fileName);
// 使用defineClass方法将字节码转换为Class对象
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException("Class not found: " + name, e);
}
}
private byte[] loadClassData(String className) throws IOException {
String path = classPath + File.separator + className;
try (InputStream inputStream = new FileInputStream(path);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
int bytesRead;
byte[] buffer = new byte[4096];
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
return outputStream.toByteArray();
}
}
}
步骤 3:测试自定义类加载器。
java
public class Main {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
// 指定类文件的路径
String classPath = "D:\\lib";
// 创建自定义类加载器
TestClassLoader classLoader = new TestClassLoader(classPath);
// 使用自定义类加载器加载类
Class<?> loadedClass = classLoader.loadClass("Test");
// 创建实例并调用方法
Object instance = loadedClass.newInstance();
Method method = loadedClass.getMethod("printMessage");
method.invoke(instance);
}
}
上述代码中,首先创建了一个自定义类加载器 TestClassLoader
,它会加载指定路径下的类文件。然后,在 Main
类中使用这个自定义类加载器加载了 Test
类并调用了其方法。
1.2.4 双亲委派模型与打破双亲委派
1)什么是双亲委派?
当一个类加载器收到类加载任务,会先交给其父类加载器去完成。因此,最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,子类才会尝试执行加载任务。
当一个类加载器收到类加载任务,会先交给其父类加载器去完成。因此,最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,子类才会尝试执行加载任务。Oracle 官网文档描述:
The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a "parent" class loader. When loading a class, a class loader first "delegates" the search for the class to its parent class loader before attempting to find the class itself. ------ Oracel Document
docs.oracle.com/javase/tuto...
看到这里,应该叫父亲委派对吧?那么为什么要叫双亲委派呢,因为最早的翻译者,导致双亲委派 的概念流行起来了。
2)为什么需要双亲委派呢?
考虑到安全因素,双亲委派可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
比如:加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。
3)双亲委派机制源码:
java
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
- 首先检查该类是否已被加载,如果加载则直接返回。
- 然后调用父类加载器的loadClass()尝试加载。
- 如果父类加载失败,则调用findClass()自己尝试加载该类。
- 如果自己也加载失败,则抛出ClassNotFoundException。
- 如果resolve参数为true,还会额外进行解析(linking阶段)。
ClassLoader的子类可以override这个方法自定义类加载过程。比如URLClassLoader会从 jar 文件加载类。
4)为什么还需要破坏双亲委派?
在实际应用中,双亲委派解决了Java 基础类统一加载的问题,但是却存在着缺陷。JDK中的基础类作为典型的api被用户调用,但是也存在api调用用户代码的情况,典型的如:SPI代码。这种情况就需要打破双亲委派模式。
举个例子:数据库驱动DriverManager。以Driver接口为例,Driver接口定义在JDK中,其实现由各个数据库的服务商来提供,由系统类加载器加载。这个时候就需要启动类加载器来 委托 子类来加载Driver实现,这就破坏了双亲委派。类似情况还有很多
5)如何破坏双亲委派?
第一种方式
在 jdk 1.2 之前,那时候还没有双亲委派模型,不过已经有了 ClassLoader 这个抽象类,所以已经有人继承这个抽象类,重写 loadClass 方法来实现用户自定义类加载器。
而在 1.2 的时候要引入双亲委派模型,为了向前兼容, loadClass 这个方法还得保留着使之得以重写,新搞了个 findClass 方法让用户去重写,并呼吁大家不要重写 loadClass 只要重写 findClass。
这就是第一次对双亲委派模型的破坏,因为双亲委派的逻辑在 loadClass 上,但是又允许重写loadClass,重写了之后就可以破坏委派逻辑了。
第二种方式:
双亲委派机制是一种自上而下的加载需求,越往上类越基础。
SPI代码打破了双亲委派
如果出现SPI相关代码时,我们应该如何解决基础类去加载用户代码类呢?
这个时候,JVM不得不妥协,推出线程上下文类加载器的概念,去解决该问题。这样也就打破了双亲委派
线程上下文类加载器
(ThreadContextClassLoader)
第三种方式
为了满足热部署、不停机更新需求。OSGI 就是利用自定义的类加载器机制来完成模块化热部署,而它实现的类加载机制就没有完全遵循自下而上的委托,有很多平级之间的类加载器查找。