JDBC 详解(二):JDBC & SPI & 双亲委派机制

551 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情

在 《JDBC 详解(一):JDBC 基本介绍与使用》中的 DriverManager 介绍中,我们已经知道 DriverManager 支持Java标准版服务提供程序机制(SPI),并且维护了一个 registeredDrivers 列表,在获取数据库连接时会根据 JDBC URL 遍历这个列表找到合适的 Driver。

这篇文章我们来聊一下 registeredDrivers 列表在初始化时是如何寻找到具体的 Driver 的,其背后使用的 SPI 机制是什么,以及 SPI 与类加载器双亲委派机制的矛盾与解决。

SPI 机制

SPI(Service Provider Interface) ,是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件。Java 中 SPI 机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦

我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。

Java SPI 定义步骤

Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件,该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。

Java SPI实现流程:

  1. 定义SPI 接口标准
  2. 提供方提供具体实现:实现方法配置在META-INF/services/${interface_name} 文件
  3. 开发者使用,JDK中查找服务的实现的工具类是:java.util.ServiceLoader

ServiceLoader 源码

// ServiceLoader实现了Iterable接口,可以遍历所有的服务实现者
public final class ServiceLoader<S> implements Iterable<S>
{
    // 查找配置文件的目录
    private static final String PREFIX = "META-INF/services/";
    // 表示要被加载的服务的类或接口
    private final Class<S> service;
    // 这个ClassLoader用来定位,加载,实例化服务提供者
    private final ClassLoader loader;
    // 访问控制上下文
    private final AccessControlContext acc;
    // 缓存已经被实例化的服务提供者,按照实例化的顺序存储
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // 迭代器
    private LazyIterator lookupIterator; 
}
// 服务提供者查找的迭代器
public Iterator<S> iterator() {
    return new Iterator<S>() {
        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();
        // hasNext方法
        public boolean hasNext() {
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }
        // next方法
        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }
    };
}

// 服务提供者查找的迭代器
private class LazyIterator implements Iterator<S> {
    // 服务提供者接口
    Class<S> service;
    // 类加载器
    ClassLoader loader;
    // 保存实现类的url
    Enumeration<URL> configs = null;
    // 保存实现类的全名
    Iterator<String> pending = null;
    // 迭代器中下一个实现类的全名
    String nextName = null;
 
    public boolean hasNext() {
        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);
            }
        }
        while ((pending == null) || !pending.hasNext()) {
            if (!configs.hasMoreElements()) {
                return false;
            }
            pending = parse(service, configs.nextElement());
        }
        nextName = pending.next();
        return true;
    }
 
    public S next() {
        if (!hasNext()) {
            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");
        }
        if (!service.isAssignableFrom(c)) {
            fail(service, "Provider " + cn  + " not a subtype");
        }
        try {
            S p = service.cast(c.newInstance());
            providers.put(cn, p);
            return p;
        } catch (Throwable x) {
            fail(service, "Provider " + cn + " could not be instantiated: " + x, x);
        }
        throw new Error();          // This cannot happen
    }
}
  1. ServiceLoader实现了Iterable接口,并且内部提供了懒加载迭代器lookupIterator
  2. LazyIterator中的hasNext方法,静态变量PREFIX就是”META-INF/services/”目录
  3. 通过反射方法Class.forName()加载类对象,并用newInstance方法将类实例化,并把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型) 然后返回实例对象

SPI 接口与 API 接口的区别

二者区别:来自stackoverflow.com/questions/2…

  • The API is the description of classes/interfaces/methods/... that you call and use to achieve a goal, and
  • the SPI is the description of classes/interfaces/methods/... that you extend and implement to achieve a goal.

Put differently, the API tells you what a specific class/method does for you, and the SPI tells you what you must do to conform.

Driver 加载

DriverManager 中的 Driver 加载发生在DriverManager类加载的时候,通过静态代码块执行 loadInitialDrivers()方法,会经过以下步骤:

  1. 系统属性中尝试获取驱动类的路径
  2. 通过SPI机制的工具类ServiceLoader去加载驱动类,并在 ServiceLoader.iterator().next()中完成类加载与实例化。(Driver 类在实例化时会向DriverManager 注册自己。)
  3. 若第一步加载类路径不为空,则再去加载系统属性中的 Driver 类
static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

private static void loadInitialDrivers() {                                                          
    String drivers;                                                                                 
    try {
        // 1. 系统属性中尝试获取驱动类的路径
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {                    
            public String run() {                                                                   
                return System.getProperty("jdbc.drivers");                                          
            }                                                                                       
        });                                                                                         
    } catch (Exception ex) {                                                                        
        drivers = null;                                                                             
    }                                                                                               
    // If the driver is packaged as a Service Provider, load it.                                    
    // Get all the drivers through the classloader                                                  
    // exposed as a java.sql.Driver.class service.                                                  
    // ServiceLoader.load() replaces the sun.misc.Providers()                                       
    // 2. 通过SPI机制的工具类ServiceLoader去加载驱动类                                                                                             
    AccessController.doPrivileged(new PrivilegedAction<Void>() {                                    
        public Void run() {                                                                         
            // 3. 加载Driver接口的服务类,META-INF/services/java.sql.Driver                                                                                     
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);                 
            Iterator<Driver> driversIterator = loadedDrivers.iterator();                            
                                                                                                    
            /* Load these drivers, so that they can be instantiated.                                
             * It may be the case that the driver class may not be there                            
             * i.e. there may be a packaged driver with the service class                           
             * as implementation of java.sql.Driver but the actual class                            
             * may be missing. In that case a java.util.ServiceConfigurationError                   
             * will be thrown at runtime by the VM trying to locate                                 
             * and load the service.                                                                
             *                                                                                      
             * Adding a try catch block to catch those runtime errors                               
             * if driver not available in classpath but it's                                        
             * packaged as service and that service is there in classpath.                          
             */                                                                                     
            try{                                                                                    
                while(driversIterator.hasNext()) {  
                    // next 方法中创建对象
                    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);                                
        }                                                                                           
    }                                                                                               
}                                                                                                   

SPI 与 ClassLoader

双亲委派机制

类加载器的基础知识点:

  • 每个ClassLoader都只能加载自己所绑定目录下的资源;
  • 一个类由类加载器A加载,那么这个类依赖类也应该由相同的类加载器加载

对于 JDBC 来说,Driver.class 在 rt.jar 中应该被 Bootstrap ClassLoader 加载,但是其 SPI 机制的实现类却是由 System ClassLoader 加载,即双亲委派机制无法解决此问题

ContextClassLoader

Bootstrap ClassLoader、ExtClassLoader、AppClassLoader,它们是真实存在的类,而且遵从”双亲委托“的机制。

而在 Java 中存在另外一种「虚拟」的 ContextClassLoader。contextClassLoader 只是一个成员变量,可以被设置和修改。

public class Thread implements Runnable {

/* The context ClassLoader for this thread */
   private ClassLoader contextClassLoader;
 
   public void setContextClassLoader(ClassLoader cl) {
       SecurityManager sm = System.getSecurityManager();
       if (sm != null) {
           sm.checkPermission(new RuntimePermission("setContextClassLoader"));
       }
       // 默认设置为当前线程 cl 即 AppClassLoader
       contextClassLoader = cl;
   }

   public ClassLoader getContextClassLoader() {
       if (contextClassLoader == null)
           return null;
       SecurityManager sm = System.getSecurityManager();
       if (sm != null) {
           ClassLoader.checkClassLoaderPermission(contextClassLoader,
                                                  Reflection.getCallerClass());
       }
       return contextClassLoader;
   }
}

SPI 使用 ContextClassLoader

在 ServiceLoader 中,load 方法中获取 ContextClassLoader 并传入迭代器中。所以,上文中 nextService() 方法中加载 Driver.class 及其实现类的加载器是被设置的线程上下文类加载器,以此打破了双亲委派规则。

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取线程上下文类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl); // 传入加载方法
}

// 在初始化 lookupIterator 时,传入线程上下文类加载器
public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}