违反双亲委派模型

在开讲双亲委派模型之前,我们先要了解一下类加载机制

类加载机制是指将类的class文件读入到内存,并为之创建一个java.lang.Class对象。中间对数据做了 校验,转换解析和初始化等操作。

一般情况下我们说了有三种加载器:

  • 最基础:Bootstrap ClassLoader(加载JDK的/lib目录下的类)
  • 次基础:Extension ClassLoader(加载JDK的/lib/ext目录下的类)
  • 普通:Application ClassLoader(程序自己classpath下的类)

图片来源于网络

双亲委派模型要求如果一个类可以被委派最基础的ClassLoader加载,就不能让高层的ClassLoader加载。

举个例子:我们实际中用到的一些类如:String,Object等,都是来自于jdk的lib下的rt.jar包,但是如果我们有工作需要在自己的项目中也创建相同名称的类,我们如何来区分呢,双亲委派就完美的解决了这个问题,BootStrap加载rt.jar 包,剩下的由它的子加载器下的子加载器来加载。当然这里的父子不是指的一般意义上的父类和子类。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派父类加载器去完成。每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个请求(他的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

在很多的时候面试官会问我们如何破坏双亲委派模型和为什么要破坏它

在实际的应用中双亲委派解决了java 基础类统一加载的问题,但是却着实存在着一定的缺席。jdk中的基础类作为用户典型的api被调用,但是也存在被api调用用户的代码的情况,典型的如SPI代码。

SPI机制简介 SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

破坏双亲委派模型的情况

在JDBC4.0以后,开始支持使用spi的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver是哪个,然后使用的时候就直接这样就可以了

 Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/test?characterEncoding=GBK", "root", "");

可以看到这里直接获取连接,省去了上面的Class.forName()注册过程。 现在,我们分析下看使用了这种spi服务的模式原本的过程是怎样的:

  1. 从META-INF/services/java.sql.Driver文件中获取具体的实现类名“com.mysql.cj.jdbc.Driver”
  2. 加载这个类,用class.forName(“com.mysql.jdbc.Driver”)来加载

Class.forName()加载用的是调用者的Classloader, 这个调用者DriverManager是在rt.jar中的,ClassLoader是启动类加载器,而com.mysql.jdbc.Driver肯定不在/lib下,所以肯定是无法加载mysql中的这个类的。这就是双亲委派模型的局限性了,父级加载器无法加载子级类加载器路径中的类。

如何解决父加载器无法加载子级类加载器路径中的类

我们分析一下,想要正常的加载,启动类加载器肯定不能加载,那么只能用应用类加载器能够加载,那么如果有什么办法能够获取到应用类加载器就可以解决问题了;我们看看 jdk是怎么做的;

线程上下文类加载器

public class DriverManager {
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    private static void loadInitialDrivers() {
        //省略代码
        //这里就是查找各个sql厂商在自己的jar包中通过spi注册的驱动
        ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
        Iterator<Driver> driversIterator = loadedDrivers.iterator();
        try{
             while(driversIterator.hasNext()) {
                driversIterator.next();
             }
        } catch(Throwable t) {
                // Do nothing
        }

        //省略代码
    }
}

看这里,加载的时候去获取了一个加载器

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

获取线程上下文类加载器Thread.currentThread().getContextClassLoader(); 这个值如果没有特定设置,一般默认使用的是应用程序类加载器;

 

所以jdk开发人员就引入了线程上下文类加载器(Thread Context ClassLoader),这类类加载器可以通过java.lang.Thread 类的setContextClassLoader方法进行设置。

其实在jdbc的使用中,我们很好的体会到它的作用,我们平时看到的mysql的加载是这个样子的:

在以上的代码中就实现了注册mysql驱动和获取数据库连接。

以上的代码是DriverManager 的初始化方法loadInitialDrivers,大家可以从中看到先是获取jdbc.drivers属性,得到类的路径。然后通过系统类加载器加载。

注意driversIterator.next()最终就是调用Class.forName(DriverName, false, loader)方法,也就是最开始我们在第一张图中注释掉的那一句代码。

对于自己加载不了的类怎么办,直接用线程上下类加载器加载,通过

ClassLoader cl = Thread.currentThread().getContextClassLoader();

这条语句获取本地线程然后实现上下类加载。牛逼了,所以这个地方Bootstrap Classloader加载器拿到了Application ClassLoader加载器应该加载的类,就打破了双亲委派模型

posted on 2022-04-16 20:04  myf008  阅读(83)  评论(0编辑  收藏  举报

导航