JDBC SPI 类加载机制

https://segmentfault.com/a/1190000017517197?utm_source=tag-newest

 

一句话总结:

1 自动加载:DriverManager的静态代码块执行的时刻,使用该时刻当前线程类加载器加载java.sql.Driver文件,并用当前线程类加载器加载及、实例化、registerDriver

2 没有自动加载的,手动forName,传入一个类加载器及是否初始化;或loadClass➕实例化(load/find class 与 forname 在static代码块加载的不同 (二)非系统类jdbc

3 getConncetion时,校验调用getConnection的类所在类加载器与driver实现所在类加载器校验存放的driver是否属于调用者的ClassloaderJDBC注册原理与自定义类加载器解决com.cloudera.hive.jdbc41.HS2Driver的加载【重点】 这个连接通过伪造一个中间人骗过校验

 

SPI概述

SPI全称为(Service Provider Interface) ,是JDK内置的一种服务提供发现机制;主要被框架的开发人员使用,比如java.sql.Driver接口,数据库厂商实现此接口即可,当然要想让系统知道具体实现类的存在,还需要使用固定的存放规则,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类;下面以JDBC为实例来进行具体的分析。

JDBC驱动

1.准备驱动包

<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>42.2.2</version>
        </dependency>
        <dependency>
            <groupId>com.microsoft.sqlserver</groupId>
            <artifactId>mssql-jdbc</artifactId>
            <version>7.0.0.jre8</version>
        </dependency>

分别准备了mysql,postgresql和sqlserver,可以打开jar,发现每个jar包的META-INF/services/都存在一个java.sql.Driver文件,文件里面存在一个或多个类名,比如mysql:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

提供的每个驱动类占据一行,解析的时候会按行读取,具体使用哪个会根据url来决定;

2.简单实例

String url = "jdbc:mysql://localhost:3306/db3";
String username = "root";
String password = "root";
String sql = "update travelrecord set name=\'bbb\' where id=1";
Connection con = DriverManager.getConnection(url, username, password);

类路径下存在多个驱动包,具体在使用DriverManager.getConnection应该使用哪个驱动类会解析url来识别,不同的数据库有不同的url前缀;

3.驱动类加载分析

具体META-INF/services/下的驱动类是什么时候加载的,DriverManager有一个静态代码块:

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}
 
private static void loadInitialDrivers() {
    String drivers;
    try {
        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()
 
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
 
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);【会遍历DriverManage静态代码块执行时,当前线程类加载器下找】
            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()) {
                    driversIterator.next();【重要,在这一句forname和实例化】
                }
            } 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);
        }
    }
}

在加载DriverManager类的时候会执行loadInitialDrivers方法,方法内通过了两种加载驱动类的方式,分别是:使用系统变量方式和ServiceLoader加载方式;系统变量方式其实就是在变量jdbc.drivers中配置好驱动类,然后使用Class.forName进行加载;下面重点看一下ServiceLoader方式,此处调用了load方法但是并没有真正去加载驱动类,而是返回了一个LazyIterator,后面的代码就是循环变量迭代器:

private static final String PREFIX = "META-INF/services/";
 
private class LazyIterator
        implements Iterator<S>
    {
 
        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;
 
        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }
 
        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    String fullName = PREFIX + service.getName(); service:java.sql.Driver
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);【重要,遍历META-INF/servises/java.sql.Driver资源文件】
                } 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;
        }
 
        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");
            }
            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);
            }
            throw new Error();          // This cannot happen
        }
        ......
    }

类中指定了一个静态常量PREFIX = “META-INF/services/”,然后和java.sql.Driver拼接组成了fullName然后通过类加载器(classloader getresource jar包资源冲突情况,父亲为大,兄弟之间,谁先加载谁牛逼(二))去获取所有类路径(当前类加载)下java.sql.Driver文件,获取之后存放在configs中,里面的每个元素对应一个文件,每个文件中可能会存在多个驱动类,所以使用pending用来存放每个文件中的驱动信息,获取驱动信息之后在nextService中使用Class.forName加载类信息,并且指定不进行初始化;同时在下面使用newInstance 触发各驱动的静态代码块load/find class 与 forname 在static代码块加载的不同对驱动类进行了实例化操作;每个驱动类中都提供了一个静态注册代码块,比如mysql:

static {
    try {
        java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}

这里又实例化了一个驱动类,同时注册到DriverManager;接下来就是调用DriverManager的getConnection方法,代码如下:

private static Connection getConnection(
       String url, java.util.Properties info, Class<?> caller) throws SQLException {
       /*
        * When callerCl is null, we should check the application's
        * (which is invoking this class indirectly)
        * classloader, so that the JDBC driver class outside rt.jar
        * can be loaded from here.
        */
       ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;【调用方的类所在类加载器】
       synchronized(DriverManager.class) {
           // synchronize loading of the correct classloader.
           if (callerCL == null) {
               callerCL = Thread.currentThread().getContextClassLoader();
           }
       }
 
       if(url == null) {
           throw new SQLException("The url cannot be null", "08001");
       }
 
       println("DriverManager.getConnection(\"" + url + "\")");
 
       // Walk through the loaded registeredDrivers attempting to make a connection.
       // Remember the first exception that gets raised so we can reraise it.
       SQLException reason = null;
 
       for(DriverInfo aDriver : registeredDrivers) {
           // If the caller does not have permission to load the driver then
           // skip it.
           if(isDriverAllowed(aDriver.driver, callerCL)) {(JDBC注册原理与自定义类加载器解决com.cloudera.hive.jdbc41.HS2Driver的加载【重点】中用一个代理中间人骗过了)
               try {
                   println("    trying " + aDriver.driver.getClass().getName());
                   Connection con = aDriver.driver.connect(url, info);
                   if (con != null) {
                       // Success!
                       println("getConnection returning " + aDriver.driver.getClass().getName());
                       return (con);
                   }
               } catch (SQLException ex) {
                   if (reason == null) {
                       reason = ex;
                   }
               }
 
           } else {
               println("    skipping: " + aDriver.getClass().getName());
           }
 
       }
 
       // if we got here nobody could connect.
       if (reason != null)    {
           println("getConnection failed: " + reason);
           throw reason;
       }
 
       println("getConnection: no suitable driver found for "+ url);
       throw new SQLException("No suitable driver found for "+ url, "08001");
   }

此方法主要是遍历之前注册的DriverInfo,拿着url信息去每个驱动类中建立连接,当然每个驱动类中都会进行url匹配校验,成功之后返回Connection,如果中途有失败的连接并不影响尝试新的驱动连接,遍历完之后还是无法获取连接,则抛出异常;

posted on 2020-01-14 16:18  silyvin  阅读(1894)  评论(0编辑  收藏  举报