Java的SPI机制和JDBC的实现

转载自https://mp.weixin.qq.com/s?__biz=MzIxNDY0MTg2MA==&mid=2247483935&idx=1&sn=e6da46cfe2df2812fd2b9e24253ec246&chksm=97a53fb4a0d2b6a2896b5c0850e83a7852ad08fbe0939bb61d04982bc0d03d3f6da25ee56dbf&scene=21#wechat_redirect

 

前言

SPI(Service Provider Interface)是JDK内置的一种提供服务发现的机制。如果你读过dubbo的源码,你就一定对SPI机制不陌生,Dubbo基于SPI机制提供了很多扩展功能,实现了微内核+插件的体系。如果你没有用过dubbo,没关系,JDBC你总该用过吧,还记得创建连接的写法吗?

 
  1. Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);

只需要一行代码,(都不需要指定Class.forName("com.mysql.jdbc.Driver");) 再提供商不同厂商的jar包,就可以轻松创建连接了,这其中的奥秘就要归功于SPI机制。

SPI机制

在java中根据一个子类获取其父类或接口信息非常方便,但是根据一个接口获取该接口的所有实现类却没那么容易。有一种比较笨的办法就是扫描classpath下所有的class与jar包中的class,接着用ClassLoader加载进来,再判断是否是给定接口的子类。但是这种方法的代价太大,一般不会使用。

根据这个问题,java推出了ServiceLoader类来提供服务发现机制,动态的为某个接口寻找服务实现,这种机制有点类似IOC思想,将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

当服务的提供者提供了服务接口的一种实现之后,必须根据SPI约定在 META-INF/services/ 目录里创建一个以服务接口命名的文件,该文件里写的就是实现该服务接口的具体实现类。当程序调用ServiceLoader的load方法的时候,ServiceLoader能够通过约定的目录找到指定的文件,并装载实例化,完成服务的发现。

我们通过一个例子来加深对SPI机制的理解:

使用

首先我们提供一个接口类 Animal 以及它的两个实现类 Dog和Pig (都在包com.github.spi下):

 
  1. public interface Animal {

  2.    void eat();

  3. }

  4.  

  5. public class Dog implements Animal {

  6.  

  7.    @Override

  8.    public void eat() {

  9.        System.out.println("Dog eating...");

  10.    }

  11. }

  12.  

  13. public class Pig implements Animal {

  14.  

  15.    @Override

  16.    public void eat() {

  17.        System.out.println("Pig eating...");

  18.    }

  19. }

接着在classpath下创建文件夹 META-INF/services ,在文件夹中新建一个文件 com.github.spi.Animal并在文件中写入具体的实现类:

 
  1. com.github.spi.Pig

  2. com.github.spi.Dog

接着,我们就可以利用ServiceLoader进行服务发现了:

 
  1. public class SPITest {

  2.  

  3.    public static void main(String[] args) {

  4.        ServiceLoader load = ServiceLoader.load(Animal.class);

  5.        Iterator iterator = load.iterator();

  6.        while (iterator.hasNext()) {

  7.            Animal animal = iterator.next();

  8.            animal.eat();

  9.        }

  10.    }

  11. }

结果验证了我们的猜想,dog和pig的eat方法依次被调用了。我们顺着好奇心看看load方法是如何实现的。

一探ServiceLoader源码

 
  1. public static ServiceLoader load(Class service) {

  2.    // 获取当前调用线程的类加载器

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

  4.    return ServiceLoader.load(service, cl);

  5. }

  6.  

  7. public static ServiceLoader load(Class service, ClassLoader loader) {

  8.    return new ServiceLoader<>(service, loader);

  9. }

load方法仅仅获取了当前调用线程的类加载器实例化之后就返回了。我们接着看iterator方法做了什么:

 
  1. public Iterator iterator() {

  2.    return new Iterator() {

  3.  

  4.         // 缓存第一次查找发现的服务类,下次再进行遍历直接返回

  5.        Iterator> knownProviders

  6.                = providers.entrySet().iterator();

  7.  

  8.        public boolean hasNext() {

  9.            if (knownProviders.hasNext())

  10.                return true;

  11.            return lookupIterator.hasNext();

  12.        }

  13.  

  14.        public S next() {

  15.            if (knownProviders.hasNext())

  16.                return knownProviders.next().getValue();

  17.            return lookupIterator.next();

  18.        }

  19.  

  20.        public void remove() {

  21.            throw new UnsupportedOperationException();

  22.        }

  23.  

  24.    };

  25. }

hasNext和next方法都调用了lookupIterator,这个类是在load的时候调用构造函数实例化的时候初始化的。看类名就能知道它是懒加载的意思(LazyIterator):

 
  1. private ServiceLoader(Class svc, ClassLoader cl) {

  2.    service = Objects.requireNonNull(svc, "Service interface cannot be null");

  3.    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;

  4.    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;

  5.    // 重新加载

  6.    reload();

  7. }

  8.  

  9. public void reload() {

  10.    // 清空缓存

  11.    providers.clear();

  12.    // 初始化LazyIterator

  13.    lookupIterator = new LazyIterator(service, loader);

  14. }

我们直接看LazyIterator里对应hasNext和next的两个方法:

 
  1. Class service;

  2. ClassLoader loader;

  3. Enumeration configs = null;

  4. Iterator pending = null;

  5. String nextName = null;

  6.  

  7. private LazyIterator(Class service, ClassLoader loader) {

  8.    this.service = service;

  9.    this.loader = loader;

  10. }

  11.  

  12. private boolean hasNextService() {

  13.    if (nextName != null) {

  14.        return true;

  15.    }

  16.    if (configs == null) {

  17.        try {

  18.             // 通过类加载器加载classpath:META-INF/services/serviceName

  19.            String fullName = PREFIX + service.getName();

  20.            if (loader == null)

  21.                configs = ClassLoader.getSystemResources(fullName);

  22.            else

  23.                configs = loader.getResources(fullName);

  24.        } catch (IOException x) {

  25.            fail(service, "Error locating configuration files", x);

  26.        }

  27.    }

  28.    while ((pending == null) || !pending.hasNext()) {

  29.        if (!configs.hasMoreElements()) {

  30.            return false;

  31.        }

  32.         // 解析文件里的值,多行为多个值,返回一个Iterator

  33.        pending = parse(service, configs.nextElement());

  34.    }

  35.     // 下一个要获取的实现类类名全称

  36.    nextName = pending.next();

  37.    return true;

  38. }

  39.  

  40. private S nextService() {

  41.    if (!hasNextService())

  42.        throw new NoSuchElementException();

  43.     // 拿到hasNext赋值的nextName

  44.    String cn = nextName;

  45.    nextName = null;

  46.    Class c = null;

  47.    try {

  48.         // 加载类

  49.        c = Class.forName(cn, false, loader);

  50.    } catch (ClassNotFoundException x) {

  51.        fail(service, "Provider " + cn + " not found");

  52.    }

  53.    if (!service.isAssignableFrom(c)) {

  54.        fail(service, "Provider " + cn  + " not a subtype");

  55.    }

  56.    try {

  57.        S p = service.cast(c.newInstance());

  58.         // 放入providers里面,下次遍历的时候可以直接用

  59.        providers.put(cn, p);

  60.        return p;

  61.    } catch (Throwable x) {

  62.        fail(service, "Provider " + cn + " could not be instantiated", x);

  63.    }

  64.    throw new Error();          // This cannot happen

  65. }

系统的ServiceLoader通过返回一个Iterator对象能够做到对服务实例的懒加载,只有当调用iterator.next()方法时才会实例化下一个服务实例,只有需要使用的时候才进行实例化。

看到这里,应该明白了ServiceLoader所干的事了。首先根据约定的包获取到对应的接口文件,接着解析出文件中的所有服务实现类并加载实例化。

JDBC中的SPI机制

回到之前的一个问题,为什么只需要下面的一行代码,再提供商不同厂商的jar包,就可以轻松创建连接了呢?

 
  1. Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);

DriverManager中有一个静态代码块,在调用getConnection之前就会被调用。

 
  1. static {

  2.    loadInitialDrivers();

  3. }

  4.  

  5. private static void loadInitialDrivers() {

  6.    String drivers;

  7.    try {

  8.        drivers = AccessController.doPrivileged(new PrivilegedAction() {

  9.            public String run() {

  10.                 // 1、处理系统属性jdbc.drivers配置的值

  11.                return System.getProperty("jdbc.drivers");

  12.            }

  13.        });

  14.    } catch (Exception ex) {

  15.        drivers = null;

  16.    }

  17.  

  18.    AccessController.doPrivileged(new PrivilegedAction() {

  19.        public Void run() {

  20.  

  21.            // 2、处理通过ServiceLoader加载的Driver类

  22.            ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);

  23.            Iterator driversIterator = loadedDrivers.iterator();

  24.  

  25.            // 加载配置在META-INF/services/java.sql.Driver文件里的Driver实现类

  26.            try{

  27.                while(driversIterator.hasNext()) {

  28.                    driversIterator.next();

  29.                }

  30.            } catch(Throwable t) {

  31.                // 忽略异常

  32.            }

  33.            return null;

  34.        }

  35.    });

  36.  

  37.    if (drivers == null || drivers.equals("")) {

  38.        return;

  39.    }

  40.    String[] driversList = drivers.split(":");

  41.    for (String aDriver : driversList) {

  42.        try {

  43.             // 3、加载driver类

  44.            Class.forName(aDriver, true,

  45.                    ClassLoader.getSystemClassLoader());

  46.        } catch (Exception ex) {

  47.        }

  48.    }

  49. }

这个代码主要分三块来看:

第一部分:当用户配置了 jdbc.drivers 时,获取到对应的值,步骤3有用。

第二部分:通过ServiceLoader加载Driver类,得到所有不同数据库厂商的Driver类。

比如你引入了mysql的jar包 mysql-connector-java ,打开jar包后会发现它按照ServiceLoader的要求提供了 META-INF/services 包,并且包下面有一个叫 java.sql.Driver 的文件,文件的内容为: com.mysql.jdbc.Driver ,当然如果你还引入了oracle的jar包,你会发现它也有一个一样的文件,不过文件的内容为: oracle.jdbc.OracleDriver 。也就是load这一步会把所有配置的Driver类都获取到。

接着拿到所有的驱动类后进行迭代并调用next加载驱动类,所以触发了类加载,我们以mysql的Driver类来看:

 
  1. static {

  2.    try {

  3.        java.sql.DriverManager.registerDriver(new Driver());

  4.    } catch (SQLException E) {

  5.        throw new RuntimeException("Can't register driver!");

  6.    }

  7. }

它反过来调用了DriverManager的registerDriver方法来注册了自己的Driver类。

 
  1. // 缓存已注册的Driver类

  2. private final static CopyOnWriteArrayList registeredDrivers = new CopyOnWriteArrayList<>();

  3.  

  4. public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException {

  5.    registerDriver(driver, null);

  6. }

  7.  

  8. public static synchronized void registerDriver(java.sql.Driver driver, DriverAction da) throws SQLException {

  9.    if(driver != null) {

  10.        registeredDrivers.addIfAbsent(new DriverInfo(driver, da));

  11.    } else {

  12.        throw new NullPointerException();

  13.    }

  14. }

因为注册Driver类是一次性操作,后面都是读操作,所以这里用了CopyOnWriteArrayList这样一个应用在读多写少场景的并发List。

第三部分:如果拿到了第一部分的值,根据 : 拆分多个驱动实现类,并手动调用Class.forName进行类的加载,从而让不同的驱动类调用刚才说过的registerDriver方法。

因为第二部分已经做过同样的事,所以用户没有必要配置 jdbc.drivers

回到最开始的调用,我们来看看getConnection方法,因为所有的获取连接都会调用下面这个方法,这里就只列出它来。

 
  1. private static Connection getConnection(

  2.        String url, java.util.Properties info, Class caller) throws SQLException {

  3.    // 获取调用类的ClassLoader

  4.    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;

  5.    synchronized(DriverManager.class) {

  6.        if (callerCL == null) {

  7.            callerCL = Thread.currentThread().getContextClassLoader();

  8.        }

  9.    }

  10.  

  11.    if(url == null) {

  12.        throw new SQLException("The url cannot be null", "08001");

  13.    }

  14.  

  15.    SQLException reason = null;

  16.  

  17.    for(DriverInfo aDriver : registeredDrivers) {

  18.        // 校验调用类是否有权限加载Driver

  19.        if(isDriverAllowed(aDriver.driver, callerCL)) {

  20.            try {

  21.                 // 调用connect建立连接

  22.                Connection con = aDriver.driver.connect(url, info);

  23.                if (con != null) {

  24.                    // Success!

  25.                    return (con);

  26.                }

  27.            } catch (SQLException ex) {

  28.                if (reason == null) {

  29.                    reason = ex;

  30.                }

  31.            }

  32.        }

  33.    }

  34.  

  35.    // if we got here nobody could connect.

  36.    if (reason != null)    {

  37.        throw reason;

  38.    }

  39.  

  40.    throw new SQLException("No suitable driver found for "+ url, "08001");

  41. }

首先拿到调用类的classLoader和缓存的驱动类的classLoader进行比较,是同一个classLoader才放行,继续调用connect建立连接,下面是isDriverAllowed的源码。

 
  1. private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {

  2.    boolean result = false;

  3.    if(driver != null) {

  4.        Class aClass = null;

  5.        try {

  6.             // 使用传入的类加载器进行driver类的加载,后面会把加载完的类和driver的类进行比较

  7.            aClass =  Class.forName(driver.getClass().getName(), true, classLoader);

  8.        } catch (Exception ex) {

  9.            result = false;

  10.        }

  11.  

  12.         // 比较是否是同一个类加载器加载的class,如果是同一个返回true允许加载driver,否则不允许

  13.        result = ( aClass == driver.getClass() ) ? true : false;

  14.    }

  15.  

  16.    return result;

  17. }

最后我们回过头来看看这发生的一切,是不是豁然开朗了许多。JDBC使用了SPI机制,让所有的任务都交给不同的数据库厂商各自去完成,无论是实现Driver接口,还是SPI要求的接口文件,都做到了让用户不需要关心一点细节,一行代码建立连接,So Cool~

你可能感兴趣的:(学学学Java)