• 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的驱动包,这里会随机返回一个?

  • 相关阅读:
    阅读笔记《梦断代码》其一
    第一次冲刺(第九天)
    第一次冲刺(第八天)
    第一冲刺阶段(第七天)
    第一冲刺阶段(第六天)
    第一冲刺阶段(第五天)
    MySQL数据库半同步复制
    MySQL事物
    MySQL字符编码
    MySQL用户授权
  • 原文地址:https://www.cnblogs.com/nickhan/p/13334044.html
Copyright © 2020-2023  润新知