Autor: Jingdong Logistics Kong Xiangdong
1. Was ist SPI?
Der vollständige Name von SPI ist Service Provider Interface, das eine Dienstschnittstelle bereitstellt; es ist ein Diensterkennungsmechanismus. Die Essenz von SPI besteht darin, den vollständig qualifizierten Namen der Schnittstellenimplementierungsklasse in der Datei zu konfigurieren, und das Dienstladeprogramm liest die Konfiguration Datei und lädt die Implementierungsklasse. . Auf diese Weise kann die Implementierungsklasse für die Schnittstelle zur Laufzeit dynamisch ersetzt werden. Aufgrund dieser Eigenschaft können wir über den SPI-Mechanismus problemlos erweiterte Funktionen für unsere Programme bereitstellen.
Wie nachfolgend dargestellt:
Jede Abstraktion des Systemdesigns hat oft viele verschiedene Implementierungsschemata. Beim objektorientierten Design wird im Allgemeinen empfohlen, zwischen Modulen auf der Grundlage von Schnittstellen zu programmieren, und die Implementierung von Modulen ist nicht fest codiert. Sobald der Code bestimmte Implementierungsklassen beinhaltet, wird es verstößt gegen das steckbare Prinzip des Herausziehens. Java SPI stellt einen solchen Mechanismus bereit, um die Implementierung eines Dienstes für eine bestimmte Schnittstelle zu finden, was etwas der Idee von IOC ähnelt, die die Steuerung der Assemblierung aus dem Programm verlagert, was besonders bei der Modularisierung wichtig ist. Anstatt zu sagen, dass SPI ein von Java bereitgestellter Diensterkennungsmechanismus ist, ist es besser zu sagen, dass es sich um eine Entkopplungsidee handelt.
2. Einsatzszenario?
- Die Datenbanktreiber-Ladeschnittstelle realisiert das Laden von Klassen, wie zum Beispiel: JDBC lädt Mysql, Oracle...
- Die Protokollfassadenschnittstelle implementiert das Laden von Klassen, wie zum Beispiel: SLF4J unterstützt log4j und logback
- SPI ist in Spring weit verbreitet, insbesondere die Implementierung der automatischen Konfiguration im Spring-Boot
- Dubbo verwendet auch viel SPI, um die Erweiterung des Frameworks zu implementieren.Es kapselt das ursprüngliche SPI und ermöglicht Benutzern, die Filterschnittstelle zu erweitern und zu implementieren.
3. Einführung in die Verwendung
Um das Java-SPI zu verwenden, müssen die folgenden Konventionen eingehalten werden:
- Wenn der Dienstanbieter eine bestimmte Implementierung der Schnittstelle bereitstellt, muss im Verzeichnis META-INF/services des JAR-Pakets eine Datei mit dem Namen „interface Fully Qualified Name“ erstellt werden, deren Inhalt der vollständig qualifizierte Name der Implementierung ist Klasse;
- Das JAR, in dem sich die Schnittstellenimplementierungsklasse befindet, wird unter dem Klassenpfad des Hauptprogramms platziert, dh die Abhängigkeit wird eingeführt.
- Das Hauptprogramm lädt das Implementierungsmodul dynamisch über java.util.ServiceLoder.Es findet den vollständig qualifizierten Namen der Implementierungsklasse, indem es dieDateien im Verzeichnis META-INF/services durchsucht, lädt die Klasse in dieJVM und instanziiert sie;
- Die Implementierungsklasse des SPI muss einen Konstruktor ohne Parameter enthalten.
Beispiel:
spi-Schnittstellenmodul-Definition
定义一组接口:public interface MyDriver
spi-jd-Treiber
spi-ali-Treiber
实现为:public class JdDriver implements MyDriver
public class AliDriver implements MyDriver
Erstellen Sie ein /META-INF/services-Verzeichnis unter src/main/resources/ und fügen Sie eine Datei hinzu, die nach der Schnittstelle benannt ist (org.MyDriver-Datei).
Der Inhalt ist die jeweils anzuwendende Implementierungsklasse com.jd.JdDriver und com.ali.AliDriver
Spi-Core
Im Allgemeinen ist es das Kernpaket, das von der Plattform bereitgestellt wird, einschließlich der Strategie zum Laden und Verwenden von Implementierungsklassen usw. Hier implementieren wir einfach die Logik: a. Es wird keine spezifische Implementierung gefunden und eine Ausnahme wird ausgelöst b. Wenn mehrere Implementierungen gefunden werden , drucken Sie sie separat aus
public void invoker(){
ServiceLoader<MyDriver> serviceLoader = ServiceLoader.load(MyDriver.class);
Iterator<MyDriver> drivers = serviceLoader.iterator();
boolean isNotFound = true;
while (drivers.hasNext()){
isNotFound = false;
drivers.next().load();
}
if(isNotFound){
throw new RuntimeException("一个驱动实现类都不存在");
}
}
Spi-Test
public class App
{
public static void main( String[] args )
{
DriverFactory factory = new DriverFactory();
factory.invoker();
}
}
1. Führen Sie das Paket spi-core ein und führen Sie das Ergebnis aus
2. Einführung von Spi-Core- und Spi-JD-Treiberpaketen
3. Eingehender Spi-Core, Spi-JD-Treiber, Spi-Ali-Treiber
4. Prinzipanalyse
Sehen Sie, wie wir gerade die spezifische Implementierungsklasse erhalten haben?
Nur zwei Zeilen Code:
ServiceLoader<MyDriver> serviceLoader = ServiceLoader.load(MyDriver.class);
Iterator<MyDriver> drivers = serviceLoader.iterator();
Schauen wir uns also zuerst die ServiceLoader-Klasse an:
public final class ServiceLoader<S> implements Iterable<S>{
//配置文件的路径
private static final String PREFIX = "META-INF/services/";
// 代表被加载的类或者接口
private final Class<S> service;
// 用于定位,加载和实例化providers的类加载器
private final ClassLoader loader;
// 创建ServiceLoader时采用的访问控制上下文
private final AccessControlContext acc;
// 缓存providers,按实例化的顺序排列
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懒查找迭代器,真正加载服务的类
private LazyIterator lookupIterator;
//服务提供者查找的迭代器
private class LazyIterator
implements Iterator<S>
{
.....
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//全限定名:com.xxxx.xxx
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
}
}
while ((pending == null) || !pending.hasNext()) {
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);
}
if (!service.isAssignableFrom(c)) {
fail(service, "Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
}
}
........
Der ungefähre Ablauf ist wie folgt:
-
Die Anwendung ruft die Methode ServiceLoader.load auf
-
Wenn die Anwendung eine Objektinstanz über einen Iterator erhält, bestimmt sie zuerst, ob es ein zwischengespeichertes Beispielobjekt im Anbieterobjekt gibt, und gibt es direkt zurück, falls es existiert
-
Wenn es nicht vorhanden ist, führen Sie Klassenneudruck durch und lesen Sie die Konfigurationsdatei unter META-INF/services, um die Namen aller Klassen zu erhalten, die instanziiert werden können, und die Konfigurationsdatei kann über JARs abgerufen werden. Laden Sie das Objekt über die Reflection-Methode Class .forName() und use Instance() Die instanziierte Klassenmethode speichert die instanziierte Klasse im Provider-Objekt und gibt sie synchron zurück.
5. Zusammenfassung
Vorteile: Entkopplung
Die Verwendung von SPI trennt die Assembly-Steuerlogik des Drittanbieter-Dienstmoduls vom Geschäftscode des Aufrufers und wird nicht miteinander gekoppelt.Die Anwendung kann Framework-Erweiterungen ermöglichen und Framework-Komponenten gemäßden tatsächlichen Geschäftsbedingungen ersetzen.
Die Verwendung von SPI macht es unnötig, die Implementierungsklasse auf folgende Weise zu erhalten
-
Code fest codierter Import importieren
-
Geben Sie den vollständig qualifizierten Namen der Klasse für die Reflektionserfassung an, wie vor JDBC4.0; Class.forName("com.mysql.jdbc.Driver")
Mangel:
Obwohl ServiceLoader als Lazy Loading angesehen werden kann, kann es nur durch Traversal erhalten werden, dh alle Implementierungsklassen der Schnittstelle werden einmal geladen und instanziiert. Wenn Sie eine Implementierungsklasse nicht verwenden möchten, wird sie ebenfalls geladen und instanziiert, was zu Verschwendung führt. Der Weg zum Erhalten einer bestimmten Implementierungsklasse ist nicht flexibel genug, sie kann nur in Form von Iterator erhalten werden, und die entsprechende Implementierungsklasse kann nicht gemäß einem bestimmten Parameter erhalten werden.