Java的SPI機制入門

一:SPI簡單介紹

最近在網上看到了java中SPI這個技術,據瞭解在JDBC,JNDI,日誌門面,Dubbo等很多技術中都有使用,因此決定學習一下.

    SPI(service provider interface:服務提供者接口),爲接口尋找服務實現類,編程時針對接口編程,由具體的服務提供商提供接口的實現(例如jdbc服務的具體實現可以是oracle,mysql等)
     java spi的具體使用如下 :
當服務的提供者,提供了服務接口的一種實現之後,在jar包的META-INF/services/目錄裏同時創建一個以服務接口命名的文件。該文件裏就是實現該服務接口的具體實現類。而當外部程序裝配這個模塊的時候,就能通過java.util.ServiceLoader解析該jar包META-INF/services/裏的配置文件找到具體的實現類名,並裝載實例化,完成模塊的注入。

二:SPI舉例

示例工程結構如下:
在這裏插入圖片描述
具體代碼如下:
服務接口:

package com.tdemo.api;

public interface TestSpi
{
	String sayHello();
}

接口實現類:

package com.tdemo.api.impl;

import com.tdemo.api.TestSpi;

public class TestSpiImpl implements TestSpi
{
	@Override
	public String sayHello()
	{
		return "HELLO";
	}
}

META-INF下的services文件夾下:
創建文件以服務接口的全類名命名:
com.tdemo.api.TestSpi
內部內容是接口的實現類全類名:
com.tdemo.api.impl.TestSpiImpl

測試代碼:

package com.tdemo.test;

import com.tdemo.api.TestSpi;

import java.util.Iterator;
import java.util.ServiceLoader;

public class Test
{
	public static void main(String[] args)
	{
		ServiceLoader<TestSpi> service = ServiceLoader.load(TestSpi.class);
		Iterator<TestSpi> its =  service.iterator();
		while (its.hasNext()) {
			TestSpi operation = its.next();
			System.out.println(operation.sayHello());
		}
	}
}

執行結果:
在這裏插入圖片描述
說明實現類的確執行了

三:JDBC中如何使用SPI

mysql數據庫實例中有一個庫:test,其中有一個表:user

在這裏插入圖片描述
使用jdbc連接代碼如下:

package com.tdemo.spiofjdbc.test;

import com.tdemo.spiofjdbc.model.User;

import java.sql.*;

public class Test
{
	public static void main(String[] args) throws SQLException
	{
		Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");
		PreparedStatement preparedStatement = connection.prepareStatement("select * from user where id = 1");
		ResultSet resultSet = preparedStatement.executeQuery();
		while (resultSet != null && resultSet.next())
		{
			int age = resultSet.getInt("age");
			System.out.println(age);
		}
	}
}

注意到連接數據庫的這行代碼:

Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");

相對於以前的Class.forname(數據庫驅動);簡單的多
這裏面就用到了SPI的機制,這是JDBC 4.0規範引入的(JDK6),這時候開始可以自動加載jdbc的驅動了

下面從源碼的角度理解如何做到的:
DriverManager調用getConnection方法,所有會先加載DriverManager,靜態變量初始化,靜態代碼塊會執行:
在DriverManager中有以下靜態代碼塊

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

靜態變量:

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

loadInitialDrivers();方法如下:

private static void loadInitialDrivers() {
	    
		// 讀取jvm的系統變量:jdbc.drivers,驅動的字符串名稱
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        
		// SPI自動加載驅動的關鍵代碼
        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);
         
		// 通過Class.forName顯示加載jvm系統配置的驅動:jdbc.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);
            }
        }
    }

先來看看SPI自動加載那段:

	// SPI自動加載驅動的關鍵代碼
        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;
            }
        });

在ServiceLoader類中可以看到以下變量,加載服務的路徑,後面會用到
在這裏插入圖片描述
ServiceLoader.load(Driver.class);中主要做了以下事情:

  1. 將Driver.class和系統類加載器初始化到以下迭代器中
    // The current lazy-lookup iterator
    private LazyIterator lookupIterator;
  2. 清空以下緩存(存放驅動)
    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
    
public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }
private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }

 public void reload() {
        providers.clear(); // 清空當前驅動緩存
        lookupIterator = new LazyIterator(service, loader); // 初始化迭代器
   }

以上可以說是準備工作,真正加載驅動的代碼如下:
Iterator driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}

public Iterator<S> iterator() {
        return new Iterator<S>() {
            // 初始驅動緩存肯定是空的
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();
           
            // 判斷是否包含下一個驅動
            public boolean hasNext() {
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }

           // 獲取下一個驅動
            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }
           
            
            public void remove() {
                throw new UnsupportedOperationException();
            }

        };
    }

以下代碼循環加載驅動:

while(driversIterator.hasNext()) {
driversIterator.next();
}

當執行driversIterator.hasNext()時
// 判斷是否包含下一個驅動
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}

初始加載時knownProviders.hasNext()爲false,所以執行lookupIterator.hasNext()

  public boolean hasNext() {
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

代理到lookupIterator的 hasNextService();這個地方會真正去加載驅動,初始時LazyIterator的nextName爲空,configs(驅動名稱枚舉)爲空,所以會執行 String fullName = PREFIX + service.getName();其中PREFIX 在ServiceLoader中:META-INF/services,顯然此處fullName = META-INF/services/java.sql.Driver,執行完成後中存儲了驅動的全名稱,在下面這句
pending = parse(service, configs.nextElement());把所有驅動名加載到ArrayList中,通過迭代器訪問

 private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } 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;
        }

回到以下代碼:在上面服務實現類名全都通過迭代器訪問,hasNext返回true,訪問一次,都將類型賦值給nextName,可以看到下文實際還是通過 c = Class.forName(cn, false, loader);來加載,然後創建實例存入緩存:
S p = service.cast(c.newInstance());
providers.put(cn, p);

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
        }

到此可以說驅動加載完成

總的來說:
這行代碼初始化了 ServiceLoader內部的迭代器lookupIterator,它使用了適配器模式
ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
// 把訪問代理到lookupIterator上
Iterator driversIterator = loadedDrivers.iterator();

try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
            // Do nothing
 }

lookupIterator的hasNext每執行一次就是讀取一個驅動名賦值給nextName,並返回true,接下來
lookupIterator的next就在加載每一個nextName對應的驅動,並存入緩存:
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章