首頁 > 軟體

Java深入講解SPI的使用

2022-06-16 10:02:11

什麼是Java SPI

    SPI的全名為:Service Provider Interface。在java.util.ServiceLoader的檔案裡有比較詳細的介紹。簡單的總結下 Java SPI 機制的思想。我們系統裡抽象的各個模組,往往有很多不同的實現方案,比如紀錄檔模組的方案,xml解析模組、jdbc模組的方案等。面向的物件的設計裡,我們一般推薦模組之間基於介面程式設計,模組之間不對實現類進行寫死。一旦程式碼裡涉及具體的實現類,就違反了可拔插的原則,如果需要替換一種實現,就需要修改程式碼。為了實現在模組裝配的時候能不在程式裡動態指明,這就需要一種服務發現機制。

    Java SPI 就是提供這樣的一個機制:為某個介面尋找服務實現的機制。有點類似IOC的思想,就是將裝配的控制權移到程式之外,在模組化設計中這個機制尤其重要Java SPI 的具體約定為:當服務的提供者,提供了服務介面的一種實現之後,在jar包的META-INF/services/目錄裡同時建立一個以服務介面命名的檔案。該檔案裡就是實現該服務介面的具體實現類。而當外部程式裝配這個模組的時候,就能通過該jar包META-INF/services/裡的組態檔找到具體的實現類名,並裝載範例化,完成模組的注入。基於這樣一個約定就能很好的找到服務介面的實現類,而不需要再程式碼裡制定。jdk提供服務實現查詢的一個工具類:java.util.ServiceLoader。

Java SPI使用demo

定義一個介面:

package com.hiwei.spi.demo;
public interface Animal {
    void speak();
}

建立兩個實現類:

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目錄下建立META-INF/services目錄:

建立以介面類路徑命名的檔案,檔案中新增實現類路徑:

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){
    	//會根據檔案找到對應的實現類
        ServiceLoader<Animal> load = ServiceLoader.load(Animal.class);
        //執行實現類方法
        for (Animal animal : load) {
            animal.speak();
        }
    }
}

執行結果:

上面我們可以看到java spi會幫助我們找到介面實現類。那麼實際生產中怎麼使用呢? 將上面的程式碼打成jar,然後在其它專案中引入,同樣的目錄下建立檔案,並寫上自己實現類的路徑:

本專案實現類:

package com.example.demo;
import com.hiwei.spi.demo.Animal;
public class Pig implements Animal {
    @Override
    public void speak() {
        System.out.println("哼哼哼!");
    }
}

程式碼中,我們呼叫jar中的main方法:

package com.example.demo;
import com.hiwei.spi.SpiDemoApplication;
public class DemoApplication {
    public static void main(String[] args) {
        SpiDemoApplication.main(args);
    }
}

執行結果:

可以看見自定義的實現類也被執行了。在實際生產中,我們就可以使用java spi面向介面程式設計,實現可插拔。

SPI在JDBC中的應用

以最新的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連線資料庫時,只需要使用:

DriverManager.getConnection("url", "username", "password");

DriverManager有靜態方法:

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

看下loadInitialDrivers()方法,其中有:

AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
				//獲取Driver.class的實現類
                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;
            }
        });

可以看見,會根據java spi獲取Driver.class的實現類,可以在mysql-connector-java-8.0.27.jar下面看到,定義的檔案:

程式會根據檔案找到對應的實現類,並連線資料庫。

SPI在sharding-jdbc中的應用

    sharding-jdbc是一款用於分庫分表的中介軟體,在資料庫分散式場景中,對於主鍵生成要保證唯一性,主鍵生成策略有很多種實現。sharding-jsbc在主鍵生成上就使用了SPI進行擴充套件。

下面看下sharding-jdbc原始碼在主鍵生成上是怎麼應用的: 原始碼中的 ShardingRule.class主要封裝分庫分表的策略規則,包括主鍵生成。看下createDefaultKeyGenerator方法:

//生成預設主鍵生成策略
private ShardingKeyGenerator createDefaultKeyGenerator(final KeyGeneratorConfiguration keyGeneratorConfiguration) {
     //SPI服務發現
     ShardingKeyGeneratorServiceLoader serviceLoader = new ShardingKeyGeneratorServiceLoader();
     return containsKeyGeneratorConfiguration(keyGeneratorConfiguration)
             ? serviceLoader.newService(keyGeneratorConfiguration.getType(), keyGeneratorConfiguration.getProperties()) : 					serviceLoader.newService();
    }

繼續看ShardingKeyGeneratorServiceLoader(),有靜態程式碼塊註冊:

    static {
        //SPI: 載入主鍵生成策略
        NewInstanceServiceLoader.register(ShardingKeyGenerator.class);
    }

看下register方法:

    public static <T> void register(final Class<T> service) {
    	//服務發現
        for (T each : ServiceLoader.load(service)) {
            registerServiceClass(service, each);
        }
    }

看到這,真相大白,就是應用java spi機制。

我們再看下resources目錄下:

可以看到有對應介面命名的檔案,檔案內容:

有兩個實現,分別是雪花演演算法和UUID,這也對應了sharding-jdbc的提供的兩種生成策略。我們在使用sharding-jdbc時,也可以自定義策略,便於擴充套件。 sharding-jdbc對於SPI的使用點還有很多,這裡就不一一列舉了。對於SPI機制,我們在工作中也可以實際應用,提升程式的可延伸性。

擴充套件

以上是Java SPI的解析。其實SPI機制在很多地方都有用到,只是以不同的形式應用,具體的實現略有不同。例如dubbo中也有類似的spi機制;springboot的自動裝配,也使用了spi機制:

springboot自動裝配:

定義檔案:

檔案中宣告需要發現的類:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.hiwei.valve.ValveAutoConfiguration

springboot的掃描檔案,裝配對應的類:

	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開發中是個很重要的設計,所以我們一定要熟練掌握。

到此這篇關於Java深入講解SPI的使用的文章就介紹到這了,更多相關Java SPI內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com