Java深入講解SPI的使用
什么是Java SPI
SPI的全名為:Service Provider Interface。在java.util.ServiceLoader的文檔里有比較詳細的介紹。簡單的總結(jié)下 Java SPI 機制的思想。我們系統(tǒng)里抽象的各個模塊,往往有很多不同的實現(xiàn)方案,比如日志模塊的方案,xml解析模塊、jdbc模塊的方案等。面向的對象的設(shè)計里,我們一般推薦模塊之間基于接口編程,模塊之間不對實現(xiàn)類進行硬編碼。一旦代碼里涉及具體的實現(xiàn)類,就違反了可拔插的原則,如果需要替換一種實現(xiàn),就需要修改代碼。為了實現(xiàn)在模塊裝配的時候能不在程序里動態(tài)指明,這就需要一種服務(wù)發(fā)現(xiàn)機制。
Java SPI 就是提供這樣的一個機制:為某個接口尋找服務(wù)實現(xiàn)的機制。有點類似IOC的思想,就是將裝配的控制權(quán)移到程序之外,在模塊化設(shè)計中這個機制尤其重要Java SPI 的具體約定為:當服務(wù)的提供者,提供了服務(wù)接口的一種實現(xiàn)之后,在jar包的META-INF/services/目錄里同時創(chuàng)建一個以服務(wù)接口命名的文件。該文件里就是實現(xiàn)該服務(wù)接口的具體實現(xiàn)類。而當外部程序裝配這個模塊的時候,就能通過該jar包META-INF/services/里的配置文件找到具體的實現(xiàn)類名,并裝載實例化,完成模塊的注入?;谶@樣一個約定就能很好的找到服務(wù)接口的實現(xiàn)類,而不需要再代碼里制定。jdk提供服務(wù)實現(xiàn)查找的一個工具類:java.util.ServiceLoader。
Java SPI使用demo
定義一個接口:
package com.hiwei.spi.demo; public interface Animal { void speak(); }
創(chuàng)建兩個實現(xiàn)類:
package com.hiwei.spi.demo; public class Cat implements Animal { @Override public void speak() { System.out.println("喵喵喵!"); } }
package com.hiwei.spi.demo; public class Dog implements Animal { @Override public void speak() { System.out.println("汪汪汪!"); } }
在resources目錄下創(chuàng)建META-INF/services目錄:
創(chuàng)建以接口類路徑命名的文件,文件中添加實現(xiàn)類路徑:
com.hiwei.spi.demo.Cat
com.hiwei.spi.demo.Dog
使用
package com.hiwei.spi; import com.hiwei.spi.demo.Animal; import java.sql.SQLException; import java.util.ServiceLoader; public class SpiDemoApplication { public static void main(String[] args){ //會根據(jù)文件找到對應(yīng)的實現(xiàn)類 ServiceLoader<Animal> load = ServiceLoader.load(Animal.class); //執(zhí)行實現(xiàn)類方法 for (Animal animal : load) { animal.speak(); } } }
執(zhí)行結(jié)果:
上面我們可以看到j(luò)ava spi會幫助我們找到接口實現(xiàn)類。那么實際生產(chǎn)中怎么使用呢? 將上面的代碼打成jar,然后在其它項目中引入,同樣的目錄下創(chuàng)建文件,并寫上自己實現(xiàn)類的路徑:
本項目實現(xiàn)類:
package com.example.demo; import com.hiwei.spi.demo.Animal; public class Pig implements Animal { @Override public void speak() { System.out.println("哼哼哼!"); } }
代碼中,我們調(diào)用jar中的main方法:
package com.example.demo; import com.hiwei.spi.SpiDemoApplication; public class DemoApplication { public static void main(String[] args) { SpiDemoApplication.main(args); } }
執(zhí)行結(jié)果:
可以看見自定義的實現(xiàn)類也被執(zhí)行了。在實際生產(chǎn)中,我們就可以使用java spi面向接口編程,實現(xiàn)可插拔。
SPI在JDBC中的應(yīng)用
以最新的mysql-connector-java-8.0.27.jar為例
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.27</version> </dependency>
在使用JDBC連接數(shù)據(jù)庫時,只需要使用:
DriverManager.getConnection("url", "username", "password");
DriverManager有靜態(tài)方法:
static { loadInitialDrivers(); println("JDBC DriverManager initialized"); }
看下loadInitialDrivers()方法,其中有:
AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { //獲取Driver.class的實現(xiàn)類 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; } });
可以看見,會根據(jù)java spi獲取Driver.class的實現(xiàn)類,可以在mysql-connector-java-8.0.27.jar下面看到,定義的文件:
程序會根據(jù)文件找到對應(yīng)的實現(xiàn)類,并連接數(shù)據(jù)庫。
SPI在sharding-jdbc中的應(yīng)用
sharding-jdbc是一款用于分庫分表的中間件,在數(shù)據(jù)庫分布式場景中,對于主鍵生成要保證唯一性,主鍵生成策略有很多種實現(xiàn)。sharding-jsbc在主鍵生成上就使用了SPI進行擴展。
下面看下sharding-jdbc源碼在主鍵生成上是怎么應(yīng)用的: 源碼中的 ShardingRule.class主要封裝分庫分表的策略規(guī)則,包括主鍵生成??聪耤reateDefaultKeyGenerator方法:
//生成默認主鍵生成策略 private ShardingKeyGenerator createDefaultKeyGenerator(final KeyGeneratorConfiguration keyGeneratorConfiguration) { //SPI服務(wù)發(fā)現(xiàn) ShardingKeyGeneratorServiceLoader serviceLoader = new ShardingKeyGeneratorServiceLoader(); return containsKeyGeneratorConfiguration(keyGeneratorConfiguration) ? serviceLoader.newService(keyGeneratorConfiguration.getType(), keyGeneratorConfiguration.getProperties()) : serviceLoader.newService(); }
繼續(xù)看ShardingKeyGeneratorServiceLoader(),有靜態(tài)代碼塊注冊:
static { //SPI: 加載主鍵生成策略 NewInstanceServiceLoader.register(ShardingKeyGenerator.class); }
看下register方法:
public static <T> void register(final Class<T> service) { //服務(wù)發(fā)現(xiàn) for (T each : ServiceLoader.load(service)) { registerServiceClass(service, each); } }
看到這,真相大白,就是應(yīng)用java spi機制。
我們再看下resources目錄下:
可以看到有對應(yīng)接口命名的文件,文件內(nèi)容:
有兩個實現(xiàn),分別是雪花算法和UUID,這也對應(yīng)了sharding-jdbc的提供的兩種生成策略。我們在使用sharding-jdbc時,也可以自定義策略,便于擴展。 sharding-jdbc對于SPI的使用點還有很多,這里就不一一列舉了。對于SPI機制,我們在工作中也可以實際應(yīng)用,提升程序的可擴展性。
擴展
以上是Java SPI的解析。其實SPI機制在很多地方都有用到,只是以不同的形式應(yīng)用,具體的實現(xiàn)略有不同。例如dubbo中也有類似的spi機制;springboot的自動裝配,也使用了spi機制:
springboot自動裝配:
定義文件:
文件中聲明需要發(fā)現(xiàn)的類:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hiwei.valve.ValveAutoConfiguration
springboot的掃描文件,裝配對應(yīng)的類:
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) { Map<String, List<String>> result = cache.get(classLoader); if (result != null) { return result; } result = new HashMap<>(); try { //加載文件中的類 Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION); while (urls.hasMoreElements()) { URL url = urls.nextElement(); UrlResource resource = new UrlResource(url); Properties properties = PropertiesLoaderUtils.loadProperties(resource); for (Map.Entry<?, ?> entry : properties.entrySet()) { String factoryTypeName = ((String) entry.getKey()).trim(); String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String) entry.getValue()); for (String factoryImplementationName : factoryImplementationNames) { result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>()) .add(factoryImplementationName.trim()); } } } // Replace all lists with unmodifiable lists containing unique elements result.replaceAll((factoryType, implementations) -> implementations.stream().distinct() .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList))); cache.put(classLoader, result); } catch (IOException ex) { throw new IllegalArgumentException("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex); } return result; }
FACTORIES_RESOURCE_LOCATION的值:
SPI在Java開發(fā)中是個很重要的設(shè)計,所以我們一定要熟練掌握。
到此這篇關(guān)于Java深入講解SPI的使用的文章就介紹到這了,更多相關(guān)Java SPI內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Mybatis-plus(MP)中CRUD操作保姆級筆記
本文主要介紹了Mybatis-plus(MP)中CRUD操作保姆級筆記,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-11-11java自動根據(jù)文件內(nèi)容的編碼來讀取避免亂碼
這篇文章主要介紹了java自動根據(jù)文件內(nèi)容的編碼來讀取避免亂碼,需要的朋友可以參考下2014-02-02MyBatis中resultType和parameterType和resultMap使用總結(jié)
這篇文章主要介紹了MyBatis中resultType和parameterType和resultMap使用總結(jié),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-11-11java?Long類型轉(zhuǎn)為json后數(shù)據(jù)損失精度的處理方式
這篇文章主要介紹了java?Long類型轉(zhuǎn)為json后數(shù)據(jù)損失精度的處理方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-01-01springboot項目數(shù)據(jù)庫密碼如何加密
在我們?nèi)粘i_發(fā)中,我們可能很隨意把數(shù)據(jù)庫密碼直接明文暴露在配置文件中,今天就來聊聊在springboot項目中如何對數(shù)據(jù)庫密碼進行加密,感興趣的可以了解一下2021-07-07