从JDBC看“破坏”双亲委派模型

去年6月第一遍看《深入理解java虚拟机》的时候,整体看的是都是很快的,很多东西就算看不懂没想明白也没有细究,然后这个月第二次看这本书的时候,有些问题看起来就一下子引起了兴趣。
7.4的类加载器就很有意义。

老生常谈--类与类加载器

类加载器用于实现类的加载动作,但是起的作用可不是加载完对后续没什么影响了。

  • 对于任意一个类,都需要由加载它的类加载器和这个类本身来一同确立其在Java虚拟机中的唯一性。
  • 比较两个类是否相等,只有两个类在同一个类加载器加载的情况下,才有意义;换句话说,就是就算来自一个Class文件的两个类,被同一个jvm加载,只要用的类加载器不同,那就不同。

复制书中的代码:

package jvm;

import java.io.IOException;
import java.io.InputStream;

/**unclewang
 * @Author 
 * @Date 2019-01-11 10:40
 */
public class ClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        ClassLoader classLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
        Object object = classLoader.loadClass("jvm.ClassLoaderTest").newInstance();
        System.out.println(object.getClass());
        System.out.println(new ClassLoaderTest().getClass());
        System.out.println(object instanceof jvm.ClassLoaderTest);
    }
}

运行结果:

虽然两个来自一个Class文件,但是object是用的自定义的classLoader,而实际上jvm.ClassLoaderTest这个Class是被系统默认的AppClassLoader加载的,所以根本就是两个独立的类,打印false。

双亲委派模型


上面这张图先镇着,其实这幅图有点问题,但是现在可以先不计较那么多的理解。

双亲委派:如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈
给子类加载器,子类加载器会尝试去自己加载。
书里面有一句话开始没注意,后来看到代码豁然开朗。“这里类加载器之家的父子关系一般不会以继承的关系来实现,而是都使用组合的关系来复用父加载器的代码”。这个在下面的几张截图里就可以看出来,他们都在Launcher类以静态内部类的方式去实现,都继承了URLClassLoader类,然后互相组合,很多native方法应该就是了。

上面说的东西有几个要注意:每次都是有下面的子类先去加载,子类加载的过程就是让父类先加载,父类是规定动作,每次就是加载某个地方的类,父类加载到就结束了,没加载到就子类开始加载。好处就是:等级森严,比如通常我们用的Object都来自rt.jar包里的java.lang这个包里面,如果你自己像下面这样新建了java.lang这个package,可以正常编译,但无法加载运行。从而保证了java程序不会混乱,虚拟机执行会有条不紊的运作。

package java.lang;

public class A extends java.lang.Object {
    public static void main(String[] args) {
        Object object = new Object();
    }
}

会出现下图这样的异常:

先说类加载器,书上说的比较简单,没有结合源代码,下面结合一下源代码来看:

  1. 从java虚拟机的角度,只存在两种不同的类加载器:

    1. 一种是启动类加载器,Bootstrap ClassLoader,用C++实现的,可以在ClassLoader这个抽象类找到确实是native的方法;
    2. 一种是其他的类加载器,通用的抽象类是ClassLoader,除了上面那个Bootstrap ClassLoader,其他类加载器,都是要继承这个ClassLoader类加载,所以都是用java语言实现。在这个类的注释里,大概有100多行就是在将类的加载机制说了一遍,基本这本书就是周老师带上自己的理解翻译了一遍。部分图:
  2. 从开发者来看,类加载器分为系统提供的3种类加载器和用户自定义的类加载器。

    • 启动类加载器
      负责将存放在$JAVA_HOME\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib中也不会被加载)类库加载到虚拟机内存中,不知道是1.8还是mac,放在了在\jre\lib里面,同时你可以运行 System.getProperty("sun.boot.class.path") 去找出来目录在哪。

    • 扩展类加载器
      扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,继承了URLClassLoader,它负责加载JAVA_HOME\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

    • 应用程序类加载器
      应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(classPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认类加载器。

最后可以根据下面的代码验证一下:

双亲委派模型的代码分析

其实loadClass方法被递归了一下,parent不等于null就调用parent.loadClass(name, false),都没找到,自己findClass一下。

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 {
                        //这里说明了,ExtClassLoader的父类其实为空,事实上也是如此,那张ClassLoader的结构图只是想说明结构,并不牵扯具体的代码
                        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();
                    //这里就是破坏双亲委派模型的第一次破坏妥协的解决方案,即不直接覆盖loadClass的方法,而是在这个findClass方法里面去写,比如URLClassLoader就是这么做的,好处就在于即兼容了原来双亲委派没出现的写法,又没有破坏双亲委派
                    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;
        }
    }

正题--第二次破坏双亲委派模型

上面在代码里面已经说了第一次破坏的结果了.
现在来说第二次破坏,先说一下原因,第二次破坏不像第一次破坏那样为了想让别人接受新的,你不能太猛,直接不支持旧的了;也不像第三次破坏是程序员想的有点多,追求动态性,希望一个类在系统之间都能保持一致;
第二次破坏是一个天生必须的弊端,和好处就是一体两面的,一般的基础类就是我们要用的,它自己本身不借助孩子的东西就可以完美运行,保证越基础的类越上层的加载器进行加载。但是有没有特殊的基础类呢,有,甚至就是个接口,根本不能自己运行,必须要借助下面加载器的东西。这种情况一般是SPI(Service Provider Interface)。
现在来说一下数据库方面的SPI,rt.jar里面有一个java.sql这个package,专门负责运作各种sql数据系统的东西,比如DriverManager和Driver就是来负责连接某种数据库,但是问题来了,mysql的driver和oracle的driver根本不一样,这个都是用户自己最后决定的,你在BootStrapClassLoader里面加载具体的driver就是有这个心也是不可能做到的,但你如果不支持这些数据库,java估计都没人用了,所以就有了下面要说的第二次破坏。

破坏,也得有个方案呀

“破坏”这个词得好好理解一下,在这个地方思考了很久。破坏的反义词是维持,就是试想一下如果还是维持双亲委派的话,事情是什么样的?
数据库管理,java本身就应该支持,那就都应该用BootStrapClassLoader加载DriverManager和你本身要用的Driver,但是BootStrapClassLoader真是“臣妾做不到”呀,我就是个语言,我又不是做数据库,你们各有各的数据库,我还要都支持你们,还不如我定义一套JDBC规范,你们都听我的支持我这个规范,我也退一步,我在实现DriverManager建立连接加载Driver的代码的时候,不非得BootStrapClassLoader里加载(其实应该在这里面加载),DriverManager的代码写成你在自己的线程所在的ClassLoader里加载,这样不就加载到了。
换句话说,虽然该我加载,但我加载不了,那就商量一下代码还是Java设计者写,你们这些数据库公司都支持我的JDBC规范,就解决,这个妥协的方案实际上违背了双亲委派模型从上到下先找的思想,BootStrap直接跟你说,我找不了,你们直接自己找吧。
这个妥协的方案就是线程上下文类加载器(Thread Context ClassLoader),这个下面在讲。
于是JAVA和SQL提供商(SPI)合作实现了JDBC的各种服务。
感觉说的很透彻了。

线程上下文类加载器思想(Thread Context ClassLoader)

这个感觉就是java自己给自己留的后手,有一种“水深则无鱼”的感觉。
虽然我希望你们严格遵守双亲委派,但现实是复杂的,要想这个潭子里鱼多,你就不能管那么严格,那这个线程上下文类加载器就保证了你的自定义能力,或者说成留点空间给java开发人员。
这是一个java.lang.Thread里面实现的,里面有关于ContextClassLoader的一组get/set方法。


这里面也存在很多native方法,所以我们可以用System.out.println(Thread.currentThread().getContextClassLoader());验证一下,确实是书里面说的AppClassLoader。

正式看DriverManager代码

先看一下我们平常是怎么使用DriverManager的?

    public Connection connection(String url, String user, String password) throws SQLException {
//        try {
//            //调用Class.forName()方法加载驱动程序
//            Class.forName("com.mysql.jdbc.Driver");
//            System.out.println("成功加载MySQL驱动!");
//        } catch (ClassNotFoundException e) {
//            System.out.println("找不到MySQL驱动!");
//            e.printStackTrace();
//        }

        //调用DriverManager对象的getConnection()方法,获得一个Connection对象
        Connection conn = DriverManager.getConnection(url, user, password);
        return conn;
    }

先说Class.forName(),首先上面这段代码是不影响正常使用的。这是因为从Java1.6开始自带的jdbc4.0版本已支持SPI服务加载机制,只要mysql的jar包在类路径中,就可以注册mysql驱动。

带着问题看代码

第一个问题:显式的注册了,是怎么个流程?

利用debug,一点一点看。
首先进去,caller是当前的我写的Class.forName()的那个类

然后下面的forName0()方法里ClassLoader.getClassLoader(caller)是AppClassLoader,这个可以进去看,就是我写的那个类,肯定返回的系统默认的AppClassLoader呀。

在回到forName0()方法,forName0是一个native方法

继续点击下一步,会进入Launcher里的AppClassLoader里的loadClass方法,这个方法里会在AppClassLoader加载了各种的maven包的ucp里去找有没有这个类,因为这个类是可以找到的,所以就顺利的去super.loadClass()了


然后就调用ClassLoader的loadClass方法,就可以对com.mysql.jdbc.Driver的相关各种类进行加载,当加载到Driver这个类的时候,会调用一个静态块

当你使用DriverManager的时候,其内部又有一个静态块需要执行,这个是用来加载System.getProperty("jdbc.drivers");,我试过里面为空,然后利用ServiceLoader机制去加载

具体这个ServiceLoader机制其实就是第二个问题的答案,不过先跳过这个继续往下看。DriverManager加载了以后,才可以真正的用registerDriver方法了
在mysql-connector-java-6.x以后,他继承了com.mysql.cj.jdbc.Driver,所以在这个里注册了

com.mysql.jdbc.Driver 没有注册,打印了相关信息。

当这一步结束以后,点击下一步,我们又回到了最开始的forName0方法,然后整个过程就结束了,看来我们可以倒推出来forName0方法干了非常多的事情。

第二个问题:不显式注册,在哪注册的?

上面埋了一个伏笔,我们说ServiceLoader机制就是这个问题的答案。
如果不显式注册,那么代码就直接是这样的

    Connection conn = DriverManager.getConnection(url, user, password);
    return conn;

那么就是直接来静态块的初始化,点进去就是loadInitialDrivers();这个方法
这个方法主要做两个事情,一个是读取 System.getProperty("jdbc.drivers");,这个没有设置的情况下就是空的,另一个是重点,调用ServiceLoader.load(Driver.class);

进到ServiceLoader.load(Class)的代码:

先看第一行,这时候就确定了就是要用ContextClassLoader,这个书上说是AppClassLoader,从哪验证呢?Launcher的构造方法。

这下就验证了确实是AppClassLoader了。
再看ServiceLoad的load的第二行,点进去,调用了构造方法。

再点进去

前面三行不看,直接看reload方法

再看LazyIterator这个类

就是去读前缀+“Driver”,前缀是“META-INF/services/“

然后就是读这里的所有的注册为Driver的东西,然后就使用Class.forName方法对每个Driver用AppClassLoader加载起来,过程和问题一说的过程一样,只不过不需要DriverManager再运行静态初始块了。

第三个问题:关于Thread Context ClassLoader是在哪个地方起作用的?

上两个问题都已经说了一定的作用了,上两个问题过后,DriverManager反正一定已经运行过静态初始化块了,那就直接来用getConnection()方法,然后调用一个内部的private的getConnection()方法

然后进去下面这个方法,感觉非常sb的一个实现,用那些Driver一个一个看报不报异常,不报异常就真的成功创建连接了。

参考

《深入理解java虚拟机》
https://blog.csdn.net/yangcheng33/article/details/52631940
https://www.cnblogs.com/joemsu/p/9310226.html

发表评论

电子邮件地址不会被公开。