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

Posted SunAlwaysOnline

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深度思考:老生常谈的双亲委派机制,JDBCTomcat是怎么反其道而行之的?相关的知识,希望对你有一定的参考价值。

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

一、类加载器的类型

类加载器有以下种类:

  • 启动类加载器(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,说明当前类加载器已经是启动类加载器,直接时候用启动类加载器去加载该class
                        c = 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;
    }

    @Override
    protected 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/*.jar

server.loader=${catalina.home}/server/classes,${catalina.home}/server/lib/*.jar

shared.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 classloader
        if (!started) {
            try {
                throw new IllegalStateException();
            } catch (IllegalStateException e) {
                log.info(sm.getString("webappClassLoader.stopped", name), e);
            }
        }

        //1、从自己的本地缓存中查找,本地缓存的数据结构为ResourceEntry
        clazz = 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会先于父加载器加载。

以上是关于深度思考:老生常谈的双亲委派机制,JDBCTomcat是怎么反其道而行之的?的主要内容,如果未能解决你的问题,请参考以下文章

双亲委派机制JVM:类加载机制深度剖析 - 第7篇

关于Java类加载双亲委派机制的思考(附一道面试题)

Java技术专题深度分析加载器与双亲委派机制「 入门篇」

双亲委派模型,类的加载机制,搞定大厂高频面试题

从源码理解双亲委派机制,原来如此简单

[五]类加载机制双亲委派机制 底层代码实现原理 源码分析 java类加载双亲委派机制是如何实现的