1. 程式人生 > 實用技巧 >從jdbc4.0 不再需要再寫Class.forName("")到SPI技術

從jdbc4.0 不再需要再寫Class.forName("")到SPI技術

一開始學習JDBC的時候到現在一直認為連線資料庫之前首先需要

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

直到某一天至少是在寫這篇部落格之前。為什麼註釋掉了還是可以連線資料庫?這個問題先等一等看。
先回憶下例項化物件的幾種方式

        //1.直接new
        Hello hello = new Hello();
        hello.say();
        //2.Class.forName.newInstance
        Hello hello2 = (Hello) Class.forName("com.mySpi.Hello").newInstance();
        hello2.say();
        //3.需實現Serializable, 運用反序列化手段,呼叫java.io.ObjectInputStream物件的 readObject()方法。

繼續我們問題,既然不需要顯示載入,那肯定是有其他的類替我們載入了,首先看DriverManager,因為除了main方法所在的類,最先調的只有他了,再找下該類下面有沒有靜態程式碼塊,因為它是最先被jvm載入的,然後我們會發現

/**
     * 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 通過classloader 獲取所有的驅動
        // exposed as a java.sql.Driver.class service. Driver介面的有實現類作為服務
        // ServiceLoader.load() replaces the sun.misc.Providers() 載入實現類

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        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);
            }
        }
    }


在loadInitialDrivers()方法中,我們發現通過ServiceLoader機制載入java.sql.Driver介面的實現類,然後對所有實現類進行遍歷,來完成了驅動類的載入。
那麼問題來了,jdk是如何知道java.sql.Driver有哪些實現類的呢?
主角登場:

SPI,官方話是JDK內建的一種服務提供發現機制。比如有一個介面,想在執行時動態地給它新增實現,只需要按照SPI的規矩來辦,那麼SPI機制在程式執行時就會發現該實現類;規則如下:
在classpath下META-INF/services 新建一以介面名(如java.sql.Driver)為名稱的檔案,檔案內容是介面實現類(如mysql驅動包下META-INF/services/java.sql.Driver裡的com.mysql.jdbc.Driver),mysql-connect--的示例檔案如下



所以不用顯示Class.forName的原因是:核心ServiceLoader類提供了一個靜態的load()方法,用於載入指定介面的所有實現類。呼叫該方法後,classpath下META-INF/services目錄的java.sql.Driver檔案中指定的所有實現類都會被載入。

現在我們知道spi機制的核心方法,可以寫一個demo 實現下這種情況
首先新建SayHello介面,然後新建它的兩個實現類Hello,Hi,程式碼如下

public interface SayHello {
    void say();
}
public class Hello implements SayHello{
    @Override
    public void say() {
        System.out.println("hello world--hello");
    }
}
public class Hi implements SayHello {
    @Override
    public void say() {
        System.out.println("hello world --hi");
    }
}

首先在resource檔案下新建META-INF資料夾,然後META-INF下新建services資料夾,該資料夾下新建com.mySpi.SayHello檔案,內容如下

下面我們寫一個main 方法測試是否可以載入實現類,如果已經載入,就可以列印

 public static void main(String[] args) {
        ServiceLoader<SayHello> sayHellos = ServiceLoader.load(SayHello.class);
        Iterator<SayHello> driversIterator = sayHellos.iterator();
        while (driversIterator.hasNext()){
            SayHello hello=driversIterator.next();
            hello.say();
        }
    }


如果com.mySpi.SayHello裡內容,那麼將無法列印hello-world,SPI簡單入門就到這裡了。