Java SPI機制分析

SPI概述

SPI全稱爲(Service Provider Interface) ,是JDK內置的一種服務提供發現機制;主要被框架的開發人員使用,好比java.sql.Driver接口,數據庫廠商實現此接口便可,固然要想讓系統知道具體實現類的存在,還須要使用固定的存放規則,須要在classpath下的META-INF/services/目錄裏建立一個以服務接口命名的文件,這個文件裏的內容就是這個接口的具體的實現類;下面以JDBC爲實例來進行具體的分析。java

JDBC驅動

1.準備驅動包

<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>42.2.2</version>
        </dependency>
        <dependency>
            <groupId>com.microsoft.sqlserver</groupId>
            <artifactId>mssql-jdbc</artifactId>
            <version>7.0.0.jre8</version>
        </dependency>

分別準備了mysql,postgresql和sqlserver,能夠打開jar,發現每一個jar包的META-INF/services/都存在一個java.sql.Driver文件,文件裏面存在一個或多個類名,好比mysql:mysql

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

提供的每一個驅動類佔據一行,解析的時候會按行讀取,具體使用哪一個會根據url來決定;git

2.簡單實例

String url = "jdbc:mysql://localhost:3306/db3";
String username = "root";
String password = "root";
String sql = "update travelrecord set name=\'bbb\' where id=1";
Connection con = DriverManager.getConnection(url, username, password);

類路徑下存在多個驅動包,具體在使用DriverManager.getConnection應該使用哪一個驅動類會解析url來識別,不一樣的數據庫有不一樣的url前綴;github

3.驅動類加載分析

具體META-INF/services/下的驅動類是何時加載的,DriverManager有一個靜態代碼塊:sql

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}
 
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() {
 
            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;
        }
    });
 
    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);
        }
    }
}

在加載DriverManager類的時候會執行loadInitialDrivers方法,方法內經過了兩種加載驅動類的方式,分別是:使用系統變量方式和ServiceLoader加載方式;系統變量方式其實就是在變量jdbc.drivers中配置好驅動類,而後使用Class.forName進行加載;下面重點看一下ServiceLoader方式,此處調用了load方法可是並無真正去加載驅動類,而是返回了一個LazyIterator,後面的代碼就是循環變量迭代器:數據庫

private static final String PREFIX = "META-INF/services/";
 
private class LazyIterator
        implements Iterator<S>
    {
 
        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;
 
        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }
 
        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;
        }
 
        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
        }
        ......
    }

類中指定了一個靜態常量PREFIX = 「META-INF/services/」,而後和java.sql.Driver拼接組成了fullName,而後經過類加載器去獲取全部類路徑下java.sql.Driver文件,獲取以後存放在configs中,裏面的每一個元素對應一個文件,每一個文件中可能會存在多個驅動類,因此使用pending用來存放每一個文件中的驅動信息,獲取驅動信息以後在nextService中使用Class.forName加載類信息,而且指定不進行初始化;同時在下面使用newInstance對驅動類進行了實例化操做;每一個驅動類中都提供了一個靜態註冊代碼塊,好比mysql:app

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

這裏又實例化了一個驅動類,同時註冊到DriverManager;接下來就是調用DriverManager的getConnection方法,代碼以下:框架

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

此方法主要是遍歷以前註冊的DriverInfo,拿着url信息去每一個驅動類中創建鏈接,固然每一個驅動類中都會進行url匹配校驗,成功以後返回Connection,若是中途有失敗的鏈接並不影響嘗試新的驅動鏈接,遍歷完以後仍是沒法獲取鏈接,則拋出異常;ide

4.擴展

若是想擴展新的驅動類也很簡單,只須要在類路徑下建立META-INF/services/文件夾,同時在裏面建立java.sql.Driver文件,在文件中寫入具體的驅動類名稱,固然此類須要繼承java.sql.Driver接口類;例如實例中提供的TestDriver。sqlserver

序列化實戰

1.準備接口類

public interface Serialization {
 
    /**
     * 序列化
     * 
     * @param obj
     * @return
     */
    public byte[] serialize(Object obj) throws Exception;
 
    /**
     * 反序列化
     * 
     * @param param
     * @param clazz
     * @return
     * @throws Exception
     */
    public <T> T deserialize(byte[] param, Class<T> clazz) throws Exception;
 
    /**
     * 序列化名稱
     * 
     * @return
     */
    public String getName();
 
}

2.準備實現類

分別準備JsonSerialization和ProtobufSerialization

3.接口文件

在META-INF/services/目錄下建立文件com.spi.serializer.Serialization,內容以下:

com.spi.serializer.JsonSerialization
com.spi.serializer.ProtobufSerialization

4.提供Manager類

public class SerializationManager {
 
    private static Map<String, Serialization> map = new HashMap<>();
 
    static {
        loadInitialSerializer();
    }
 
    private static void loadInitialSerializer() {
        ServiceLoader<Serialization> loadedSerializations = ServiceLoader.load(Serialization.class);
        Iterator<Serialization> iterator = loadedSerializations.iterator();
 
        try {
            while (iterator.hasNext()) {
                Serialization serialization = iterator.next();
                map.put(serialization.getName(), serialization);
            }
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }
 
    public static Serialization getSerialization(String name) {
        return map.get(name);
    }
}

提供相似DriverManager的SerializationManager類,在加載類的時候加載全部配置的序列化方式;提供一個getSerialization的今天方法相似getConnection;

總結

本文以JDBC驅動爲實例,重點對使用ServiceLoader方式服務發現進行分析,同時提供了序列化的簡單實戰;dubbo也提供了相似的SPI方式,核心類是ExtensionLoader,比起java官方提供的ServiceLoader功能更強大,後續繼續分析一下dubbo的SPI方式,而後進行一個對比。

示例代碼地址

https://github.com/ksfzhaohui...
https://gitee.com/OutOfMemory...

相關文章
相關標籤/搜索