聊聊 SPI 机制

大模型向量数据库机器学习

SPI 全称为 Service Provider Interface ,是一种服务发现机制。

SPI 的本质是将接口实现类全限定名配置在文件 中,并由服务加载器读取配置文件,加载实现类 。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。

picture.image

1 Java SPI 示例

本节通过一个示例演示 Java SPI 的使用方法。首先,我们定义一个接口,名称为 Robot。


        
          
public interface Robot {  
    void sayHello();  
}  

      

接下来定义两个实现类,分别为 OptimusPrimeBumblebee


        
          
public class OptimusPrime implements Robot {  
      
    @Override  
    public void sayHello() {  
        System.out.println("Hello, I am Optimus Prime.");  
    }  
    
}  
  
public class Bumblebee implements Robot {  
  
    @Override  
    public void sayHello() {  
        System.out.println("Hello, I am Bumblebee.");  
    }  
    
}  

      

接下来 META-INF/services 文件夹下创建一个文件,名称为 Robot 的全限定名 org.apache.spi.Robot。文件内容为实现类的全限定的类名,如下:


        
          
org.apache.spi.OptimusPrime  
org.apache.spi.Bumblebee  

      

做好所需的准备工作,接下来编写代码进行测试。


        
          
public class JavaSPITest {  
    @Test  
    public void sayHello() throws Exception {  
        ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);  
        System.out.println("Java SPI");  
        // 1. forEach 模式  
        serviceLoader.forEach(Robot::sayHello);  
        // 2. 迭代器模式  
        Iterator<Robot> iterator = serviceLoader.iterator();  
        while (iterator.hasNext()) {  
            Robot robot = iterator.next();  
          //System.out.println(robot);  
          //robot.sayHello();  
        }  
    }  
}  

      

最后来看一下测试结果,如下 :

picture.image

2 经典 Java SPI 应用 : JDBC DriverManager

JDBC4.0 之前,我们开发有连接数据库的时候,通常先加载数据库相关的驱动,然后再进行获取连接等的操作。


        
          
// STEP 1: Register JDBC driver  
Class.forName("com.mysql.jdbc.Driver");  
// STEP 2: Open a connection  
String url = "jdbc:xxxx://xxxx:xxxx/xxxx";  
Connection conn = DriverManager.getConnection(url,username,password);  

      

JDBC4.0之后使用了 Java 的 SPI 扩展机制,不再需要用 Class.forName("com.mysql.jdbc.Driver") 来加载驱动,直接就可以获取 JDBC 连接。

接下来,我们来看看应用如何加载 MySQL JDBC 8.0.22 驱动:

picture.image

首先 DriverManager类是驱动管理器,也是驱动加载的入口。


        
          
/**  
 * Load the initial JDBC drivers by checking the System property  
 * jdbc.properties and then use the {@code ServiceLoader} mechanism  
 */  
static {  
     loadInitialDrivers();  
     println("JDBC DriverManager initialized");  
}  

      

在 Java 中,static 块用于静态初始化,它在类被加载到 Java 虚拟机中时执行。

静态块会加载实例化驱动,接下来我们看看loadInitialDrivers 方法。

picture.image

加载驱动代码包含四个步骤:

  1. 系统变量中获取有关驱动的定义。
  2. 使用 SPI 来获取驱动的实现类(字符串的形式)。
  3. 遍历使用 SPI 获取到的具体实现,实例化各个实现类。
  4. 根据第一步获取到的驱动列表来实例化具体实现类。

我们重点关注 SPI 的用法,首先看第二步,使用 SPI 来获取驱动的实现类 , 对应的代码是:


        
          
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);  

      

这里没有去 META-INF/services目录下查找配置文件,也没有加载具体实现类,做的事情就是封装了我们的接口类型和类加载器,并初始化了一个迭代器。

接着看第三步,遍历使用SPI获取到的具体实现,实例化各个实现类,对应的代码如下:


        
          
Iterator<Driver> driversIterator = loadedDrivers.iterator();  
//遍历所有的驱动实现  
while(driversIterator.hasNext()) {  
    driversIterator.next();  
}  

      

在遍历的时候,首先调用driversIterator.hasNext()方法,这里会搜索 classpath 下以及 jar 包中所有的META-INF/services目录下的java.sql.Driver文件,并找到文件中的实现类的名字,此时并没有实例化具体的实现类。

然后是调用driversIterator.next()方法,此时就会根据驱动名字具体实例化各个实现类了,现在驱动就被找到并实例化了。

3 Java SPI 机制源码解析

我们根据第一节 JDK SPI 示例,学习 ServiceLoader 类的实现。

picture.image ServiceLoader类

进入 ServiceLoader 类的load方法:


        
          
public static <S> ServiceLoader<S> load(Class<S> service) {  
     ClassLoader cl = Thread.currentThread().getContextClassLoader();  
     return ServiceLoader.load(service, cl);  
}  
  
public static <S> ServiceLoader<S> load(Class<S> service , ClassLoader loader) {  
     return new ServiceLoader<>(service, loader);  
}  

      

上面的代码,load 方法会通过传递的服务类型类加载器 classLoader 创建一个 ServiceLoader 对象。


        
          
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 LinkedHashMap<String,S> providers = new LinkedHashMap<>();  
  
public void reload() {  
     providers.clear();  
     lookupIterator = new LazyIterator(service, loader);  
}  

      

私有构造器会创建懒迭代器 LazyIterator 对象 ,所谓懒迭代器,就是对象初始化时,仅仅是初始化,只有在真正调用迭代方法时,才执行加载逻辑

示例代码中创建完 serviceLoader 之后,接着调用iterator()方法:


        
          
Iterator<Robot> iterator = serviceLoader.iterator();  
  
// 迭代方法实现  
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();  
          }  
     };  
}  

      

迭代方法的实现本质是调用懒迭代器 lookupIterator 的 hasNext()next() 方法。

1、hasNext() 方法


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

      

懒迭代器的hasNextService方法首先会通过加载器通过文件全名获取配置对象 Enumeration<URL> configs ,然后调用解析parse方法解析classpath下的META-INF/services/目录里以服务接口命名的文件。


        
          
private boolean hasNextService() {  
        if (nextName != null) {  
              return true;  
        }  
        if (configs == null) {  
             try {  
                 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);  
              }  
         }  
         while ((pending == null) || !pending.hasNext()) {  
             if (!configs.hasMoreElements()) {  
                  return false;  
             }  
             pending = parse(service, configs.nextElement());  
         }  
         nextName = pending.next();  
         return true;  
}  

      

hasNextService 方法返回 true , 我们可以调用迭代器的 next 方法 ,本质是调用懒加载器 lookupIterator 的 next() 方法:

2、next() 方法


        
          
Robot robot = iterator.next();  
  
// 调用懒加载器 lookupIterator 的  `next()` 方法  
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 {  
              S p = service.cast(c.newInstance());  
              providers.put(cn, p);  
              return p;  
          } catch (Throwable x) {  
              fail(service,  
                     "Provider " + cn + " could not be instantiated",  
                     x);  
          }  
          throw new Error();  // This cannot happen  
  }  

      

通过反射方法 Class.forName() 加载类对象,并用newInstance方法将类实例化,并把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型,然后返回实例对象。

4 Java SPI 机制的缺陷

通过上面的解析,可以发现,我们使用 JDK SPI 机制的缺陷 :

  • 不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
  • 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
  • 多个并发多线程使用 ServiceLoader 类的实例是不安全的。
5 Spring SPI 机制

Spring SPI 沿用了 Java SPI 的设计思想,Spring 采用的是 spring.factories 方式实现 SPI 机制,可以在不修改 Spring 源码的前提下,提供 Spring 框架的扩展性。

picture.image

1、创建 MyTestService 接口


        
          
public interface MyTestService {  
    void printMylife();  
}  

      

2、创建 MyTestService 接口实现类

  • WorkTestService :

        
          
public class WorkTestService implements MyTestService {  
    public WorkTestService(){  
        System.out.println("WorkTestService");  
    }  
    public void printMylife() {  
        System.out.println("我的工作");  
    }  
}  

      
  • FamilyTestService :

        
          
public class FamilyTestService implements MyTestService {  
    public FamilyTestService(){  
        System.out.println("FamilyTestService");  
    }  
    public void printMylife() {  
        System.out.println("我的家庭");  
    }  
}  

      

3、在资源文件目录,创建一个固定的文件 META-INF/spring.factories


        
          
#key是接口的全限定名,value是接口的实现类  
com.courage.platform.sms.demo.service.MyTestService = com.courage.platform.sms.demo.service.impl.FamilyTestService,com.courage.platform.sms.demo.service.impl.WorkTestService  

      

4、运行代码


        
          
// 调用 SpringFactoriesLoader.loadFactories 方法加载 MyTestService 接口所有实现类的实例  
List<MyTestService> myTestServices = SpringFactoriesLoader.loadFactories(  
            MyTestService.class,  
            Thread.currentThread().getContextClassLoader()  
);  
  
for (MyTestService testService : myTestServices) {  
     testService.printMylife();  
}  

      

运行结果:


        
          
FamilyTestService  
WorkTestService  
我的家庭  
我的工作   

      

Spring SPI 机制非常类似 ,但还是有一些差异:

  • Java SPI 是一个服务提供接口对应一个配置文件,配置文件中存放当前接口的所有实现类,多个服务提供接口对应多个配置文件,所有配置都在 services 目录下。
  • Spring SPI 是一个 spring.factories 配置文件存放多个接口及对应的实现类,以接口全限定名作为key,实现类作为value来配置,多个实现类用逗号隔开,仅 spring.factories 一个配置文件。

和 Java SPI 一样,Spring SPI 也无法获取某个固定的实现,只能按顺序获取所有实现

6 Dubbo SPI 机制

基于 Java SPI 的缺陷无法支持按需加载接口实现类,Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。

Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。

Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,配置内容如下:


        
          
optimusPrime = org.apache.spi.OptimusPrime  
bumblebee = org.apache.spi.Bumblebee  

      

与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。

另外,在测试 Dubbo SPI 时,需要在 Robot 接口上标注 @SPI 注解。

下面来演示 Dubbo SPI 的用法:


        
          
public class DubboSPITest {  
  
    @Test  
    public void sayHello() throws Exception {  
        ExtensionLoader<Robot> extensionLoader =   
            ExtensionLoader.getExtensionLoader(Robot.class);  
        Robot optimusPrime = extensionLoader.getExtension("optimusPrime");  
        optimusPrime.sayHello();  
        Robot bumblebee = extensionLoader.getExtension("bumblebee");  
        bumblebee.sayHello();  
    }  
}  

      

测试结果如下 :

picture.image

另外,Dubbo SPI 除了支持按需加载接口实现类,还增加了 IOC 和 AOP 等特性


最后欢迎加入苏三的星球,你将获得:商城系统实战、秒杀系统实战、代码生成工具、系统设计、性能优化、技术选型、高频面试题、底层原理、Spring源码解读、工

作经验分享、痛点问题等多个优质专栏。

还有1V1答疑、修改简历、职业规划、送书活动、技术交流。

目前星球已经更新了4400+篇优质内容,还在持续爆肝中..星球已经被官方推荐了3次,收到了小伙伴们的一致好评。戳我加入学习,已有1400+小伙伴加入学习

我的技术专栏《程序员最常见的100个问题》,目前已经更新了80篇干货文章,里面收录了很多踩坑经历,对你的职业生涯或许有些帮助,最近收到的好评挺多的。

这个专栏总结了我10年工作中,遇到过的100个非常有代表性的技术问题,非常有参考和学习价值。

Java、Spring、分布式、高并发、数据库、海量数据、线上问题什么都有。

每篇文章从发现问题、分析问题、解决问题和问题总结等多个维度,深入浅出,分享了很多技术细节,定位和排查问题思路,解决问题技巧,以及实际工作经验。

你能从中学到很多有用知识,帮你少走很多弯路。

扫描下方二维码即可订阅:

picture.image

原价199,现价只需23,即将涨价。

picture.image

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
云原生可观测性技术的落地实践
云原生技术和理念在近几年成为了备受关注的话题。应用通过云原生改造,变得更动态、弹性,可以更好地利用云的弹性能力。但是动态、弹性的环境也给应用以及基础设施的观测带来了更大的挑战。本次分享主要介绍了云原生社区中可观测性相关的技术和工具,以及如何使用这些工具来完成对云原生环境的观测。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论