我学Java(1)——ClassLoader与双亲委托模式以及「SPI」

一、ClassLoader分类

Java虚拟机会建立三类ClassLoader,分别以下html

名称 加载 加载路径 父加载器 实现
BootStrap 虚拟机的核心类库 sun.boot.class.path 系统
Extension 扩展类库 java.ext.dirs、jre/lib/ext BootStrap Java
System 应用类库 classpath、java.class.path Extension Java

注:父子加载器并不是继承关系,也就是说子加载器不必定是继承了父加载器
java

二、双亲委托模式

其实我以为把「双亲委托模式」称为「父加载委托模式」更好理解,「双」字把我给弄混了。mysql

「双亲委托模式」指的就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,若是父类加载器能够完成类加载任务,就成功返回;只有父类加载器没法完成此加载任务时,本身才去加载。android



下面是一段ClassLoader的源码,很容易能够看出上述规则:sql

protected synchronized Class loadClass(String name, boolean resolve) 
    throws ClassNotFoundException{  
     // 首先检查该name指定的class是否有被加载  
     Class c = findLoadedClass(name);  
     if (c == null) {  
         try {  
             if (parent != null) {  
                 //若是parent不为null,则调用parent的loadClass进行加载  
                 c = parent.loadClass(name, false);  
             }else{  
                 //parent为null,则调用BootstrapClassLoader进行加载  
                 c = findBootstrapClass0(name);  
             }  
         }catch(ClassNotFoundException e) {  
             //若是仍然没法加载成功,则调用自身的findClass进行加载              
             c = findClass(name);  
         }  
     }  
     if (resolve) {  
         resolveClass(c);  
     }  
     return c;  
}

(1)优势

  • 避免类库重复加载
  • 安全,将核心类库与用户类库隔离,用户不能经过加载器替换核心类库,如String类。

(2)弊端

委托永远是子加载器去请求父加载器,是单向的,即上层的类加载器没法访问下层的类加载器所加载的类数据库



下层类对于上层类是不可见的

举个例子,假设「BootStrap」中提供了一个接口,及一个建立其实例的工厂方法,可是该接口的实现类在「System」中,那么就会出现工厂方法没法建立在「System」加载的类的实例的问题。拥有这样问题的组件有不少,好比JDBC、Xml parser等。安全

三、如何解决弊端——使用「SPI」

如今引入一个新的名词「SPI」。框架

「SPI」 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。 目前有很多框架用它来作服务的扩展发现, 简单来讲,它就是一种动态替换发现的机制。ide

JDBC自己是Java链接数据库的一个标准,是进行数据库链接的抽象层,由Java编写的一组类和接口组成,接口的实现由各个数据库厂商来完成,不一样厂商能够针对同一接口作出不一样的实现,MySQL和PostgreSQL都有不一样的实现提供给用户,而Java的「SPI」机制能够为某个接口寻找服务实现。函数

三、JDBC举例

下面以JDBC为例,介绍「SPI」机制。

在JDBC4.0以前,咱们开发有链接数据库的时候,一般会用Class.forName("com.mysql.jdbc.Driver")这句先加载数据库相关的驱动,而后再进行获取链接等的操做。而JDBC4.0以后不须要用Class.forName("com.mysql.jdbc.Driver")来加载驱动,直接获取链接就能够了,如今这种方式就是使用了Java的「SPI」扩展机制来实现。

(1)接口定义

JDBC在java.sql.Driver只定义了接口。



JDBC中定义的接口

(2)厂商实现

这里以MySQL为例,在mysql-connector-java-6.0.6.jar包里的META-INF/services目录下能够找到一个java.sql.Driver文件,文件内容是一个类名,这个名叫com.mysql.cj.jdbc.Driver的类就是MySQL针对JDBC中定义的接口的实现。




MySQL对JDBC中定义的接口的实现类

(3)如何使用

在咱们的应用里面,咱们就能够直接链接MySQL了。

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

显然语句并无加载实现类,这里就涉及到使用「SPI」扩展机制来查找相关驱动了,接下来,咱们结合源码探究一下这是如何实现的。

四、源码解析

关于驱动的查找其实都在DriverManager中,DriverManager位于java.sql包里,用来获取数据库链接,在DriverManager中有一个静态代码块以下:



静态加载

loadInitialDrivers方法用于实例化驱动,由3部分构成:

(1)获取有关驱动的名称



drivers保存驱动的定义

(2)加载并实例化驱动




两个比较关键的地方是ServiceLoader.load, 还有loadedDrivers.iterator,下面结合源码介绍一下:

(A)ServiceLoader.load

ServiceLoader封装了一个自定义加载器loader,还应留意一下下面2个成员,以后会用到:

  • 默认接口路径:PREFIX
  • 实现类的加载迭代器:lookupIterator




ServiceLoader.load(Driver.class)最后会调用构造函数,返回ServiceLoader实例





获取应用层加载器——SystemClassLoader

每个线程都有本身的ContextClassLoader,默认以SystemClassLoader为ContextClassLoader。经过Thread.currentThread().getContextClassLoader(),能够把一个ClassLoader置于一个线程的实例之中,使该ClassLoader成为一个相对共享的实例,这样即便是启动类加载器中的代码也能够经过这种方式访问应用类加载器中的类了。



多个加载器经过上下文加载器共享

(B)loadedDrivers.iterator

loadedDrivers.iterator方法返回一个迭代器,这个迭代器是「SPI」机制加载实现类的关键,迭代器在iterator()方法内定义:



Iterator的定义,关注hasNext和next方法

「SPI」加载代码的是这样的:



经过一个迭代来加载实现类

执行driversIterator.hasNext时,会调用lookupIterator.hasNext去找的实现类的名字。



driversIterator.hasNext方法

lookupIterator.hasNext方法

lookupIterator.hasNext方法,根据全路径名找实现类的名字

接着会调用lookupIterator.next()去加载这个类:



driversIterator.next方法

lookupIterator.next方法

加载实现类

至此,已经将实现类成功加载。

(3)加载驱动

如今就能够根据第1步获取到的驱动列表来加载实现类了:



调用Class.forName加载类

五、「SPI」的弊端

「SPI」经过循环加载实现类,显而易见,它会把全部的类一同加载,不管有没有用到,这形成了必定的资源浪费:



参考连接

android classloader双亲委托模式
dubbo源码解析-spi(一)
Java中SPI机制深刻及源码解析
走出ClassLoader的迷宫

相关文章
相关标签/搜索