Auteur : Jingdong Logistics Kong Xiangdong
1. Qu'est-ce que le SPI ?
Le nom complet de SPI est Service Provider Interface, qui fournit une interface de service ; il s'agit d'un mécanisme de découverte de service. L'essence de SPI est de configurer le nom complet de la classe d'implémentation d'interface dans le fichier, et le chargeur de service lit la configuration fichier et charge la classe d'implémentation. . De cette manière, la classe d'implémentation peut être remplacée dynamiquement pour l'interface lors de l'exécution. Grâce à cette fonctionnalité, nous pouvons facilement fournir des fonctions étendues pour nos programmes via le mécanisme SPI.
Comme indiqué ci-dessous:
Chaque abstraction de la conception du système a souvent de nombreux schémas d'implémentation différents. Dans la conception orientée objet, il est généralement recommandé de programmer entre les modules en fonction des interfaces, et l'implémentation des modules n'est pas codée en dur. Une fois que le code implique des classes d'implémentation spécifiques, il viole le pluggable Le principe de l'extraction. Java SPI fournit un tel mécanisme pour trouver l'implémentation d'un service pour une certaine interface, ce qui est quelque peu similaire à l'idée d'IOC, qui déplace le contrôle de l'assemblage hors du programme, ce qui est particulièrement important dans la modularisation. Plutôt que de dire que SPI est un mécanisme de découverte de service fourni par java, il vaut mieux dire que c'est une idée de découplage.
2. Scénario d'utilisation ?
- L'interface de chargement du pilote de base de données réalise le chargement des classes ; telles que : JDBC charge Mysql, Oracle...
- L'interface de façade de journal implémente le chargement de classe, par exemple : SLF4J prend en charge log4j et logback
- SPI est largement utilisé dans Spring, en particulier la mise en œuvre de la configuration automatique dans spring-boot
- Dubbo utilise également beaucoup de SPI pour implémenter l'extension du framework, qui encapsule le SPI d'origine et permet aux utilisateurs d'étendre et d'implémenter l'interface Filter.
3. Introduction à l'utilisation
Pour utiliser Java SPI, les conventions suivantes doivent être suivies :
- Lorsque le fournisseur de services fournit une implémentation spécifique de l'interface, il est nécessaire de créer un fichier nommé "nom complet de l'interface" dans le répertoire META-INF/services du package JAR, et le contenu est le nom complet de l'implémentation classe;
- Le JAR où se trouve la classe d'implémentation d'interface est placé sous le chemin de classe du programme principal, c'est-à-dire que la dépendance est introduite.
- Le programme principal charge dynamiquement le module d'implémentation via java.util.ServiceLoder. Il trouvera le nom complet de la classe d'implémentation en analysant les fichiers du répertoire META-INF/services, chargera la classe dans la JVM et l'instanciera ;
- La classe d'implémentation du SPI doit porter un constructeur sans paramètres.
Exemple:
définition du module spi-interface
定义一组接口:public interface MyDriver
pilote spi-jd
pilote spi-ali
实现为:public class JdDriver implements MyDriver
public class AliDriver implements MyDriver
Créez un répertoire /META-INF/services sous src/main/resources/ et ajoutez un fichier nommé d'après l'interface (fichier org.MyDriver)
Le contenu est la classe d'implémentation à appliquer respectivement com.jd.JdDriver et com.ali.AliDriver
spi-core
Généralement, il s'agit du package de base fourni par la plate-forme, y compris la stratégie de chargement et d'utilisation des classes d'implémentation, etc. Ici, nous implémentons simplement la logique : a. Aucune implémentation spécifique n'est trouvée et une exception est levée b. Si plusieurs implémentations sont trouvées , imprimez-les séparément
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. Introduisez le package spi-core et exécutez le résultat
2. Introduire les packages spi-core, spi-jd-driver
3. Spi-core entrant, pilote spi-jd, pilote spi-ali
4. Analyse de principe
Voyez comment nous avons obtenu la classe d'implémentation spécifique tout à l'heure ?
Juste deux lignes de code :
ServiceLoader<MyDriver> serviceLoader = ServiceLoader.load(MyDriver.class);
Iterator<MyDriver> drivers = serviceLoader.iterator();
Alors, regardons d'abord la classe ServiceLoader :
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;
}
}
........
Le processus approximatif est le suivant :
-
L'application appelle la méthode ServiceLoader.load
-
Lorsque l'application obtient une instance d'objet via un itérateur, elle détermine d'abord s'il existe un exemple d'objet en cache dans l'objet fournisseurs et le renvoie directement s'il existe
-
S'il n'existe pas, effectuez une réimpression de classe et lisez le fichier de configuration sous META-INF/services pour obtenir les noms de toutes les classes pouvant être instanciées, et le fichier de configuration peut être obtenu à travers les fichiers JAR. Chargez l'objet via la méthode de réflexion Classe .forName() et utilisez Instance() La méthode de classe instanciée met en cache la classe instanciée dans l'objet fournisseurs et renvoie de manière synchrone.
5. Résumé
Avantages : découplage
L'utilisation de SPI sépare la logique de contrôle d'assemblage du module de service tiers du code métier de l'appelant et ne sera pas couplée.L'application peut activer des extensions de framework et remplacer des composants de framework en fonction des conditions commerciales réelles.
L'utilisation de SPI rend inutile l'obtention de la classe d'implémentation des manières suivantes
-
Importation d'importation codée en dur
-
Spécifiez le nom complet de la classe pour l'acquisition de la réflexion, comme avant JDBC4.0 ; Class.forName("com.mysql.jdbc.Driver")
défaut:
Bien que ServiceLoader puisse être considéré comme un chargement paresseux, il ne peut être obtenu que par traversée, c'est-à-dire que toutes les classes d'implémentation de l'interface sont chargées et instanciées une fois. Si vous ne souhaitez pas utiliser une classe d'implémentation, elle est également chargée et instanciée, ce qui entraîne un gaspillage. La manière d'obtenir une certaine classe d'implémentation n'est pas assez flexible, elle ne peut être obtenue que sous la forme d'Iterator, et la classe d'implémentation correspondante ne peut pas être obtenue en fonction d'un certain paramètre.