双亲委派模型和Class类加载器

89 阅读6分钟

1、秒懂双亲委派机制

mp.weixin.qq.com/s/4e49mAqZB…

1.1、流程图

完整的流程图如下所示:

1.2、双亲委派机制的意义是什么?

1、避免一个类被重复的加载。重复会出现什么问题呢?

  1. 类的类型不匹配。即便两个类的名称和包名都相同,通过不同的类加载器加载仍然会被认为是两个不同类,在类转换时,可能会出现同名类不匹配的问题;

  2. 浪费内存;多个类加载器加载多次,所有的变量、常量和方法都会在占用一份内存;

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/classesWEB-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();
        }
    }
}

详细解释

  1. 类加载器的构造方法

    1. CustomClassLoader(String classPath):接受类文件的路径作为参数,初始化类加载器。
  2. 重写**findClass**方法

    1. findClass(String name):根据类的全限定名加载类。先调用loadClassData方法读取类文件数据,然后使用defineClass方法将字节数组转换为Class对象。
  3. 读取类文件

    1. loadClassData(String name):根据类的全限定名构建类文件的路径,读取类文件并转换为字节数组。
  4. 主方法**main**

    1. 创建CustomClassLoader实例,调用loadClass方法加载类,并创建类的实例。

使用自定义类加载器的场景

  1. 热部署

    1. 在应用服务器中,允许在不重启服务器的情况下更新应用程序代码。
  2. 插件机制

    1. 加载和卸载插件,实现应用程序的动态扩展。
  3. 类隔离

    1. 在同一个JVM中运行多个版本的同一个库,避免类冲突。
  4. 字节码生成

    1. 动态生成和加载字节码,例如在一些框架中生成代理类。

注意事项

  1. 委派机制

    1. 虽然自定义类加载器可以自定义类加载逻辑,但通常还是要遵循双亲委派模型,优先调用父类加载器加载类,只有在父类加载器无法加载时,才调用自定义的类加载逻辑。
  2. 安全性

    1. 自定义类加载器需要注意安全问题,避免加载未授权的类。
  3. 性能

    1. 类加载是一个开销较大的操作,应避免频繁加载和卸载类。

通过自定义类加载器,可以实现Java应用的灵活扩展和动态管理,满足特定的应用需求。