双亲委派机制的概念
双亲委派机制是指当一个类加载器收到某个类加载请求时,该类加载器首先会把请求委派给父类加载器。每个类加载器都是如此,它会先委托父类加载器在自己的搜索范围内找不到对应的类时,该类加载器才会尝试自己去加载。
双亲委派模式的工作流程
双亲委派模型的核心代码
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查这类是否已经被加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果存在父类加载器,则取找该类的父类加载器
c = parent.loadClass(name, false);
} else {
//返回由引导类加载器加载的类;如果未找到,则返回 null。
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException异常
// 则说明父类加载器无法完成加载请求
}
if (c == null) {
// 在父类加载器无法加载时
// 再调用本身的findClass方法来进行加载
long t1 = System.nanoTime();
c = findClass(name);
// 这是定义类加载器;记录统计数据
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派机制的好处
- 避免类的重复加载
- 避免Java核心API被篡改
如何打破双亲委派机制?
(一) 为什么要打破双亲委派机制?
有时我们需要多次加载同名目录下的类,比如:当我们在Tomcat上部署多个服务时,不同服务上可能依赖了不同版本的第三方jar,如果此时使用双亲委派机制加载类,会导致多个服务中第三方jar只加载一次,其他服务中的其他版本jar将不会生效,导致请求结果异常。为了避免这种情况,我们需要打破双亲委派机制,不再让父类[应用类加载器]加载,而是为每个服务创建自己的子类加载器。
(二) 如何打破双亲委派机制?
打破双亲委派有两种方式:(1)不委派【SPI机制】;(2)向下委派。
Tomcat使用父类加载器加载了公用的jar,对于非公用的jar则使用自己的子类加载器进行单独加载。打破双亲委派需要重写findLoadedClass()方法。
(三) 模拟Tomcat中WebappClassLoader打破双亲委派
public class MyClassLoader extends ClassLoader {
private final String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
/**
* 重写类加载方法,模拟Tomcat打破双亲委派过程。
* 对特定类自己加载,其他类还是通过父加载器加载
*
* @param name 类的二进制名称
* @param resolve 是否需要解决该类,一般为false
* @return 二进制名称(binary name)对应的Class对象
* @throws ClassNotFoundException 如果类未被找到,会抛出ClassNotFoundException
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 基于类的binary name,对相同名称的类加锁,防止多个线程重复加载。这点不变
synchronized (getClassLoadingLock(name)) {
// 首先,先检查类是否已被加载,避免重复加载。这点不变
Class<?> c = findLoadedClass(name);
// 如果没找到,通过findClass加载。这点不变
if (c == null) {
long t1 = System.nanoTime();
/*
* 重点
* 该处对于com.tomcat.webapp(只是模拟)包下的class自己加载
* 对于其他class文件还是委托父类加载
*/
if (!name.startsWith("com.tomcat.webapp")){
c = this.getParent().loadClass(name);
}else{
c = findClass(name);
}
// ------------- 以下为JDK8原逻辑,删除获取父加载器时间 --------------
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
private byte[] loadByte(String name) throws Exception {
// 替换为实际地址
name = name.replaceAll("\\.", "/");
FileInputStream fis = null;
byte[] data = null;
try {
// 加载class文件
fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
data = new byte[len];
fis.read(data);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fis != null) {
fis.close();
}
}
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 加载class文件
byte[] data = loadByte(name);
//defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}