詳解java實踐SPI機制及淺析源碼
1.概念
正式步入今天的核心內容之前,溪源先給大家介紹一下關于SPI機制的相關概念,最后會提供實踐源代碼。
SPI即Service Provider Interface,屬于JDK內置的一種動態(tài)的服務提供發(fā)現(xiàn)機制,可以理解為運行時動態(tài)加載接口的實現(xiàn)類。更甚至,大家可以將SPI機制與設計模式中的策略模式建立聯(lián)系。
SPI機制:

從上圖中理解SPI機制:標準化接口+策略模式+配置文件;
SPI機制核心思想:系統(tǒng)設計的各個抽象,往往有很多不同的實現(xiàn)方案,在面向的對象的設計里,一般推薦模塊之間基于接口編程,模塊之間不對實現(xiàn)類進行硬編碼。一旦代碼里涉及具體的實現(xiàn)類,就違反了可拔插的原則,如果需要替換一種實現(xiàn),就需要修改代碼。為了實現(xiàn)在模塊裝配的時候能不在程序里動態(tài)指明,這就需要一種服務發(fā)現(xiàn)機制
使用場景:
- 1.數(shù)據(jù)庫驅動加載:面對不同廠商的數(shù)據(jù)庫,JDBC需要加載不同類型的數(shù)據(jù)庫驅動;
- 2.日志接口實現(xiàn):SLF4J加載不同日志實現(xiàn)類;
- 3.溪源在實際開發(fā)中也使用了SPI機制:面對不同儀器平臺的結果文件上傳需要解析具體的結果,文件不同,解析邏輯不同,因此采用SPI機制能夠解耦和降低維護成本;
SPI機制使用約定:
從上面的圖中,我們可以清晰的知道SPI的三部分:接口+實現(xiàn)類+配置文件;因此,項目中若要利用SPI機制,則需要遵循以下約定:
- 當服務提供者提供了接口的一種具體實現(xiàn)后,在jar包的META-INF/services目錄下創(chuàng)建一個以“接口全限定名”為命名的文件,內容為實現(xiàn)類的全限定名。
- 主程序通過java.util.ServiceLoder動態(tài)裝載實現(xiàn)模塊,它通過掃描META-INF/services目錄下的配置文件找到實現(xiàn)類的全限定名,把類加載到JVM;
注意:除SPI,我還發(fā)布了最新Java架構項目實戰(zhàn)教程+大廠面試題庫, 點擊此處免費獲取,小白勿進!
2.實踐
整體包結構如圖:

新建標準化接口:
public interface SayService {
void say(String word);
}
建立兩個實現(xiàn)類
@Service
public class ASayServiceImpl implements SayService {
@Override
public void say(String word) {
System.out.println(word + " A say: I am a boy");
}
}
@Service
public class BSayServiceImpl implements SayService {
@Override
public void say(String word) {
System.out.println(word + " B say: I am a girl");
}
}
新建META-INF/services目錄和配置文件(以接口全限定名)
配置文件內容為實現(xiàn)類全限定名
com.qxy.spi.impl.ASayServiceImpl com.qxy.spi.impl.BSayServiceImpl
單測
@SpringBootTest
@RunWith(SpringRunner.class)
public class SpiTest {
static ServiceLoader<SayService> services = ServiceLoader.load(SayService.class);
@Test
public void test1() {
for (SayService sayService : services) {
sayService.say("Hello");
}
}
}
結果
Hello A say: I am a boy
Hello B say: I am a girl
3.源碼
源碼主要加載流程如下:
應用程序調用ServiceLoader.load方法 ServiceLoader.load方法內先創(chuàng)建一個新的ServiceLoader,并實例化該類中的成員變量;
- loader(ClassLoader類型,類加載器)
- acc(AccessControlContext類型,訪問控制器)
- providers(LinkedHashMap<String,S>類型,用于緩存加載成功的類)
- lookupIterator(實現(xiàn)迭代器功能)
應用程序通過迭代器接口獲取對象實例 ServiceLoader先判斷成員變量providers對象中(LinkedHashMap<String,S>類型)是否有緩存實例對象,如果有緩存,直接返回。如果沒有緩存,執(zhí)行類的裝載。
- 讀取META-INF/services/下的配置文件,獲得所有能被實例化的類的名稱,值得注意的是,ServiceLoader可以跨越jar包獲取META-INF下的配置文件;
- 通過反射方法Class.forName()加載類對象,并用instance()方法將類實例化。
- 把實例化后的類緩存到providers對象中,(LinkedHashMap<String,S>類型) 然后返回實例對象。
public final class ServiceLoader<S>
implements Iterable<S>
{
// 加載具體實現(xiàn)類信息的前綴
private static final String PREFIX = "META-INF/services/";
// 需要加載的接口
// The class or interface representing the service being loaded
private final Class<S> service;
// 用于加載的類加載器
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// 創(chuàng)建ServiceLoader時采用的訪問控制上下文
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// 用于緩存已經(jīng)加載的接口實現(xiàn)類,其中key為實現(xiàn)類的完整類名
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 用于延遲加載接口的實現(xiàn)類
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
private static void fail(Class<?> service, String msg, Throwable cause)
throws ServiceConfigurationError
{
throw new ServiceConfigurationError(service.getName() + ": " + msg,
cause);
}
private static void fail(Class<?> service, String msg)
throws ServiceConfigurationError
{
throw new ServiceConfigurationError(service.getName() + ": " + msg);
}
private static void fail(Class<?> service, URL u, int line, String msg)
throws ServiceConfigurationError
{
fail(service, u + ":" + line + ": " + msg);
}
// Parse a single line from the given configuration file, adding the name
// on the line to the names list.
//具體解析資源文件中的每一行內容
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
List<String> names)
throws IOException, ServiceConfigurationError
{
String ln = r.readLine();
if (ln == null) {
//-1表示解析完成
return -1;
}
// 如果存在'#'字符,截取第一個'#'字符串之前的內容,'#'字符之后的屬于注釋內容
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
//不合法的標識:' '、'\t'
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
//判斷第一個 char 是否一個合法的 Java 起始標識符
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
//判斷所有其他字符串是否屬于合法的Java標識符
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
//不存在則緩存
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
return lc + 1;
}
private Iterator<String> parse(Class<?> service, URL u)
throws ServiceConfigurationError
{
InputStream in = null;
BufferedReader r = null;
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
return names.iterator();
}
// Private inner class implementing fully-lazy provider lookup
//
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
// 加載資源的URL集合
Enumeration<URL> configs = null;
// 需加載的實現(xiàn)類的全限定類名的集合
Iterator<String> pending = null;
// 下一個需要加載的實現(xiàn)類的全限定類名
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 {
// 資源名稱,META-INF/services + 全限定名
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);
}
}
// 從資源中解析出需要加載的所有實現(xiàn)類的全限定名
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
//下一個需要加載的實現(xiàn)類全限定名
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//反射構造Class實例
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
// 類型判斷,校驗實現(xiàn)類必須與當前加載的類/接口的關系是派生或相同,否則拋出異常終止
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
//強轉
S p = service.cast(c.newInstance());
// 實例完成,添加緩存,Key:實現(xiàn)類全限定類名,Value:實現(xiàn)類實例
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
// 返回ServiceLoader的實例
return new ServiceLoader<>(service, loader);
}
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}
public String toString() {
return "java.util.ServiceLoader[" + service.getName() + "]";
}
}
4.總結
SPI機制在實際開發(fā)中使用得場景也有很多。特別是統(tǒng)一標準的不同廠商實現(xiàn),溪源也正是利用SPI機制(但略做改進,避免過多加載資源浪費)實現(xiàn)不同技術平臺的結果文件解析需求。
優(yōu)點
使用Java SPI機制的優(yōu)勢是實現(xiàn)解耦,使得第三方服務模塊的裝配控制的邏輯與調用者的業(yè)務代碼分離,而不是耦合在一起。應用程序可以根據(jù)實際業(yè)務情況啟用框架擴展或替換框架組件。
缺點
雖然ServiceLoader也算是使用的延遲加載,但是基本只能通過遍歷全部獲取,也就是接口的實現(xiàn)類全部加載并實例化一遍。如果你并不想用某些實現(xiàn)類,它也被加載并實例化了,這就造成了浪費。
源碼傳送門:SPI Service
到此這篇關于詳解java實踐SPI機制及淺析源碼的文章就介紹到這了,更多相關java SPI機制內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Springboot實現(xiàn)全局自定義異常的方法詳解
這篇文章主要介紹了Springboot實現(xiàn)全局自定義異常的方法詳解,SpringBoot的項目已經(jīng)對有一定的異常處理了,但是對于我們開發(fā)者而言可能就不太合適了,因此我們需要對這些異常進行統(tǒng)一的捕獲并處理,需要的朋友可以參考下2023-11-11
Java中Collections.emptyList()的注意事項
這篇文章主要給大家介紹了關于Java中Collections.emptyList()的注意事項,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-03-03
SpringCloud-Hystrix-Dashboard客戶端服務監(jiān)控的實現(xiàn)方法
這篇文章主要介紹了SpringCloud-Hystrix-Dashboard客戶端服務監(jiān)控的實現(xiàn)方法,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-03-03
基于Beanutils.copyProperties()的用法及重寫提高效率
這篇文章主要介紹了Beanutils.copyProperties( )的用法及重寫提高效率的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09
java 中Comparable與Comparator詳解與比較
這篇文章主要介紹了java 中Comparable與Comparator詳解與比較的相關資料,需要的朋友可以參考下2017-04-04
spring-boot-maven-plugin 插件的作用詳解
添加了spring-boot-maven-plugin插件后,當運行maven打包的命令,項目會被打包成一個可以直接運行的jar包,使用"java -jar"可以直接運行。這篇文章主要給大家介紹spring-boot-maven-plugin 插件的作用,感興趣的朋友一起看看吧2018-10-10

