Java SPI機制詳解

aoho發表於2019-03-03

什麼是SPI?

SPI 全稱為 (Service Provider Interface) ,是JDK內建的一種服務提供發現機制。SPI是一種動態替換發現的機制, 比如有個介面,想執行時動態的給它新增實現,你只需要新增一個實現。我們經常遇到的就是java.sql.Driver介面,其他不同廠商可以針對同一介面做出不同的實現,mysql和postgresql都有不同的實現提供給使用者,而Java的SPI機制可以為某個介面尋找服務實現。

Java SPI機制詳解

類圖中,介面對應定義的抽象SPI介面;實現方實現SPI介面;呼叫方依賴SPI介面。

SPI介面的定義在呼叫方,在概念上更依賴呼叫方;組織上位於呼叫方所在的包中;實現位於獨立的包中。

當介面屬於實現方的情況,實現方提供了介面和實現,這個用法很常見,屬於API呼叫。我們可以引用介面來達到呼叫某實現類的功能。

Java SPI 應用例項

當服務的提供者提供了一種介面的實現之後,需要在classpath下的META-INF/services/目錄裡建立一個以服務介面命名的檔案,這個檔案裡的內容就是這個介面的具體的實現類。當其他的程式需要這個服務的時候,就可以通過查詢這個jar包(一般都是以jar包做依賴)的META-INF/services/中的配置檔案,配置檔案中有介面的具體實現類名,可以根據這個類名進行載入例項化,就可以使用該服務了。JDK中查詢服務實現的工具類是:java.util.ServiceLoader。

SPI介面

public interface ObjectSerializer {

    byte[] serialize(Object obj) throws ObjectSerializerException;

    <T> T deSerialize(byte[] param, Class<T> clazz) throws ObjectSerializerException;

    String getSchemeName();
}
複製程式碼

定義了一個物件序列化介面,內有三個方法:序列化方法、反序列化方法和序列化名稱。

SPI具體實現

public class KryoSerializer implements ObjectSerializer {

    @Override
    public byte[] serialize(Object obj) throws ObjectSerializerException {
        byte[] bytes;
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {
            //獲取kryo物件
            Kryo kryo = new Kryo();
            Output output = new Output(outputStream);
            kryo.writeObject(output, obj);
            bytes = output.toBytes();
            output.flush();
        } catch (Exception ex) {
            throw new ObjectSerializerException("kryo serialize error" + ex.getMessage());
        } finally {
            try {
                outputStream.flush();
                outputStream.close();
            } catch (IOException e) {

            }
        }
        return bytes;
    }

    @Override
    public <T> T deSerialize(byte[] param, Class<T> clazz) throws ObjectSerializerException {
        T object;
        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(param)) {
            Kryo kryo = new Kryo();
            Input input = new Input(inputStream);
            object = kryo.readObject(input, clazz);
            input.close();
        } catch (Exception e) {
            throw new ObjectSerializerException("kryo deSerialize error" + e.getMessage());
        }
        return object;
    }

    @Override
    public String getSchemeName() {
        return "kryoSerializer";
    }

}
複製程式碼

使用Kryo的序列化方式。Kryo 是一個快速高效的Java物件圖形序列化框架,它原生支援java,且在java的序列化上甚至優於google著名的序列化框架protobuf。

public class JavaSerializer implements ObjectSerializer {
    @Override
    public byte[] serialize(Object obj) throws ObjectSerializerException {
        ByteArrayOutputStream arrayOutputStream;
        try {
            arrayOutputStream = new ByteArrayOutputStream();
            ObjectOutput objectOutput = new ObjectOutputStream(arrayOutputStream);
            objectOutput.writeObject(obj);
            objectOutput.flush();
            objectOutput.close();
        } catch (IOException e) {
            throw new ObjectSerializerException("JAVA serialize error " + e.getMessage());
        }
        return arrayOutputStream.toByteArray();
    }

    @Override
    public <T> T deSerialize(byte[] param, Class<T> clazz) throws ObjectSerializerException {
        ByteArrayInputStream arrayInputStream = new ByteArrayInputStream(param);
        try {
            ObjectInput input = new ObjectInputStream(arrayInputStream);
            return (T) input.readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new ObjectSerializerException("JAVA deSerialize error " + e.getMessage());
        }
    }

    @Override
    public String getSchemeName() {
        return "javaSerializer";
    }

}
複製程式碼

Java原生的序列化方式。

增加META-INF目錄檔案

Resource下面建立META-INF/services 目錄裡建立一個以服務介面命名的檔案

Java SPI機制詳解
com.blueskykong.javaspi.serializer.KryoSerializer
com.blueskykong.javaspi.serializer.JavaSerializer
複製程式碼

Service類

@Service
public class SerializerService {


    public ObjectSerializer getObjectSerializer() {
        ServiceLoader<ObjectSerializer> serializers = ServiceLoader.load(ObjectSerializer.class);

        final Optional<ObjectSerializer> serializer = StreamSupport.stream(serializers.spliterator(), false)
                .findFirst();

        return serializer.orElse(new JavaSerializer());
    }
}
複製程式碼

獲取定義的序列化方式,且只取第一個(我們在配置中寫了兩個),如果找不到則返回Java原生序列化方式。

測試類

    @Autowired
    private SerializerService serializerService;

    @Test
    public void serializerTest() throws ObjectSerializerException {
        ObjectSerializer objectSerializer = serializerService.getObjectSerializer();
        System.out.println(objectSerializer.getSchemeName());
        byte[] arrays = objectSerializer.serialize(Arrays.asList("1", "2", "3"));
        ArrayList list = objectSerializer.deSerialize(arrays, ArrayList.class);
        Assert.assertArrayEquals(Arrays.asList("1", "2", "3").toArray(), list.toArray());
    }
複製程式碼

測試用例通過,且輸出kryoSerializer

SPI的用途

資料庫DriverManager、Spring、ConfigurableBeanFactory等都用到了SPI機制,這裡以資料庫DriverManager為例,看一下其實現的內幕。

DriverManager是jdbc裡管理和註冊不同資料庫driver的工具類。針對一個資料庫,可能會存在著不同的資料庫驅動實現。我們在使用特定的驅動實現時,不希望修改現有的程式碼,而希望通過一個簡單的配置就可以達到效果。
在使用mysql驅動的時候,會有一個疑問,DriverManager是怎麼獲得某確定驅動類的?我們在運用Class.forName(“com.mysql.jdbc.Driver”)載入mysql驅動後,就會執行其中的靜態程式碼把driver註冊到DriverManager中,以便後續的使用。

在JDBC4.0之前,連線資料庫的時候,通常會用Class.forName("com.mysql.jdbc.Driver")這句先載入資料庫相關的驅動,然後再進行獲取連線等的操作。而JDBC4.0之後不需要Class.forName來載入驅動,直接獲取連線即可,這裡使用了Java的SPI擴充套件機制來實現。

在java中定義了介面java.sql.Driver,並沒有具體的實現,具體的實現都是由不同廠商來提供的。

mysql

在mysql-connector-java-5.1.45.jar中,META-INF/services目錄下會有一個名字為java.sql.Driver的檔案:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
複製程式碼

pg

而在postgresql-42.2.2.jar中,META-INF/services目錄下會有一個名字為java.sql.Driver的檔案:

org.postgresql.Driver
複製程式碼

用法

String url = "jdbc:mysql://localhost:3306/test";
Connection conn = DriverManager.getConnection(url,username,password);
複製程式碼

上面展示的是mysql的用法,pg用法也是類似。不需要使用Class.forName("com.mysql.jdbc.Driver")來載入驅動。

Mysql DriverManager實現

上面程式碼沒有了載入驅動的程式碼,我們怎麼去確定使用哪個資料庫連線的驅動呢?這裡就涉及到使用Java的SPI擴充套件機制來查詢相關驅動的東西了,關於驅動的查詢其實都在DriverManager中,DriverManager是Java中的實現,用來獲取資料庫連線,在DriverManager中有一個靜態程式碼塊如下:

static {
	loadInitialDrivers();
	println("JDBC DriverManager initialized");
}
複製程式碼

可以看到其內部的靜態程式碼塊中有一個loadInitialDrivers方法,loadInitialDrivers用法用到了上文提到的spi工具類ServiceLoader:

    public Void run() {

        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();
            }
        } catch(Throwable t) {
        // Do nothing
        }
        return null;
    }
複製程式碼

遍歷使用SPI獲取到的具體實現,例項化各個實現類。在遍歷的時候,首先呼叫driversIterator.hasNext()方法,這裡會搜尋classpath下以及jar包中所有的META-INF/services目錄下的java.sql.Driver檔案,並找到檔案中的實現類的名字,此時並沒有例項化具體的實現類。

總結

SPI機制在實際開發中使用得場景也有很多。特別是統一標準的不同廠商實現,當有關組織或者公司定義標準之後,具體廠商或者框架開發者實現,之後提供給開發者使用。

本文程式碼: https://github.com/keets2012/Spring-Boot-Samples/tree/master/java-spi

訂閱最新文章,歡迎關注我的公眾號

微信公眾號

參考

  1. Java中SPI機制深入及原始碼解析
  2. Java SPI思想梳理

相關文章