文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

温习 SPI 机制 (Java SPI 、Spring SPI、Dubbo SPI)

2024-11-28 16:37

关注

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

图片图片

1 Java SPI 示例

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

public interface Robot {
    void sayHello();
}

接下来定义两个实现类,分别为 OptimusPrime 和 Bumblebee。

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 serviceLoader = ServiceLoader.load(Robot.class);
        System.out.println("Java SPI");
        // 1. forEach 模式
        serviceLoader.forEach(Robot::sayHello);
        // 2. 迭代器模式
        Iterator iterator = serviceLoader.iterator();
        while (iterator.hasNext()) {
            Robot robot = iterator.next();
          //System.out.println(robot);
          //robot.sayHello();
        }
    }
}

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

图片图片

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 驱动:

图片图片

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


static {
     loadInitialDrivers();
     println("JDBC DriverManager initialized");
}

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

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

图片图片

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

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

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

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

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

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

Iterator 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 类的实现。

ServiceLoader类ServiceLoader类

进入 ServiceLoader 类的load方法:

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

public static  ServiceLoader load(Class service , ClassLoader loader) {
     return new ServiceLoader<>(service, loader);
}

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

private ServiceLoader(Class 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 providers = new LinkedHashMap<>();

public void reload() {
     providers.clear();
     lookupIterator = new LazyIterator(service, loader);
}

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

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

Iterator iterator = serviceLoader.iterator();

// 迭代方法实现
public Iterator iterator() {
     return new Iterator() {
          Iterator> 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 action = new PrivilegedAction() {
                 public Boolean run() { return hasNextService(); }
           };
           return AccessController.doPrivileged(action, acc);
      }
}
public S next() {
       if (acc == null) {
            return nextService();
       } else {
            PrivilegedAction action = new PrivilegedAction() {
                 public S run() { return nextService(); }
            };
            return AccessController.doPrivileged(action, acc);
       }
}

懒迭代器的hasNextService方法首先会通过加载器通过文件全名获取配置对象 Enumeration 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类型,然后返回实例对象。

4 Java SPI 机制的缺陷

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

5 Spring SPI 机制

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

图片图片

1)创建 MyTestService 接口

public interface MyTestService {
    void printMylife();
}

2)创建 MyTestService 接口实现类

public class WorkTestService implements MyTestService {
    public WorkTestService(){
        System.out.println("WorkTestService");
    }
    public void printMylife() {
        System.out.println("我的工作");
    }
}
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 myTestServices = SpringFactoriesLoader.loadFactories(
            MyTestService.class,
            Thread.currentThread().getContextClassLoader()
);

for (MyTestService testService : myTestServices) {
     testService.printMylife();
}

运行结果:

FamilyTestService
WorkTestService
我的家庭
我的工作

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

和 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 extensionLoader = 
            ExtensionLoader.getExtensionLoader(Robot.class);
        Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
        optimusPrime.sayHello();
        Robot bumblebee = extensionLoader.getExtension("bumblebee");
        bumblebee.sayHello();
    }
}

测试结果如下 :

图片图片

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

来源:勇哥Java实战内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯