深度思考:老生常谈的双亲委派机制,JDBC、Tomcat是怎么反其道而行之的?

本文主要是介绍深度思考:老生常谈的双亲委派机制,JDBC、Tomcat是怎么反其道而行之的?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

要说双亲委派机制,还得从类加载器的类型谈起

一、类加载器的类型

类加载器有以下种类:

  • 启动类加载器(Bootstrap ClassLoader)
  • 扩展类加载器(Extension ClassLoader)
  • 应用类加载器(Application ClassLoader)

启动类加载器

内嵌在JVM内核中的加载器,由C++语言编写(因此也不会继承ClassLoader),是类加载器层次中最顶层的加载器。用于加载java的核心类库,即加载jre/lib/rt.jar里所有的class。由于启动类加载器涉及到虚拟机本地实现细节,我们无法获取启动类加载器的引用。

扩展类加载器

它负责加载JRE的扩展目录,jre/lib/ext或者由java.ext.dirs系统属性指定的目录中jar包的类。父类加载器为启动类加载器,但使用扩展类加载器调用getParent依然为null。

应用类加载器

又称系统类加载器,可用通过 java.lang.ClassLoader.getSystemClassLoader()方法获得此类加载器的实例,系统类加载器也因此得名。应用类加载器主要加载classpath下的class,即用户自己编写的应用编译得来的class,调用getParent返回扩展类加载器。

扩展类加载器与应用类加载器继承结构如图所示:

可以看到除了启动类加载器,其余的两个类加载器都继承于ClassLoader,我们自定义的类加载器,也需要继承ClassLoader。

值得注意的是,启动类、扩展类与应用类加载器之间的父子关系,并不是通过继承来实现的,而是通过组合,即使用parent变量来保存“父加载器”的引用。


二、双亲委派机制

当一个类加载器收到了一个类加载请求时,它自己不会先去尝试加载这个类,而是把这个请求转交给父类加载器,每一个层的类加载器都是如此,因此所有的类加载请求都应该传递到最顶层的启动类加载器中。只有当父类加载器在自己的加载范围内没有搜寻到该类时,并向子类反馈自己无法加载后,子类加载器才会尝试自己去加载。

加载标准类库与用户代码,会有不同的方式:

 ClassLoader内的loadClass方法,就很好的解释了双亲委派的加载过程:

    protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {//检查该class是否已经被当前类加载器加载过Class<?> c = findLoadedClass(name);if (c == null) {//此时该class还没有被加载try {if (parent != null) {//如果父加载器不为null,则委托给父类加载c = parent.loadClass(name, false);} else {//如果父加载器为null,说明当前类加载器已经是启动类加载器,直接时候用启动类加载器去加载该classc = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {}if (c == null) {//此时父类加载器都无法加载该class,则使用当前类加载器进行加载long t1 = System.nanoTime();c = findClass(name);...}}//是否需要连接该类if (resolve) {resolveClass(c);}return c;}}

三、双亲委派存在的意义

为什么要使用双亲委派机制呢?

假设用户自己定义了java.lang.Object类,由于双亲委派机制的存在,最终会委托到启动类加载器去加载,即返回rt.jar中的Object类,并不会加载用户编写的Object类。

大家上班摸鱼刷的LeetCode,本质上自定义了一个类加载器,重写了findClass方法,会从网络中加载字节码,生成Class对象,最终通过loadClass定义的双亲委派机制进行加载。如果这个时候,我定义了一个恶意java.lang.Object类,在没有双亲委派机制的情况下,可能会对jvm产生安全风险。

双亲委派机制存在的意义,就是为了防止findClass与defineclass生成的Class对象覆盖掉标准类库中的基础类,避免产生安全风险。


四、如何自定义类加载器

我们整理ClassLoader里面的流程

  1. loadclass:双亲委派机制,子加载器委托父加载器加载,父加载器都加载失败时,子加载器通过findclass自行加载
  2. findclass:当前类加载器根据路径以及class文件名称加载字节码,从class文件中读取字节数组,然后使用defineClass
  3. defineclass:根据字节数组,返回Class对象

我们在ClassLoader里面找到findClass方法,发现该方法直接抛出异常,应该是留给子类实现的。

    protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);}

到这里,我们应该明白,loadClass方法使用了模版方法模式,主线逻辑是双亲委派,但如何将class文件转化为Class对象的步骤,已经交由子类去实现。对模版方法模式不熟悉的同学,可以先参考我的另外一篇文章模版方法模式

其实源码中,已经有一个自定义类加载的样例代码,在注释中:

      class NetworkClassLoader extends ClassLoader {String host;int port;public Class findClass(String name) {byte[] b = loadClassData(name);return defineClass(name, b, 0, b.length);}private byte[] loadClassData(String name) {// load the class data from the connection}}

看得出来,如果我们需要自定义类加载器,只需要继承ClassLoader,并且重写findClass方法即可。

现在有一个简单的样例,class文件依然在文件目录中:

package com.yang.testClassLoader;import sun.misc.Launcher;import java.io.*;public class MyClassLoader extends ClassLoader {/*** 类加载路径,不包含文件名*/private String path;public MyClassLoader(String path) {super();this.path = path;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {byte[] bytes = getBytesFromClass(name);assert bytes != null;//读取字节数组,转化为Class对象return defineClass(name, bytes, 0, bytes.length);}//读取class文件,转化为字节数组private byte[] getBytesFromClass(String name) {String absolutePath = path + "/" + name + ".class";FileInputStream fis = null;ByteArrayOutputStream bos = null;try {fis = new FileInputStream(new File(absolutePath));bos = new ByteArrayOutputStream();byte[] temp = new byte[1024];int len;while ((len = fis.read(temp)) != -1) {bos.write(temp, 0, len);}return bos.toByteArray();} catch (IOException e) {e.printStackTrace();} finally {if (null != fis) {try {fis.close();} catch (IOException e) {e.printStackTrace();}}if (null != bos) {try {bos.close();} catch (IOException e) {e.printStackTrace();}}}return null;}public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {MyClassLoader classLoader = new MyClassLoader("C://develop");Class test = classLoader.loadClass("Student");test.newInstance();}
}

Student类:

public class Student {public Student() {System.out.println("student classloader is" + this.getClass().getClassLoader().toString());}
}

注意,这个Student类千万不要加包名,idea报错不管他即可,然后使用javac Student.java编译该类,将生成的class文件复制到c://develop下即可。

运行MyClassLoader的main方法后,可以看到输出:

看得出来,Student.class确实是被我们自定义的类加载器给加载了。


五、双亲委派机制能被破坏吗

从上面的自定义类加载器的内容中,我们应该可以猜到了,破坏双亲委派直接重写loadClass方法就完事了。事实上,我们确实可以重写loadClass方法,毕竟这个方法没有被final修饰。双亲委派既然有好处,为什么jdk对loadClass开放重写呢?这要从双亲委派引入的时间来看:

双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,jdk为了向前兼容,不得已开放对loadClass的重写操作。

当然,破坏也不止这一次,jdbc与tomcat也破坏了双亲委派。


六、JDBC对双亲委派的破坏

还记得,我们第一次学jdbc的时候,是怎么连接数据库的吗?

先引用一个mysql-connector-java的jar包,这里的版本是5.0.8

        <dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.0.8</version></dependency>
        Class.forName("com.mysql.jdbc.Driver");Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");

这段代码,真的是勾起了我好多回忆啊~   想起了那年在夕阳下奔跑的时光,那是我逝去的青春

首先要说明的是,该版本的jdbc并没有去打破双亲委派,或者说jdbc4.0前没有破坏双亲委派

数据库这么多,jdk为了统一管理数据库驱动,在java.sql下定义了Driver接口,具体的实现由数据库厂商去做。

mysql对Driver接口的实现类是com.mysql.jdbc.Driver类,位于我们新引入的jar包中。

我们进入Class.forName中,发现最终会使用应用类加载器去加载com.mysql.jdbc.Driver类。

而该Driver位于引入的jar包中,确实是应该被应用类加载器加载。

 接着进入到com.mysql.jdbc包下的Driver类中,它实现了rt.jar中的java.sql.Driver接口。

 Class.forName会初始化该类,初始化的时候会执行静态方法。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {public Driver() throws SQLException {}static {try {//将mysql的Driver注册进驱动管理器中DriverManager.registerDriver(new Driver());} catch (SQLException var1) {throw new RuntimeException("Can't register driver!");}}
}

所以,整个过程是:

  1. Class.forName会使用应用类加载器加载Driver实现类
  2. 加载Driver实现类需要执行静态方法,即将mysql的Driver注册进驱动管理器中,那么此时需要加载DriverManager类
  3. 应用类加载器去加载DriverManager类,而DriverManager位于rt.jar中,便一直向上委托到启动类加载器完成加载

这个过程确实没有破坏双亲委派

那么jdbc4.0后的情况呢?

为了使用该特性,我们需要引入高版本的mysql-connector-java,这里引入的版本是5.1.8

此时完全可以抛弃第一行的Class.forName语句了,使用以下语句来进行实验

        Enumeration<Driver> en = DriverManager.getDrivers();while (en.hasMoreElements()) {java.sql.Driver driver = en.nextElement();System.out.println(driver);}

输出为:

 看来内存中已经存在mysql的Driver了,这到底是怎么做的呢?

应用类加载器逐层委托到启动类加载器去加载DriverManager时,会同时执行它的静态方法

    static {loadInitialDrivers();println("JDBC DriverManager initialized");}

loadInitialDrivers内部核心的代码这有这两句

    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();

看到ServiceLoader,大家想到了什么,这不是jdk spi机制的核心吗?

spi机制在我的这篇文章SpringBoot的自动装配原理、自定义starter与spi机制,一网打尽有详细的一个介绍,并且对比了SpringBoot与JDK中spi机制的异同。

既然使用到了spi机制,那么mysql-connector-java的jar包在META-INF目录下必然有services目录,内容如下。

 启动类加载DriverManager,之后需要通过spi机制去加载jar包中的Driver类,而该Driver理应被应用类加载器加载,这个时候就需要启动类加载器去通知应用类加载器,这明显违背了双亲委派机制

那么,启动类加载器是怎么去通知应用类加载器的呢?

我们继续进入到ServiceLoader.load方法中

    public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);}

Thread.currentThread().getContextClassLoader()是线程上下文类加载器,看来最终使用的是线程上下文类加载器去加载的Driver实现类。

而在sun.misc.Launcher类中,将应用类加载器设置进了线程上下文类加载器中,所以可以理解为,通过线程上下文类加载器,我们可以拿到应用类加载器的引用。

    public Launcher() {this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);Thread.currentThread().setContextClassLoader(this.loader);}

在jdbc4.0的情况下,梳理一下整个过程:

  1. 应用类加载器逐层委托到启动类加载器去加载DriverManager类
  2. 启动类加载器加载DriverManager类时,会执行其静态方法,即通过spi机制去加载jar包中的Driver实现类
  3. 此时启动类加载器需要委托应用类加载器加载Driver实现类,具体做法是通过线程上下文类加载器拿到应用类加载器的引用

确实是破坏了双亲委派!


七、Tomcat对双亲委派的破坏

tomcat有两个最基础的知识点,一个是应用打包放在webapps目录下就可以运行,另外一个是修改jsp会实时生效。

那这里抛出几个问题,来猜想一下Tomcat中类加载器的一个结构。

(1)jsp实时生效是怎么做的?

首先,在jvm中,如何去确定类的唯一性呢?是由类加载器实例+全限定名一起确定的。全限定名相同,类加载器不同,则会被认定为不同的类。

jsp文件被修改后,会被重新编译成Servlet,全限定名肯定是不变的,如果这个时候不去卸载加载该Servlet的类加载器,那么新jsp是无论如何都不会被加载进来的。因此,我们可以得知,每一个jsp文件都会对应一个类加载器实例

(2)每个webapps下的应用依赖的类库是否会互相影响?

显然是不会影响的。应用A依赖低版本的Spring,而应用B依赖高版本的Spring,都是允许的。虽然Spring的版本不同,但某些类的全限定名是完全一致的。如果应用A与应用B采用同一个类加载器,是不会允许Spring版本不一样的。这里,我们猜想webapps下的每一个应用都会对应一个不同的类加载器实例,用以保持应用间的隔离。

从以上的两个问题,我们可以了解到:每一个jsp(或者说servlet)都对应一个不同的类加载器实例,每个webapp应用也是。

其实,tomcat5版本(以下如果没有另外声明版本,那么都是以该版本为例)的类加载器结构为:

 其中各个加载器加载的范围为:

  • Common ClassLoader:主要加载common目录下的资源
  • Catalina ClassLoader:主要加载server目录下的资源
  • Shared ClassLoader:主要加载shared目录下的资源
  • Webapp ClassLoader:每一个应用会对应与该类型的一个实例,主要加载该应用下的WEB-INF下的资源
  • JasperLoader:每一个jsp文件会对应于该类型的一个实例,就是为了修改jsp能及时生效

(前三个类加载器在tomcat6中已经合并了,合并之后的加载器加载lib目录下的资源)

它们在Bootstrap类中有过声明:

    protected ClassLoader commonLoader = null;protected ClassLoader catalinaLoader = null;protected ClassLoader sharedLoader = null;private void initClassLoaders() {try {commonLoader = createClassLoader("common", null);if( commonLoader == null ) {// no config file, default to this loader - we might be in a 'single' env.commonLoader=this.getClass().getClassLoader();}catalinaLoader = createClassLoader("server", commonLoader);sharedLoader = createClassLoader("shared", commonLoader);} catch (Throwable t) {log.error("Class loader creation threw exception", t);System.exit(1);}}

其中createClassLoader方法会从container\catalina\src\conf\catalina.properties配置中读取每个加载器加载的范围:

common.loader=${catalina.home}/common/classes,${catalina.home}/common/i18n/*.jar,${catalina.home}/common/endorsed/*.jar,${catalina.home}/common/lib/*.jarserver.loader=${catalina.home}/server/classes,${catalina.home}/server/lib/*.jarshared.loader=${catalina.base}/shared/classes,${catalina.base}/shared/lib/*.jar

catalina.home是安装目录,catalina.base是每个tomcat实例的工作目录。在只用一个tomcat的情况下,两个目录是一样的。

在了解了加载器的类型与范围之后,那么tomcat到底是怎么打破双亲委派机制的呢?

前面说过,双亲委派机制被定义在ClassLoader中的loadClass方法中,如果某个自定义的类加载想要打破双亲委派,那么重新loadClass方法即可。

Tomcat中的WebappClassLoader就是自定义类加载器,它的loadClass方法为:
 

    public Class loadClass(String name) throws ClassNotFoundException {return (loadClass(name, false));}public Class loadClass(String name, boolean resolve)throws ClassNotFoundException {if (log.isDebugEnabled())log.debug("loadClass(" + name + ", " + resolve + ")");Class clazz = null;// Log access to stopped classloaderif (!started) {try {throw new IllegalStateException();} catch (IllegalStateException e) {log.info(sm.getString("webappClassLoader.stopped", name), e);}}//1、从自己的本地缓存中查找,本地缓存的数据结构为ResourceEntryclazz = findLoadedClass0(name);if (clazz != null) {if (log.isDebugEnabled())log.debug("  Returning class from cache");if (resolve)resolveClass(clazz);return (clazz);}//2、从jvm的缓存中查找clazz = findLoadedClass(name);if (clazz != null) {if (log.isDebugEnabled())log.debug("  Returning class from cache");if (resolve)resolveClass(clazz);return (clazz);}//3、如果缓存中都找不到,则利用系统类加载器加载try {clazz = system.loadClass(name);if (clazz != null) {if (resolve)resolveClass(clazz);return (clazz);}} catch (ClassNotFoundException e) {// Ignore}if (securityManager != null) {int i = name.lastIndexOf('.');if (i >= 0) {try {securityManager.checkPackageAccess(name.substring(0,i));} catch (SecurityException se) {String error = "Security Violation, attempt to use " +"Restricted Class: " + name;log.info(error, se);throw new ClassNotFoundException(error, se);}}}boolean delegateLoad = delegate || filter(name);//4、开启代理的话,则使用父加载器加载if (delegateLoad) {if (log.isDebugEnabled())log.debug("  Delegating to parent classloader1 " + parent);ClassLoader loader = parent;if (loader == null)loader = system;try {clazz = loader.loadClass(name);if (clazz != null) {if (log.isDebugEnabled())log.debug("  Loading class from parent");if (resolve)resolveClass(clazz);return (clazz);}} catch (ClassNotFoundException e) {;}}//5、自行加载if (log.isDebugEnabled())log.debug("  Searching local repositories");try {clazz = findClass(name);if (clazz != null) {if (log.isDebugEnabled())log.debug("  Loading class from local repository");if (resolve)resolveClass(clazz);return (clazz);}} catch (ClassNotFoundException e) {;}//如果自己也加载不了,那就只能让父加载器加载了if (!delegateLoad) {if (log.isDebugEnabled())log.debug("  Delegating to parent classloader at end: " + parent);ClassLoader loader = parent;if (loader == null)loader = system;try {clazz = loader.loadClass(name);if (clazz != null) {if (log.isDebugEnabled())log.debug("  Loading class from parent");if (resolve)resolveClass(clazz);return (clazz);}} catch (ClassNotFoundException e) {;}}throw new ClassNotFoundException(name);}

loadClass内部的逻辑整理如下:

  1. 先从WebappClassLoader的ResourceEntry缓存中查找
  2. 从jvm缓存中查找,比如去元数据区查找
  3. 利用系统类(应用类)加载器加载,避免webapp中的类覆盖掉标准类库中的类。
  4. 开启代理的话,则使用父加载器加载,这个默认没开启的。
  5. webappClassLoader自行去加载
  6. 自己也没加载成功的话,最后只能让父加载器去加载

这里有一个问题,对于一些非基础类库,为什么要先让webappClassLoader先去加载呢?

假设应用a依赖1.0版本的x.jar,而应用b依赖2.0版本的x.jar。为了保证两个应用的隔离性,首先要做的就是保证两个应用各自对应不同的webappClassLoader实例。如果这两个webappClassLoader实例在加载x.jar的时候,直接向上委托,那么最终只会加载一个版本的x.jar。

从上面,我们可以了解到:

对于一些标准类库中的类,比如Object类,会让系统类加载器加载,然后一直委托到启动类加载器,这个过程是没有违背双亲委派的

而对于webapp中独有的类,则是webappClassLoader自行去加载,加载失败才让父加载器加载,明显是违背双亲委派的。


八、总结

双亲委派机制,核心是子加载器委托父加载器,能够避免java核心类库被篡改,增加了安全性。

但发展会带来创新,创新就会带来变革,jdbc与tomcat打破了这个自古相传的机制。

在jdbc中,父加载器委托子加载器。即利用线程上下文类加载器,让启动类加载器得以委托应用类加载器,去加载jar中的数据库驱动。

在tomcat中,子加载器优先于父加载器加载。即为了实现各个webapp的隔离性,webappClassLoader会先于父加载器加载。

这篇关于深度思考:老生常谈的双亲委派机制,JDBC、Tomcat是怎么反其道而行之的?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/831485

相关文章

Java深度学习库DJL实现Python的NumPy方式

《Java深度学习库DJL实现Python的NumPy方式》本文介绍了DJL库的背景和基本功能,包括NDArray的创建、数学运算、数据获取和设置等,同时,还展示了如何使用NDArray进行数据预处理... 目录1 NDArray 的背景介绍1.1 架构2 JavaDJL使用2.1 安装DJL2.2 基本操

最长公共子序列问题的深度分析与Java实现方式

《最长公共子序列问题的深度分析与Java实现方式》本文详细介绍了最长公共子序列(LCS)问题,包括其概念、暴力解法、动态规划解法,并提供了Java代码实现,暴力解法虽然简单,但在大数据处理中效率较低,... 目录最长公共子序列问题概述问题理解与示例分析暴力解法思路与示例代码动态规划解法DP 表的构建与意义动

Spring排序机制之接口与注解的使用方法

《Spring排序机制之接口与注解的使用方法》本文介绍了Spring中多种排序机制,包括Ordered接口、PriorityOrdered接口、@Order注解和@Priority注解,提供了详细示例... 目录一、Spring 排序的需求场景二、Spring 中的排序机制1、Ordered 接口2、Pri

MySql死锁怎么排查的方法实现

《MySql死锁怎么排查的方法实现》本文主要介绍了MySql死锁怎么排查的方法实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录前言一、死锁排查方法1. 查看死锁日志方法 1:启用死锁日志输出方法 2:检查 mysql 错误

Tomcat高效部署与性能优化方式

《Tomcat高效部署与性能优化方式》本文介绍了如何高效部署Tomcat并进行性能优化,以确保Web应用的稳定运行和高效响应,高效部署包括环境准备、安装Tomcat、配置Tomcat、部署应用和启动T... 目录Tomcat高效部署与性能优化一、引言二、Tomcat高效部署三、Tomcat性能优化总结Tom

MySQL 缓存机制与架构解析(最新推荐)

《MySQL缓存机制与架构解析(最新推荐)》本文详细介绍了MySQL的缓存机制和整体架构,包括一级缓存(InnoDBBufferPool)和二级缓存(QueryCache),文章还探讨了SQL... 目录一、mysql缓存机制概述二、MySQL整体架构三、SQL查询执行全流程四、MySQL 8.0为何移除查

通过prometheus监控Tomcat运行状态的操作流程

《通过prometheus监控Tomcat运行状态的操作流程》文章介绍了如何安装和配置Tomcat,并使用Prometheus和TomcatExporter来监控Tomcat的运行状态,文章详细讲解了... 目录Tomcat安装配置以及prometheus监控Tomcat一. 安装并配置tomcat1、安装

Rsnapshot怎么用? 基于Rsync的强大Linux备份工具使用指南

《Rsnapshot怎么用?基于Rsync的强大Linux备份工具使用指南》Rsnapshot不仅可以备份本地文件,还能通过SSH备份远程文件,接下来详细介绍如何安装、配置和使用Rsnaps... Rsnapshot 是一款开源的文件系统快照工具。它结合了 Rsync 和 SSH 的能力,可以帮助你在 li

一文详解Java Condition的await和signal等待通知机制

《一文详解JavaCondition的await和signal等待通知机制》这篇文章主要为大家详细介绍了JavaCondition的await和signal等待通知机制的相关知识,文中的示例代码讲... 目录1. Condition的核心方法2. 使用场景与优势3. 使用流程与规范基本模板生产者-消费者示例

电脑密码怎么设置? 一文读懂电脑密码的详细指南

《电脑密码怎么设置?一文读懂电脑密码的详细指南》为了保护个人隐私和数据安全,设置电脑密码显得尤为重要,那么,如何在电脑上设置密码呢?详细请看下文介绍... 设置电脑密码是保护个人隐私、数据安全以及系统安全的重要措施,下面以Windows 11系统为例,跟大家分享一下设置电脑密码的具体办php法。Windo