1. 程式人生 > 實用技巧 >Java中的SPI是怎麼一回事

Java中的SPI是怎麼一回事

SPI ,全稱為 Service Provider Interface,是一種服務發現機制。它通過在ClassPath路徑下的META-INF/services資料夾查詢檔案,自動載入檔案裡所定義的類。

我先舉例如何使用java的spi。

1. 首先定義一個服務介面,比如LogService.java

package code.classloader;

/**
 * 
 * @author dgm
 * @describe "日誌服務介面"
 * @date 2020年5月22日
 */
public interface LogService {

    void print(String message);
}

2.再定義三個LogService介面的實現類

package code.classloader;

/**
 * @author dgm
 * @describe "日誌到控制檯"
 * @date 2020年5月22日
 */
public class StdOutLogServiceImpl implements LogService {

	@Override
	public void print(String message) {
		// TODO Auto-generated method stub
        System.out.println(message);
        System.out.println("寫日誌到控制檯!");
	}
}


package code.classloader;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

/**
 * 
 * @author dgm
 * @describe "日誌到檔案"
 * @date 2020年5月22日
 */
public class FileLogServiceImpl implements LogService {

	private static final String FILE_NAME="d://LogService.txt";
	@Override
	public void print(String message) {
		try {
			File file = new File(FILE_NAME);
			FileWriter fw = null;
			// true:表示是追加的標誌
			fw = new FileWriter(file, true);
			fw.write(message+"\n");
			fw.close();

	        System.out.println(message);
			System.out.println("寫日誌入檔案!");
		} catch (IOException e) {
		}
	}
}


package code.classloader;

/**
 * @author dgm
 * @describe "寫日誌入mysql資料庫"
 * @date 2020年5月22日
 */
public class MysqlLogServiceImpl implements LogService {

	@Override
	public void print(String message) {
		// TODO Auto-generated method stub
        System.out.println(message);
        System.out.println("寫日誌入資料庫");
	}
}

注意:我把三個實現類(StdOutLogServiceImpl.java,FileLogServiceImpl,MysqlLogServiceImpl)一個程式碼框裡了

3.在專案src目錄下新建一個META-INF/services資料夾,然後再新建一個以LogService介面的全限定名命名的檔案code.classloader.LogService
,其檔案內容為:

code.classloader.StdOutLogServiceImpl
code.classloader.FileLogServiceImpl
code.classloader.MysqlLogServiceImpl

4.最後我們再新建一個測試類LogClientTest

package code.test;

import java.util.Iterator;
import java.util.ServiceLoader;

import code.classloader.LogService;

/**
 * @author dgm
 * @describe ""
 * @date 2020年5月22日
 */
public class LogClientTest {
	
	 public static void main(String[] args) {
	        ServiceLoader<LogService> loader = ServiceLoader.load(LogService.class);
	        Iterator<LogService> it = loader.iterator();
	        while (it != null && it.hasNext()){
	        	LogService logService = it.next();
	        	logService.print("日誌實現是:= " + logService.getClass());	        
	        }
	    }
}

執行測試類,結果如下圖所示:

5. Java的SPI機制的原始碼分析

從測試類LogClientTest我們看到Java的SPI機制實現跟ServiceLoader這個類有關,那麼我們先來看下ServiceLoader的類結構程式碼:

//注意ServiceLoader類實現了Iterable介面
publicfinalclass ServiceLoader<S>  implements Iterable<S>{
    //這下知道為啥要把案例中約束目錄固定死了吧
    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;
    // The access control context taken when the ServiceLoader is created
    private final AccessControlContext acc;
    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // The current lazy-lookup iterator
    private LazyIterator lookupIterator;
    // 構造方法
    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();
    }
	
    // ...暫時省略相關程式碼
    
    // ServiceLoader的內部類LazyIterator,實現了【Iterator】介面
    // Private inner class implementing fully-lazy provider lookup
    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;
        }
        // 覆寫Iterator介面的hasNext方法
        public boolean hasNext() {
            // ...暫時省略相關程式碼
        }
        // 覆寫Iterator介面的next方法
        public S next() {
            // ...暫時省略相關程式碼
        }
        // 覆寫Iterator介面的remove方法
        public void remove() {
            // ...暫時省略相關程式碼
        }

    }

    // 覆寫Iterable介面的iterator方法,返回一個迭代器
    public Iterator<S> iterator() {
        // ...暫時省略相關程式碼
    }

    // ...暫時省略相關程式碼

}

這下知道為啥要把案例中約束目錄名META-INF/services/固定死了吧。

可以看到,ServiceLoader實現了Iterable介面,覆寫其iterator方法能產生一個迭代器;同時ServiceLoader有一個內部類LazyIterator,而LazyIterator又實現了Iterator介面,說明LazyIterator是一個迭代器。

5.1 ServiceLoader.load方法,為載入服務提供者實現類做前期準備

我們開始探究Java的SPI機制的原始碼, 先來看LogClientTest的第一句程式碼

ServiceLoader<LogService> loader = ServiceLoader.load(LogService.class);

ServiceLoader.load(LogService.class)的原始碼如下:

// ServiceLoader.java
public static <S> ServiceLoader<S> load(Class<S> service) {
    //獲取當前執行緒上下文類載入器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    // 將service介面類和執行緒上下文類載入器作為引數傳入,繼續呼叫load方法
    return ServiceLoader.load(service, cl);
}

我們繼續往下看ServiceLoader.load(service, cl)方法:

// ServiceLoader.java

public static <S> ServiceLoader<S> load(Class<S> service,
                                        ClassLoader loader)
{
    // 將service介面類和執行緒上下文類載入器作為構造引數,新建了一個ServiceLoader物件
    return new ServiceLoader<>(service, loader);
}

繼續接著看new ServiceLoader<>(service, loader)是如何構建的?

// ServiceLoader.java

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

可以看到在構建ServiceLoader物件時除了給其成員屬性賦值外,還呼叫了reload方法:

// ServiceLoader.java

public void reload() {
    providers.clear();

    lookupIterator = new LazyIterator(service, loader);
}

可以看到在reload方法中又新建了一個LazyIterator物件,然後賦值給lookupIterator

// ServiceLoader$LazyIterator.java

private LazyIterator(Class<S> service, ClassLoader loader) {
    this.service = service;
    this.loader = loader;
}

可以看到在構建LazyIterator物件時,也只是給其成員變數serviceloader屬性賦值。

5.2 ServiceLoader.iterator方法,實現服務提供者實現類的懶載入

我們現在再來看LogClientTest的第二句程式碼

Iterator<LogService> it = loader.iterator();

,執行這句程式碼後最終會呼叫serviceLoaderiterator方法:

// serviceLoader.java

public Iterator<S> iterator() {
    return new Iterator<S>() {

        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();

        public boolean hasNext() {
            if (knownProviders.hasNext())
                returntrue;
            // 呼叫lookupIterator即LazyIterator的hasNext方法
            // 可以看到是委託給LazyIterator的hasNext方法來實現
            return lookupIterator.hasNext();
        }

        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            // 呼叫lookupIterator即LazyIterator的next方法
            // 可以看到是委託給LazyIterator的next方法來實現
            return lookupIterator.next();
        }

        public void remove() {
            thrownew UnsupportedOperationException();
        }

    };
}

可以看到呼叫serviceLoaderiterator方法會返回一個匿名的迭代器物件,而這個匿名迭代器物件其實相當於一個門面類,其覆寫的hasNextnext方法又分別委託LazyIteratorhasNextnext方法來實現了。

我們繼續追蹤程式碼,發現接下來會進入LazyIteratorhasNext方法:

// serviceLoader$LazyIterator.java

public boolean hasNext() {
    if (acc == null) {
        // 呼叫hasNextService方法
        return hasNextService();
    } else {
        PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
            public Boolean run() { return hasNextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

然後繼續跟進hasNextService方法:

// serviceLoader$LazyIterator.java

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            // 終於出現約定目錄名了,即PREFIX = "META-INF/services/"
            // service.getName()即介面的全限定名
            // 還記得前面的程式碼構建LazyIterator物件時已經給其成員屬性service賦值嗎
            String fullName = PREFIX + service.getName();
            // 載入META-INF/services/目錄下的介面檔案中的服務提供者類
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                // 還記得前面的程式碼構建LazyIterator物件時已經給其成員屬性loader賦值嗎
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
        // 返回META-INF/services/目錄下的介面檔案中的服務提供者類並賦值給pending屬性
        pending = parse(service, configs.nextElement());
    }
    // 然後取出一個全限定名賦值給LazyIterator的成員變數nextName
    nextName = pending.next();
    return true;
}

可以看到在執行LazyIteratorhasNextService方法時最終將去META-INF/services/目錄下載入介面檔案的內容即載入服務提供者實現類的全限定名,然後取出一個服務提供者實現類的全限定名賦值給LazyIterator的成員變數nextName。到了這裡,我們就明白了LazyIterator的作用真的是懶載入,在用到的時候才會真正去載入服務提供者實現類。

同樣,執行完LazyIteratorhasNext方法後,會繼續執行LazyIteratornext方法:

// serviceLoader$LazyIterator.java

public S next() {
    if (acc == null) {
        // 呼叫nextService方法
        return nextService();
    } else {
        PrivilegedAction<S> action = new PrivilegedAction<S>() {
            public S run() { return nextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

我們繼續跟進nextService方法:

// serviceLoader$LazyIterator.java

private S nextService() {
    if (!hasNextService())
        thrownew NoSuchElementException();
    // 還記得在hasNextService方法中為nextName賦值過服務提供者實現類的全限定名嗎
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        // 【1】去classpath中根據傳入的類載入器和服務提供者實現類的全限定名去載入服務提供者實現類
        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 {
        // 【2】例項化剛才載入的服務提供者實現類,並進行轉換
        S p = service.cast(c.newInstance());
        // 【3】最終將例項化後的服務提供者實現類放進providers集合
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    thrownew Error();          // This cannot happen
}

可以看到LazyIteratornextService方法最終將例項化之前載入的服務提供者實現類,並放進providers集合中,隨後再呼叫服務提供者實現類的方法。注意,這裡是載入一個服務提供者實現類後,若main函式中有呼叫該服務提供者實現類的方法的話,緊接著會呼叫其方法;然後繼續例項化下一個服務提供者類。

因此,我們看到了ServiceLoader.iterator方法真正承擔了載入並例項化META-INF/services/目錄下的介面檔案裡定義的服務提供者實現類。

想了解SpringBoot的SPI機制的樣板,META-INF/spring.factories,算是一種約定,也可以參考下

SpringBoot擴充套件點之EnvironmentPostProcessorhttps://blog.csdn.net/dong19891210/article/details/106436364

總結: 如果你看懂了java的spi,那麼spring boot、dubbo的spi也能搞懂了,變體(當看到一樣事物的內在邏輯,就要學會潤色、加工、處理、改造、完善,靈活變通、舉一反三)!!!

附程式碼目錄結構:

參考:

0.java.util Class ServiceLoaderhttps://docs.oracle.com/javase/6/docs/api/java/util/ServiceLoader.html

  1. Java是如何實現自己的SPI機制的? JDK原始碼(一)https://mp.weixin.qq.com/s/6BhHBtoBlSqHlXduhzg7Pw

  2. Spring-SpringFactoriesLoader詳解https://msd.misuland.com/pd/2884250137616453978

  3. 探討註解驅動Spring應用的機制,詳解ServiceLoader、SpringFactoriesLoader的使用(以JDBC、spring.factories為例介紹SPI)https://cloud.tencent.com/developer/article/1497777

  4. Dubbo原始碼解析之SPI(一):擴充套件類的載入過程https://blog.51cto.com/14159827/2475733?source=drh

  5. Java Code Examples for org.springframework.core.io.support.SpringFactoriesLoaderhttps://www.programcreek.com/java-api-examples/index.php?api=org.springframework.core.io.support.SpringFactoriesLoader

  6. Java Service Loader vs Spring Factories Loaderhttps://blog.frankel.ch/java-service-loader-vs-spring-factories/

  7. JDK的SPI原理及原始碼分析https://mp.weixin.qq.com/s?__biz=MzI1MjQ2NjEyNA==&mid=2247483671&idx=1&sn=6d6ea78a1d7fd7ef0fb2a3f948bdca99