SPI全稱爲(Service Provider Interface) ,是JDK內置的一種服務提供發現機制;主要被框架的開發人員使用,好比java.sql.Driver接口,數據庫廠商實現此接口便可,固然要想讓系統知道具體實現類的存在,還須要使用固定的存放規則,須要在classpath下的META-INF/services/目錄裏建立一個以服務接口命名的文件,這個文件裏的內容就是這個接口的具體的實現類;下面以JDBC爲實例來進行具體的分析。java
<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
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
具體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
若是想擴展新的驅動類也很簡單,只須要在類路徑下建立META-INF/services/文件夾,同時在裏面建立java.sql.Driver文件,在文件中寫入具體的驅動類名稱,固然此類須要繼承java.sql.Driver接口類;例如實例中提供的TestDriver。sqlserver
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(); }
分別準備JsonSerialization和ProtobufSerialization
在META-INF/services/目錄下建立文件com.spi.serializer.Serialization,內容以下:
com.spi.serializer.JsonSerialization com.spi.serializer.ProtobufSerialization
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...