結合實戰和原始碼來聊聊Java中的SPI機制?

冰河團隊發表於2020-11-20

寫在前面

SPI機制能夠非常方便的為某個介面動態指定其實現類,在某種程度上,這也是某些框架具有高度可擴充套件性的基礎。今天,我們就從原始碼級別深入探討下Java中的SPI機制。

注:文章已收錄到:https://github.com/sunshinelyz/technology-binghe

SPI的概念

SPI在Java中的全稱為Service Provider Interface,是JDK內建的一種服務提供發現機制,是Java提供的一套用來被第三方實現或者擴充套件的API,它可以用來啟用框架擴充套件和替換元件。

JAVA SPI = 基於介面的程式設計+策略模式+配置檔案的動態載入機制

SPI的使用場景

Java是一種面嚮物件語言,雖然Java8開始支援函數語言程式設計和Stream,但是總體來說,還是物件導向的語言。在使用Java進行物件導向開發時,一般會推薦使用基於介面的程式設計,程式的模組與模組之前不會直接進行實現類的硬編碼。而在實際的開發過程中,往往一個介面會有多個實現類,各實現類要麼實現的邏輯不同,要麼使用的方式不同,還有的就是實現的技術不同。為了使呼叫方在呼叫介面的時候,明確的知道自己呼叫的是介面的哪個實現類,或者說為了實現在模組裝配的時候不用在程式裡動態指明,這就需要一種服務發現機制。Java中的SPI載入機制能夠滿足這樣的需求,它能夠自動尋找某個介面的實現類。

大量的框架使用了Java的SPI技術,如下:

(1)JDBC載入不同型別的資料庫驅動
(2)日誌門面介面實現類載入,SLF4J載入不同提供商的日誌實現類
(3)Spring中大量使用了SPI

  • 對servlet3.0規範
  • 對ServletContainerInitializer的實現
  • 自動型別轉換Type Conversion SPI(Converter SPI、Formatter SPI)等

(4)Dubbo裡面有很多個元件,每個元件在框架中都是以介面的形成抽象出來!具體的實現又分很多種,在程式執行時根據使用者的配置來按需取介面的實現

SPI的使用

當服務的提供者,提供了介面的一種實現後,需要在Jar包的META-INF/services/目錄下,建立一個以介面的名稱(包名.介面名的形式)命名的檔案,在檔案中配置介面的實現類(完整的包名+類名)。

當外部程式通過java.util.ServiceLoader類裝載這個介面時,就能夠通過該Jar包的META/Services/目錄裡的配置檔案找到具體的實現類名,裝載例項化,完成注入。同時,SPI的規範規定了介面的實現類必須有一個無參構造方法。

SPI中查詢介面的實現類是通過java.util.ServiceLoader,而在java.util.ServiceLoader類中有一行程式碼如下:

// 載入具體實現類資訊的字首,也就是以介面命名的檔案需要放到Jar包中的META-INF/services/目錄下
private static final String PREFIX = "META-INF/services/";

這也就是說,我們必須將介面的配置檔案寫到Jar包的META/Services/目錄下。

SPI例項

這裡,給出一個簡單的SPI使用例項,演示在Java程式中如何使用SPI動態載入介面的實現類。

注意:例項是基於Java8進行開發的。

1.建立Maven專案

在IDEA中建立Maven專案spi-demo,如下:

2.編輯pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<artifactId>spi-demo</artifactId>
<groupId>io.binghe.spi</groupId>
<packaging>jar</packaging>
<version>1.0.0-SNAPSHOT</version>
<modelVersion>4.0.0</modelVersion>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.6.0</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
    </plugins>
</build>

</project>

3.建立類載入工具類

在io.binghe.spi.loader包下建立MyServiceLoader,MyServiceLoader類中直接呼叫JDK的ServiceLoader類載入Class。程式碼如下所示。

package io.binghe.spi.loader;
 
import java.util.ServiceLoader;
 
/**
 * @author binghe
 * @version 1.0.0
 * @description 類載入工具
 */
public class MyServiceLoader {
 
    /**
     * 使用SPI機制載入所有的Class
     */
    public static <S> ServiceLoader<S> loadAll(final Class<S> clazz) {
        return ServiceLoader.load(clazz);
    }
}

4.建立介面

在io.binghe.spi.service包下建立介面MyService,作為測試介面,介面中只有一個方法,列印傳入的字串資訊。程式碼如下所示:

package io.binghe.spi.service;
 
/**
 * @author binghe
 * @version 1.0.0
 * @description 定義介面
 */
public interface MyService {
 
    /**
     *  列印資訊
     */
    void print(String info);
}

5.建立介面的實現類

(1)建立第一個實現類MyServiceA

在io.binghe.spi.service.impl包下建立MyServiceA類,實現MyService介面。程式碼如下所示:

package io.binghe.spi.service.impl;
import io.binghe.spi.service.MyService;
 
/**
 * @author binghe
 * @version 1.0.0
 * @description 介面的第一個實現
 */
public class MyServiceA implements MyService {
    @Override
    public void print(String info) {
        System.out.println(MyServiceA.class.getName() + " print " + info);
    }
}

(2)建立第二個實現類MyServiceB

在io.binghe.spi.service.impl包下建立MyServiceB類,實現MyService介面。程式碼如下所示:

package io.binghe.spi.service.impl;
 
import io.binghe.spi.service.MyService;
 
/**
 * @author binghe
 * @version 1.0.0
 * @description 介面第二個實現
 */
public class MyServiceB implements MyService {
    @Override
    public void print(String info) {
        System.out.println(MyServiceB.class.getName() + " print " + info);
    }
}

6.建立介面檔案

在專案的src/main/resources目錄下建立META/Services/目錄,在目錄中建立io.binghe.spi.service.MyService檔案,注意:檔案必須是介面MyService的全名,之後將實現MyService介面的類配置到檔案中,如下所示:

io.binghe.spi.service.impl.MyServiceA
io.binghe.spi.service.impl.MyServiceB

7.建立測試類

在專案的io.binghe.spi.main包下建立Main類,該類為測試程式的入口類,提供一個main()方法,在main()方法中呼叫ServiceLoader類載入MyService介面的實現類。並通過Java8的Stream將結果列印出來,如下所示:

package io.binghe.spi.main;
 
import io.binghe.spi.loader.MyServiceLoader;
import io.binghe.spi.service.MyService;
 
import java.util.ServiceLoader;
import java.util.stream.StreamSupport;
 
/**
 * @author binghe
 * @version 1.0.0
 * @description 測試的main方法
 */
public class Main {
 
    public static void main(String[] args){
        ServiceLoader<MyService> loader = MyServiceLoader.loadAll(MyService.class);
        StreamSupport.stream(loader.spliterator(), false).forEach(s -> s.print("Hello World"));
    }
}

8.測試例項

執行Main類中的main()方法,列印出的資訊如下所示:

io.binghe.spi.service.impl.MyServiceA print Hello World
io.binghe.spi.service.impl.MyServiceB print Hello World

Process finished with exit code 0

通過列印資訊可以看出,通過Java SPI機制正確載入出介面的實現類,並呼叫介面的實現方法。

原始碼解析

這裡,主要是對SPI的載入流程涉及到的java.util.ServiceLoader的原始碼的解析。

進入java.util.ServiceLoader的原始碼,可以看到ServiceLoader類實現了java.lang.Iterable介面,如下所示。

public final class ServiceLoader<S>  implements Iterable<S> 

說明ServiceLoader類是可以遍歷迭代的。

java.util.ServiceLoader類中定義瞭如下的成員變數:

// 載入具體實現類資訊的字首,也就是以介面命名的檔案需要放到Jar包中的META-INF/services/目錄下
private static final String PREFIX = "META-INF/services/";

// 需要載入的介面
private final Class<S> service;

// 類載入器,用於載入以介面命名的檔案中配置的介面的實現類
private final ClassLoader loader;

// 建立ServiceLoader時採用的訪問控制上下文環境
private final AccessControlContext acc;

// 用來快取已經載入的介面實現類,其中,Key是介面實現類的完整類名,Value為實現類物件
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// 用於延遲載入實現類的迭代器
private LazyIterator lookupIterator;

可以看到ServiceLoader類中定義了載入字首為“META-INF/services/”,所以,介面檔案必須要在專案的src/main/resources目錄下的META-INF/services/目錄下建立。

從MyServiceLoader類呼叫ServiceLoader.load(clazz)方法進入原始碼,如下所示:

//根據類的Class物件載入指定的類,返回ServiceLoader物件
public static <S> ServiceLoader<S> load(Class<S> service) {
	//獲取當前執行緒的類載入器
	ClassLoader cl = Thread.currentThread().getContextClassLoader();
	//動態載入指定的類,將類載入到ServiceLoader中
	return ServiceLoader.load(service, cl);
}

方法中呼叫了ServiceLoader.load(service, cl)方法,繼續跟蹤程式碼,如下所示:

//通過ClassLoader載入指定類的Class,並將返回結果封裝到ServiceLoader物件中
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
	return new ServiceLoader<>(service, loader);
}

可以看到ServiceLoader.load(service, cl)方法中,呼叫了ServiceLoader類的構造方法,繼續跟進程式碼,如下所示:

//構造ServiceLoader物件
private ServiceLoader(Class<S> svc, ClassLoader cl) {
	//如果傳入的Class物件為空,則判處空指標異常
	service = Objects.requireNonNull(svc, "Service interface cannot be null");
	//如果傳入的ClassLoader為空,則通過ClassLoader.getSystemClassLoader()獲取,否則直接使用傳入的ClassLoader
	loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
	acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
	reload();
}

繼續跟reload()方法,如下所示。

//重新載入
public void reload() {
	//清空儲存載入的實現類的LinkedHashMap
	providers.clear();
	//構造延遲載入的迭代器
	lookupIterator = new LazyIterator(service, loader);
}

繼續跟進懶載入迭代器的建構函式,如下所示。

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

可以看到,會將需要載入的介面的Class物件和類載入器賦值給LazyIterator的成員變數。

當我們在程式中迭代獲取物件例項時,首先在成員變數providers中查詢是否有快取的例項物件。如果存在則直接返回,否則呼叫lookupIterator延遲載入迭代器進行載入。

迭代器進行邏輯判斷的程式碼如下所示:

//迭代ServiceLoader的方法
public Iterator<S> iterator() {
	return new Iterator<S>() {
		//獲取儲存實現類的LinkedHashMap<String,S>的迭代器
		Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
		//判斷是否有下一個元素
		public boolean hasNext() {
			//如果knownProviders存在元素,則直接返回true
			if (knownProviders.hasNext())
				return true;
			//返回延遲載入器是否存在元素
			return lookupIterator.hasNext();
		}
		//獲取下一個元素
		public S next() {
			//如果knownProviders存在元素,則直接獲取
			if (knownProviders.hasNext())
				return knownProviders.next().getValue();
			//獲取延遲迭代器lookupIterator中的元素
			return lookupIterator.next();
		}

		public void remove() {
			throw new UnsupportedOperationException();
		}
	};
}

LazyIterator載入類的流程如下程式碼所示

//判斷是否擁有下一個例項
private boolean hasNextService() {
	//如果擁有下一個例項,直接返回true
	if (nextName != null) {
		return true;
	}
	//如果實現類的全名為null
	if (configs == null) {
		try {
			//獲取全檔名,檔案相對路徑+檔名稱(包名+介面名)
			String fullName = PREFIX + service.getName();
			//類載入器為空,則通過ClassLoader.getSystemResources()方法獲取
			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()) {
		//如果configs中沒有更過的元素,則直接返回false
		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 {
		//通過c.newInstance()生成物件例項
		S p = service.cast(c.newInstance());
		//將生成的物件例項儲存到快取中(LinkedHashMap<String,S>)
		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);
	}
}

最後,給出整個java.util.ServiceLoader的類,如下所示:

package java.util;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;


public final class ServiceLoader<S>  implements Iterable<S> {
    // 載入具體實現類資訊的字首,也就是以介面命名的檔案需要放到Jar包中的META-INF/services/目錄下
    private static final String PREFIX = "META-INF/services/";

    // 需要載入的介面
    private final Class<S> service;
    
    // 類載入器,用於載入以介面命名的檔案中配置的介面的實現類
    private final ClassLoader loader;
    
    // 建立ServiceLoader時採用的訪問控制上下文環境
    private final AccessControlContext acc;
    
    // 用來快取已經載入的介面實現類,其中,Key是介面實現類的完整類名,Value為實現類物件
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    
    // 用於延遲載入實現類的迭代器
    private LazyIterator lookupIterator;
    
    //重新載入
    public void reload() {
        //清空儲存載入的實現類的LinkedHashMap
        providers.clear();
        //構造延遲載入的迭代器
        lookupIterator = new LazyIterator(service, loader);
    }
    
    //構造ServiceLoader物件
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        //如果傳入的Class物件為空,則判處空指標異常
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        //如果傳入的ClassLoader為空,則通過ClassLoader.getSystemClassLoader()獲取,否則直接使用傳入的ClassLoader
        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) {
            return -1;
        }
        int ci = ln.indexOf('#');
        if (ci >= 0) ln = ln.substring(0, ci);
        ln = ln.trim();
        int n = ln.length();
        if (n != 0) {
            if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
                fail(service, u, lc, "Illegal configuration-file syntax");
            int cp = ln.codePointAt(0);
            if (!Character.isJavaIdentifierStart(cp))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
            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 lookupload
    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() {
            //如果擁有下一個例項,直接返回true
            if (nextName != null) {
                return true;
            }
            //如果實現類的全名為null
            if (configs == null) {
                try {
                    //獲取全檔名,檔案相對路徑+檔名稱(包名+介面名)
                    String fullName = PREFIX + service.getName();
                    //類載入器為空,則通過ClassLoader.getSystemResources()方法獲取
                    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()) {
                //如果configs中沒有更過的元素,則直接返回false
                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 {
                //通過c.newInstance()生成物件例項
                S p = service.cast(c.newInstance());
                //將生成的物件例項儲存到快取中(LinkedHashMap<String,S>)
                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();
        }
    
    }
    
    //迭代ServiceLoader的方法
    public Iterator<S> iterator() {
        return new Iterator<S>() {
            //獲取儲存實現類的LinkedHashMap<String,S>的迭代器
            Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
            //判斷是否有下一個元素
            public boolean hasNext() {
                //如果knownProviders存在元素,則直接返回true
                if (knownProviders.hasNext())
                    return true;
                //返回延遲載入器是否存在元素
                return lookupIterator.hasNext();
            }
            //獲取下一個元素
            public S next() {
                //如果knownProviders存在元素,則直接獲取
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                //獲取延遲迭代器lookupIterator中的元素
                return lookupIterator.next();
            }
    
            public void remove() {
                throw new UnsupportedOperationException();
            }
    
        };
    }
    
    //通過ClassLoader載入指定類的Class,並將返回結果封裝到ServiceLoader物件中
    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }
    
    //根據類的Class物件載入指定的類,返回ServiceLoader物件
    public static <S> ServiceLoader<S> load(Class<S> service) {
        //獲取當前執行緒的類載入器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        //動態載入指定的類,將類載入到ServiceLoader中
        return ServiceLoader.load(service, cl);
    }
    
    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);
    }
    
    /**
     * Returns a string describing this service.
     *
     * @return  A descriptive string
     */
    public String toString() {
        return "java.util.ServiceLoader[" + service.getName() + "]";
    }
}

SPI總結

最後,對Java提供的SPI機制進行簡單的總結。

優點:

能夠實現專案解耦,使得第三方服務模組的裝配控制的邏輯與呼叫者的業務程式碼分離,而不是耦合在一起。應用程式可以根據實際業務情況啟用框架擴充套件或替換框架元件。

缺點:

  • 多個併發多執行緒使用ServiceLoader類的例項是不安全的
  • 雖然ServiceLoader也算是使用的延遲載入,但是基本只能通過遍歷全部獲取,也就是介面的實現類全部載入並例項化一遍。

參考:深入理解Java中的spi機制

相關文章