1、秒懂双亲委派机制
1.1、流程图
完整的流程图如下所示:
1.2、双亲委派机制的意义是什么?
1、避免一个类被重复的加载。重复会出现什么问题呢?
-
类的类型不匹配。即便两个类的名称和包名都相同,通过不同的类加载器加载仍然会被认为是两个不同类,在类转换时,可能会出现同名类不匹配的问题;
-
浪费内存;多个类加载器加载多次,所有的变量、常量和方法都会在占用一份内存;
2、保证类的安全性;避免核心类被多次加载和篡改。
通过双亲委派机制,所有的核心类在应用启动的时候都被顶层的启动类加载器加载过了,以后即使有人篡改了同类名同包名的类,也不会被再进行加载了;
3、提高类加载的性能
通过复用已经加载过的类的方式,减少了不必要的类加载操作,提升了类加载的性能。
1.3、双亲委派模型被打破的示例有哪些?为什么需要被打破?
1.3.1、Tomcat
因为Tomcat中需要加载不同的应用,而不同的应用之间的环境是相互隔离的,也就意味着即便是同样包名、类名的类也应该具备两套环境,所以此时双亲委派模型就不再试用了。
当然对于一些通用的spring或者mybatis的类,也并不需要完全的两套环境,则明显此时就需要对双亲委派模型进行定制化的开发;
那么Tomcat是怎么来做的呢?
-
CommonClassLoader:是Tomcat最基本的类加载器,它加载的类可以被Tomcat容器和Web应用访问。
- 加载
$CATALINA_HOME/lib
目录下的类和资源。 - 这些类可以被Tomcat容器和所有的Web应用访问。
- 加载
-
CatalinaClassLoader:是Tomcat容器私有的类加载器,加载类对于Web应用不可见。
- 加载
$CATALINA_HOME/server/lib
目录下的类和资源。 - 这些类对Web应用不可见,只供Tomcat容器使用。
- 用于加载Tomcat内部的类和资源,如Tomcat的管理和配置类。
- 加载
-
SharedClassLoader:各个Web应用共享的类加载器,加载的类对于所有Web应用可见,但是对于Tomcat容器不可见。
- 加载
$CATALINA_HOME/shared/lib
目录下的类和资源。 - 这些类对所有的Web应用可见,但对Tomcat容器不可见。
- 用于加载需要在多个Web应用中共享的类和资源,避免类的重复加载。
- 加载
-
WebAppClassLoader:各个Web应用私有的类加载器,加载类只对当前Web应用可见。
-
每个Web应用都有一个独立的
WebAppClassLoader
。 -
加载Web应用的
WEB-INF/classes
和WEB-INF/lib
目录下的类和资源。 -
这些类和资源只对当前Web应用可见,实现类的隔离。
-
允许不同Web应用使用不同版本的库而互不干扰,例如不同的Spring版本
-
1.3.2、热部署
热部署是指在不重启服务器的情况下,动态地部署、更新或移除 Web 应用程序。
由于用户对程序动态性的追求,比如:代码热部署、代码热替换等功能,引入了OSGi(Open Service Gateway Initiative)。【其实Tomcat也是支持热部署的,为了实现这一功能,Tomcat 需要能够在运行时重新加载类和资源。】
OSGi中的每一个模块(称为Bundle)。
当程序升级或者更新时,可以只停用、重新安装然后启动程序的其中一部分,对企业来说这是一个非常诱人的功能。
OSGi的Bundle类加载器之间只有规则,没有固定的委派关系。
各个Bundle加载器是平级关系。
不是双亲委派关系。
1.3.3、jdbc
原生的JDBC
中Driver驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库类型去实现的。
例如,MySQL的mysql-connector.jar中的Driver类具体实现的。
原生的JDBC中的类是放在rt.jar包,是由启动类加载器进行类加载的。
在JDBC中需要动态去加载不同数据库类型的Driver实现类,而mysql-connector.jar中的Driver实现类是用户自己写的代码,启动类加载器肯定是不能加载的,那就需要由应用程序启动类去进行类加载。
为了解决这个问题,也可以使用线程上下文类加载器
(Thread Context ClassLoader)。
1.4、如何自定义一个类加载器?
自定义类加载器是Java中一个高级特性,通常用于实现特定的类加载需求,例如动态加载类、热部署等。下面将介绍如何创建一个自定义类加载器,并详细解释其工作机制。
创建自定义类加载器
自定义类加载器需要继承java.lang.ClassLoader
类,并重写其findClass
方法。以下是一个基本的自定义类加载器示例:
示例代码
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class CustomClassLoader extends ClassLoader {
private String classPath;
/**
* 构造函数,传入类加载路径
*
* @param classPath 类文件的加载路径
*/
public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
/**
* 重写父类的findClass方法
*
* @param name 类的全限定名
* @return 返回加载的类
* @throws ClassNotFoundException 如果类未找到
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
// 定义类,返回Class对象
return defineClass(name, classData, 0, classData.length);
}
/**
* 读取类文件,并将其转换为字节数组
*
* @param name 类的全限定名
* @return 返回类文件的字节数组
*/
private byte[] loadClassData(String name) {
String fileName = classPath + name.replace('.', '/') + ".class";
try (InputStream inputStream = new FileInputStream(fileName);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length;
while ((length = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, length);
}
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public static void main(String[] args) {
try {
CustomClassLoader customClassLoader = new CustomClassLoader("/path/to/classes/");
// 加载类
Class<?> clazz = customClassLoader.loadClass("com.example.MyClass");
// 创建实例
Object instance = clazz.newInstance();
System.out.println("Class loaded: " + clazz.getName());
System.out.println("Instance created: " + instance);
} catch (Exception e) {
e.printStackTrace();
}
}
}
详细解释
-
类加载器的构造方法:
CustomClassLoader(String classPath)
:接受类文件的路径作为参数,初始化类加载器。
-
重写
**findClass**
方法:findClass(String name)
:根据类的全限定名加载类。先调用loadClassData
方法读取类文件数据,然后使用defineClass
方法将字节数组转换为Class
对象。
-
读取类文件:
loadClassData(String name)
:根据类的全限定名构建类文件的路径,读取类文件并转换为字节数组。
-
主方法
**main**
:- 创建
CustomClassLoader
实例,调用loadClass
方法加载类,并创建类的实例。
- 创建
使用自定义类加载器的场景
-
热部署:
- 在应用服务器中,允许在不重启服务器的情况下更新应用程序代码。
-
插件机制:
- 加载和卸载插件,实现应用程序的动态扩展。
-
类隔离:
- 在同一个JVM中运行多个版本的同一个库,避免类冲突。
-
字节码生成:
- 动态生成和加载字节码,例如在一些框架中生成代理类。
注意事项
-
委派机制:
- 虽然自定义类加载器可以自定义类加载逻辑,但通常还是要遵循双亲委派模型,优先调用父类加载器加载类,只有在父类加载器无法加载时,才调用自定义的类加载逻辑。
-
安全性:
- 自定义类加载器需要注意安全问题,避免加载未授权的类。
-
性能:
- 类加载是一个开销较大的操作,应避免频繁加载和卸载类。
通过自定义类加载器,可以实现Java应用的灵活扩展和动态管理,满足特定的应用需求。