采用JDBC解释java SPI机制和线程上下文类加载器——————开开开山怪

折月煮酒 提交于 2020-02-09 14:05:52

采用JDBC解释JAVA SPI机制和线程上下文类加载器

SPI(Service Provider Interface)网上有关于SPI的解释,在这里我简单总结一下。

SPI机制可以做到将服务接口和真正的服务接口的实现类分开,可以增加程序的可扩展性,通过扫描规定的路径来进行实现类的获取,可以说是一种服务发现机制。

优点: 在面向对象的设计中,我们一般建议基于接口的编程,如果代码中涉及到具体的实现类,如果我们想要换一种实现方案就不得不更改代码,但是采用接口的方式,只要我们采用一种机制,可以使得我们能够获取接口的不同的实现类,那么我们的代码的灵活性就比较的高,这种机制就是SPI。

SPI机制的工作过程

当服务提供者提供了服务接口的实现类,当实现类打成jar包之后,在jar包的META-INF/services/ 建立一个以服务接口名称为文件名称的文件,并且文件的内容为该服务接口的实现类的名称,那么当应用程序需要这部分功能模块的时候,就能通过META-INF/services/下的这个配置文件找到对应的实现类的名称,可以进行加载并且实例化。这也是同时也是服务提供者需要遵守的规则。

下面我们举例子所说的服务提供者就是mysql-connector-java-5.1.46-bin.jar
服务接口就是java.sql.Driver.

栗子:
在我们程序中,需要链接数据库的时候,我们都会在工程中导入一个jar包,数据库驱动jar包在我们的用户类路径中。当然关于实现java.sql.Driver接口的就是在jar包中的。
我的jar包就是这个mysql-connector-java-5.1.46-bin.jar

在这里插入图片描述

然后我们在我们自己的程序的代码中会写像下面这样的的代码进行数据库的链接,在第一句代码执行的时候就开始加载com.mysql.jdbc.Driver这个类了,并且采用的是加载当前类的系统类加载进行加载,因为com.mysql.jdbc.Driver这个类是在我们用户类路径中mysql-connector-java-5.1.46-bin.jar中所实现java.sql.Driver接口的实现类,系统类加载器可以直接加载这个类。

图一:
数据库链接述
之前在使用的过程中,不明白为什么这么写,只是内心默念一句,好了,这是神仙咒语,记住就好,管它什么意思。最近在看线程上下文类加载器的时候牵扯到了这部分,中间也牵扯到了SPI机制,所以就拿这个举例子进行线程上下文加载器和SPI机制的说明。

其实上面代码中Class.forName(“com.mysql.jdbc.Driver”)这句代码不用写。

图二:在这里插入图片描述像图二这样的代码也会正常运行滴,主要是因为SPI和线程上下文加载器应用的原因。

下面详细解释为什么图二代码可以正常运行,并且一同解释SPI机制的工作原理,和线程上下文类加载器的作用。

这部分代码是在我们用户自己的程序中写的,那么当我们用户代码需要运行的时候,首先会加载我们的主类,当然加载我们主类的类加载器是系统类加载器,当主类的代码运行到DriverManager.getConnection(url, “KSG”, “99999”);的时候,我们知道调用了一个类的静态方法,那么会对这个类进行初始化,同时也要加载类DriverManager,那么在初始化的时候当然是执行该类的类构造器()方法,那么就会执行该类中的静态块中的方法。

下面是DriverManager类的静态块的方法:

DriverManager在rt.jar中的java.sql下:加载DriverManager类的类加载器为启动类加载器
DriverManager类是驱动程序管理类

public class DriverManager {  
  private DriverManager(){}

   static {
          loadInitialDrivers();//加载数据库驱动程序,初始化时执行这个方法
          println("JDBC DriverManager initialized");
    }
 }

图三:

在这里插入图片描述
下面看loadInitialDrivers方法,

private static void loadInitialDrivers() {
   String drivers;
    try {
    //这种方式是通过系统的属性得到驱动程序的实现类的名称,上面的图可以说明问题
    //将我们的驱动程序类通过属性的设置,在这个方法中便可以通过属性系统属性获取驱动类的名称;
    //如果我们在图三中不进行系统属性的设置,也是可以运行成功的,这里只是为了解释System.getProperty("jdbc.drivers")这句代码,所以给出的图三。
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
            //通过系统属性获取驱动程序的类的名称,
            //如果获取不到,没有关系,原因是图三的写法其实和图一的写法效果一样
            //但是没有图三和图一的第一句,程序依然可以成功执行,原因就在下面的 AccessController.doPrivileged这段代码中。
                return System.getProperty("jdbc.drivers");
                
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }
    

   AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
    
       
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
            // Do nothing
            }
            return null;
          }
    });
    
    println("DriverManager.initialize: jdbc.drivers = " + drivers);
  
   //下面就是对通过系统属性获得的驱动程序的类的名称的处理
   //通过类名称采用系统类加载器进行类的加载
    if (drivers == null || drivers.equals("")) {
        return;//表示没有设置系统属性
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            //进行驱动程序类的加载,采用系统加载器进行加载
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
  }

上述代码中中间一部分没有写注释,其实中间部分是我所要说的重点。

loadInitialDrivers方法是在初始化DriverManager 类的时候执行的,DriverManager类由启动类加载器加载,当然在loadInitialDrivers方法中如果用到某个类还没有加载,当然是采用启动类加载器进行加载。

ServiceLoader.load(Driver.class)这句代码调用了ServiceLoader类中的静态方法,所以需要加载ServiceLoader类,由启动类加载器进行加载该类,然后执行load方法。

第一句:
ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);

Driver.class 位于rt.jar包下 java.sql.Driver.class是一个接口

ServiceLoader这个类其实是在rt.jar java.util下的类,这个类的作用就是用来查找服务,可以说是jdk提供的一个实现服务查找的一个工具类。同样也是SPI机制中的一部分。

在DriverManager驱动程序管理类中用到这个ServiceLoader类当然是需要查找相关驱动程序接口的服务。

那很明显这里想要查找的服务是Driver服务,也就是数据库连接的驱动程序的实现类,因为Driver是java sql下的一个接口,在博文最开头已经说过了,SPI机制就是将接口与实现类进行分离,但同时也是一种服务发现机制,这里已经开始有些SPI的苗头了。

public static <S> ServiceLoader<S> load(Class<S> service) {
//这个ServiceLoader类中的load方法,采用java.lang.Thread类中getContextClassLoader方法获取类加载器,获取的类加载器默认为系统类加载器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();     
        return ServiceLoader.load(service, cl);
        
    }
}

第二句和第三句:

Iterator driversIterator = loadedDrivers.iterator();
while ( driversIterator.hasNext () ) {
driversIterator.next();
}
上面说到需要查找的服务为Driver服务,那么这里就说到真正的SPI机制,
因为ServiceLoader这个类是用于查找实现类的,所有此类中有一个类的静态变量为private static final String PREFIX = “META-INF/services/”;

public final class ServiceLoader<S> implements Iterable<S>{
    private static final String PREFIX = "META-INF/services/";
    private final ClassLoader loader;
    private final ClassLoader loader;
    private final AccessControlContext acc;
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    private LazyIterator lookupIterator;
}

hasNext()方法中会将"META-INF/services/" + 服务接口的全称作为最终的查找文件的路径 ,因为服务接口是Driver,那么此时的文件查找路径就是"META-INF/services/java.sql.Driver.

hasNext()方法调用了ServiceLoader中的hasNextService()方法:
截取了一部分代码进行说明

private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
  }

因为传入的服务是Driver.class 那么此时的service.getName()就是java.sql.Driver

fullName = PREFIX + service.getName()
= META-INF/services/ java.sql.Driver

configs = loader.getResources(fullName);
这里的loader就是通过ServiceLoader.load(Driver.class)中的load方法所获得的的线程上下文类加载器(即默认的系统类加载器)

loader.getResources(fullName) 就是获取这个fullName文件中的实现类的名称,也就是下面的两个名称
(com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver)
名称为最后一张图的右边内容。

这就对照了我们开头所说的,一个服务提供者需要在自己的jar包中
"META-INF/services/"路径下创建一个以服务接口为名称的文件,并将自己的实现类的名称写入。并且我们的服务提供者就是mysql-connector-java-5.1.46-bin.jar

因为mysql-connector-java-5.1.46-bin.jar实现java.sql.Driver的接口,所以同样也遵守相应的规则,从最后一张图就可以看出来。

driversIterator.next()调用了ServiceLoader中的nextService()方法

 private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
   }

我们很明显看到,在方法中有一句代码是
c = Class.forName(cn, false, loader);这句话事实上就代替了图一的第一句话
Class.forName(name),所以开头说过图一的第一行代码完全不用写也可以运行。

像图一那样写的话,程序进行到Class.forname(name)时,我们就要对名为name的实现类进行初始化,同时也要加载名为name的类,那么图一的情况当然是采用加载当前主类的系统类加载器进行加载,合情合理。

但是在 nextService() 方法中为什么采用了三参,原因是如果我们在nextService() 方法中采用单参的Class.forname(name)会出现什么情况,当运行到Class.forname(name)的时候需要加载名为name的类,但是会采用加载当前的类的类加载器来加载名为name的类,当前类是ServiceLoader,而ServiceLoader是由启动类加载器进行加载的,我们又知道双亲委派的加载模式是自下而上的,启动类加载器没有父加载器,所以只能由自己进行加载,可是当前名为name的类是我们查找的在用户类路径下类库中的类。
启动类加载器无法加载,所以采用了三参模式。

c = Class.forName(cn, false, loader);
此时的cn就是上面通过SPI机制找到的位于用户类路径下的"META-INF/services/java.sql.Driver文件配置的类的名称
(com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver))也就是下图中的右边的内容。

这个loader就是上面ServiceLoader.load(Driver.class)的load代码中通过ClassLoader cl = Thread.currentThread().getContextClassLoader()获得的线程上下文类加载器(默认为系统类加载器)所以此时loader就是系统类加载器,那么此时可以加载名为com.mysql.jdbc.Driver和com.mysql.fabric.jdbc.FabricMySQLDriver的实现类了。

因为这两个类是由服务提供者mysql-connector-java-5.1.46-bin.jar实现的类,并在mysql-connector-java-5.1.46-bin.jar中,所以这两个类是在用户类路径下的两个类,所有可由系统类加载器直接进行加载。但是这样便打破了了双亲委派加载模式,由于启动类记载器不能够进行加载,所以采用了线程上下文类加载器。也就是父加载器要求子类加载器去完成类加载。在这里插入图片描述mysql-connector-java-5.1.46-bin.jar这个jar包中的com.mysql.jdbc.Driver实现了我们之前说的rt.jar中的java.sql.Driver接口,那么mysql-connector-java-5.1.46-bin.jar相当于一个服务提供者,它在自己的jar包的"META-INF/services/路径下创建一个文件名为java.sql.Driver的文件,并且在文件中配置 了实现Driver接口的实现类的名称。

上面的例子同时也体现出了当基础类调用用户代码的时候,我们实际上采用的是一种打破双亲委派模型的的一种加载方式,因为我们知道双亲委派模型的加载方式是自下而上的,而这里采用的是线程上下文类加载器去加载所需要的SPI代码,也就是父加载器请求子加载器去完成类加载动作。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!