JAVA的SPI机制-案例-JDBC

建议打开Idea,引入mysql的驱动包,跟一遍代码

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.49</version>
</dependency>

原生JDBC

JDBC提供了java访问数据库的规范,当连接mysql时,引入mysql的jdbc驱动包;连接sqlserver时,引入sqlserver的jdbc驱动包;oracle也是一样。各种驱动像是一个部件,想用哪个直接更换到对应的驱动即可,代码里面连接数据库的操作不用做任何改动。

JDBC是怎么做到的?

回想一下jdbc的原生写法:

加载驱动

Class.forName(classname);

建立连接

Connection conn = DriverManager.getConnection(url);

在JDBC4.0之后,不需要加载驱动,也可以成功获取到连接,那么这个时候驱动是如何被加载到JVM并使用的?

DriverManager.getConnection

点击进入DriverManager.getConnection(url)方法

public static Connection getConnection(String url)
        throws SQLException {

        java.util.Properties info = new java.util.Properties();
        return (getConnection(url, info, Reflection.getCallerClass()));
    }

逻辑在getConnection方法中,继续点击

//  Worker method called by the public getConnection() methods.
    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        // 省略...
        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                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");
    }

这里会遍历registeredDrivers进行检查并获取到真实的连接,跟踪看到声明,是DriverManager的私有成员变量,默认初始化为空List:

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

那么registeredDrivers这个集合的内容又来自于哪里? 在DriverManager中,只有一个registerDriver的方法会往里面设值

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

        /* Register the driver if it has not already been added to our list */
        if(driver != null) {
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            // This is for compatibility with the original DriverManager
            throw new NullPointerException();
        }

        println("registerDriver: " + driver);

    }

但是在DriverManager中并没有看到调用的地方,说明这个方法是被外部调用的。引入mysql的驱动,然后再看java.sql.DriverManager#registerDriver(java.sql.Driver)的引用,发现

registerDriver(java.sql.Driver)

mysql驱动包的静态代码块

点击进入到com.mysql.fabric.jdbc.FabricMySQLDriver类,发现调用是在静态代码块中

// Register ourselves with the DriverManager
static {
    try {
        DriverManager.registerDriver(new FabricMySQLDriver());
    } catch (SQLException ex) {
        throw new RuntimeException("Can't register driver", ex);
    }
}

嗯。。静态代码块?需要类被加载的时候才执行,但是我们不调用Class.forName,怎么才能加载FabricMySQLDriver呢?但是事实证明肯定被加载了,因为我们可以正常连接到数据库。

回到DriverManager的静态代码块

不慌,继续回到JDK提供的DriverManager中,发现里面也有一个静态代码块,其实这里的注释已经说明了一切。

    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

进入loadInitialDrivers方法

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() {
								// Java的SPI机制加载classpath中所有的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()) {
                        driversIterator.next(); // 重点是这个next里面
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

可以看到有一行ServiceLoader.load(Driver.class),ServiceLoader的全路径是java.util.ServiceLoader,来自于jdk。这就是Java的SPI机制。

可以看到,在mysql的驱动包下面,有一个META-INF/services/java.sql.Driver的文件

spi path

文件内容为

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

了解SPI机制原理请参考:JAVA的SPI机制

这里的load方法会把classpath中的所有Driver的实现类加载进来并注册到DriverManager里面,作为Driver的实现类,com.mysql.fabric.jdbc.FabricMySQLDriver自然也会被加载进来,其中的静态代码块也就执行了,将自己注册到DriverManger中,供DriverManger.getConnection()进行使用。

画个图

最后画个图总结一下

画了半天,将就看,领会精神。。

driver load progress

DriverManager在加载时通过静态代码块,已经通过SPI机制,通过ServiceLoader将classpath的实现类加载进来,并通过实现类的静态代码块完成真实driver的注册,以便于在调用者调用getConnection的时候可以遍历已有的driver进行连接的获取。

最后,可能稍微细心点会发现getConnection内部的for循环中有一个检测isDriverAllowed(aDriver.driver, callerCL),具体方法体

    private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
        boolean result = false;
        if(driver != null) {
            Class<?> aClass = null;
            try {
                aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
            } catch (Exception ex) {
                result = false;
            }

             result = ( aClass == driver.getClass() ) ? true : false; // 这个三目运算...
        }

        return result;
    }

这个方法的主要作用是为了确保加载到的driver class与调用者所在的类加载器是同一个,for里面的注释也进行了说明

// If the caller does not have permission to load the driver then
// skip it.

疑问

所有,如果我的环境里面同事存在多个驱动包,比如mysql与sqlsever的驱动包,这里会随机返回一个?

posted @ 2020-07-17 23:19  六月瓜  阅读(303)  评论(0编辑  收藏  举报