JDBC驱动自动加载的原理和ServiceLoader类的使用

在使用JDBC的时候我们都见过这样一条代码

Class.forName("com.mysql.cj.jdbc.Driver");

很多人都告诉我们说这是用来加载数据库驱动的,是的没错,他确实是用来加载驱动的,为什他就能实现驱动的加载?

在mysql驱动包下的Driver类中有下面这样的代码,这个代码是数据静态代码块,当类被初始化的时候会执行该代码块,Class.forName("")方式加载的类都会被自动的初始化

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

他就是向我们的DriverManager这个类中注册自己,所以最后我们能利用DriverManager这个类获取到数据库连接。

下面就是DriverManager类,其中的registeredDrivers就是所有注册过的驱动类

    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)) {
                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");
    }

上面的获取驱动的方式是以前的方式了,现在有了SPI,已经不需要我们手动的去加载驱动类了,SPI会帮助我们自动的加载驱动实现类,其核心就是利用到了ServiceLoader这个类,这个类会扫描我们每一个jar包下面的"/META-INF/service/接口全限定名"文件,以此来获取到对应接口的实现类,然后通过ServiceLoader.load(Driver.class)这种方式就能加载到对应接口的实现类。

我们去观察版本稍微高一点的数据库驱动文件,我们都能在其中找到对应的文件,比如在mysql中

那么问题又来了,是谁去调用的ServiceLoader.load()这个方法呢?

扫描二维码关注公众号,回复: 8944264 查看本文章

答案就在DriverManager这个类中,这个类也存在一个静态代码块

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

在静态代码块中调用了loadInitialDrivers方法,在该方法中我们能找到下面这样一段代码

AccessController.doPrivileged(new PrivilegedAction<Void>() {
    public Void run() {
        // 利用该类去加载了Driver.class接口的实现类,并且进行了实例化
        ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
        Iterator<Driver> driversIterator = loadedDrivers.iterator();
        try{
            while(driversIterator.hasNext()) {
                driversIterator.next();
            }
        } catch(Throwable t) {
        }
        return null;
    }
});

 可以看到加载了实现类之后,并没有做任何的处理,因为加载一个类会自动的执行其static代码块,回顾上面mysql的Driver实现类,他自己就会向DriverManager注册自己,所以这里不用进行任何的处理,就这样完成了驱动的自动加载。

利用SPI加载自定义接口的实现类

先定义一个接口

package com;

public interface SpiService {
    void doService();
}

定义两个简单的实现类

package com;

public class SpiServiceOne implements SpiService {
    public SpiServiceOne() {
    }

    public void doService() {
        System.out.println("第一个实现类");
    }
}
package com;

public class SpiServiceTwo implements SpiService {
    public SpiServiceTwo() {
    }

    public void doService() {
        System.out.println("第二个实现类");
    }
}

将这两个实现类打包成两个jar,然后在两个jar中新增文件夹

这是其中的一个jar

com.SpiService这个文件名即是我们的接口名,我们利用ServiceLoader这个类加载接口的实现类的时候就会去找对应的接口的文件名,然后找到文件中的内容

 这样就能成功的去加载这个实现类了,我们只需要将jar包加到buildPath中,不用进行任何其他的操作

测试

public class TestSpi {
    public static void main(String[] args) {
        loadImpl();
    }


    static void loadImpl() {
        // 加载对应的接口的实现类
        ServiceLoader<SpiService> load = ServiceLoader.load(SpiService.class);
        Iterator<SpiService> iterator = load.iterator();
        while (iterator.hasNext()) {
            SpiService next = iterator.next();
            next.doService();
        }
    }
}

结果

需要注意的是,需要将我们的jar包放到buildPath中,否则spi是不能找到对应的实现类的,因为ServiceLoader这个类默认是使用系统类加载器进行加载的对接口的实现类进行加载的,而系统类加载器默认的加载路径是classpath。当然我们也可以传入自己的类加载器实例来实现不同位置的jar包进行加载,这就是类加载器的知识了

就像下面这样

public class TestSpi {
    public static void main(String[] args) throws MalformedURLException {
        loadImpl();
    }


    static void loadImpl() throws MalformedURLException {
        // 创建一个类加载器实例,并且指定了几个加载的jar
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:/C:/Users/12130/Desktop/spiServiceImpOne.jar")
                , new URL("file:/C:/Users/12130/Desktop/spiServiceImpTwo.jar")});
        // 传入我们的类加载器
        ServiceLoader<SpiService> load = ServiceLoader.load(SpiService.class, urlClassLoader);
        Iterator<SpiService> iterator = load.iterator();
        while (iterator.hasNext()) {
            SpiService next = iterator.next();
            next.doService();
        }
    }
}
发布了162 篇原创文章 · 获赞 44 · 访问量 8855

猜你喜欢

转载自blog.csdn.net/P19777/article/details/103356943