Java 插件模式SPI学习与理解(Java SPI 、Spring SPI、Dubbo SPI)

哈根达斯
2025-01-14 / 0 评论 / 5 阅读 / 正在检测是否收录...

SPI 全称为 Service Provider Interface,是一种服务发现机制,其核心在于将接口实现类的全限定名配置在文件中,由服务加载器读取并加载实现类,从而可在运行时为接口动态替换实现类,为程序提供拓展功能。

1. 重新阐述 Java SPI 示例

  • 定义接口和实现类
  • 首先定义一个名为 Robot 的接口,包含 sayHello 方法。

    public interface Robot {  
       void sayHello();  
    }  

然后创建两个实现类 OptimusPrimeBumblebee,它们都实现了 Robot 接口并实现了 sayHello 方法。

    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 文件夹下创建名为 org.apache.spi.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");  
            // 两种遍历模式
            serviceLoader.forEach(Robot::sayHello);  
            Iterator<Robot> iterator = serviceLoader.iterator();  
            while (iterator.hasNext()) {  
                Robot robot = iterator.next();  
                // 可以调用 robot.sayHello() 进行测试
            }  
        }  
    }  

2. 经典 Java SPI 应用:JDBC DriverManager

  • 传统 JDBC 驱动加载
    JDBC4.0 之前,需要先使用 Class.forName("com.mysql.jdbc.Driver") 加载数据库驱动,然后进行连接操作。
    // 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);  
  • 使用 SPI 后的 JDBC 驱动加载

    • JDBC4.0 之后利用 Java 的 SPI 扩展机制,无需手动调用 Class.forName 加载驱动,可直接获取连接。
    • DriverManager 类是驱动管理器,在其静态初始化块中调用 loadInitialDrivers 方法,其中涉及 SPI 的使用:
    static {  
        loadInitialDrivers();  
        println("JDBC DriverManager initialized");  
    }  

加载驱动的四个步骤:

  1. 从系统变量获取驱动定义。
  2. 用 SPI 获取驱动实现类(字符串形式)。
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);  
  1. 遍历使用 SPI 获取的实现,实例化各实现类。
        Iterator<Driver> driversIterator = loadedDrivers.iterator();  
        while(driversIterator.hasNext()) {  
            driversIterator.next();  
        }  
  1. 根据第一步的驱动列表实例化具体实现类。

3. Java SPI 机制源码解析

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

该方法根据服务类型和类加载器创建 ServiceLoader 对象。

- `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 对象,只有调用迭代方法时才执行加载逻辑。

  • 迭代器的 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);  
        }  
    }  

该方法最终调用 hasNextService 方法,会通过加载器获取配置对象并解析 META-INF/services/ 目录下的文件。

  • 迭代器的 next() 方法
    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);  
        }  
    }  

本质是调用 nextService 方法,通过反射加载类对象,实例化类并缓存到 providers 对象中。

4. Java SPI 机制的缺陷

  • 无法按需加载,会遍历并实例化所有实现,即使部分实现类不需要或实例化耗时。
  • 获取实现类的方式仅能通过 Iterator,不够灵活,不能根据参数获取特定实现类。
  • 多线程使用 ServiceLoader 实例时不安全。

5. Spring SPI 机制

创建接口和实现类
定义 MyTestService 接口:

    public interface MyTestService {  
        void printMylife();  
    }  

创建实现类 WorkTestServiceFamilyTestService

    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("我的家庭");  
        }  
    }  
  • 配置文件

META-INF/spring.factories 中配置接口和实现类:

com.courage.platform.sms.demo.service.MyTestService = com.courage.platform.sms.demo.service.impl.FamilyTestService,com.courage.platform.sms.demo.service.impl.WorkTestService  
  • 测试代码
    List<MyTestService> myTestServices = SpringFactoriesLoader.loadFactories(  
            MyTestService.class,  
            Thread.currentThread().getContextClassLoader()  
    );  
    for (MyTestService testService : myTestServices) {  
        testService.printMylife();  
    }  
  • 与 Java SPI 的区别

    • Java SPI 一个服务接口对应一个配置文件,Spring SPI 一个 spring.factories 配置文件存放多个接口及实现类,以接口全限定名作为 key,实现类作为 value,多个实现类用逗号分隔。
    • 两者都无法获取特定的实现,只能按顺序获取所有实现。

6. Dubbo SPI 机制

  • 配置文件

    • 配置文件放在 META-INF/dubbo 路径下,以键值对方式配置实现类:
    optimusPrime = org.apache.spi.OptimusPrime  
    bumblebee = org.apache.spi.Bumblebee  
  • 测试代码

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();  
        }  
}  
  • 特点

    • 支持按需加载接口实现类,可通过键值对方式指定要加载的实现。
    • 相比 Java SPI 增加了 IOC 和 AOP 等特性。

近期才哥整理出了一个可用于快速刷面试题的小程序,其中收录了常见面试题及其答案,涵盖了基础、并发、JVM、MySQL、Redis、Spring、SpringMVC、SpringBoot、SpringCloud、消息队列等多个类型,感兴趣的可以点击下方试试。

才哥IT刷题小程序

0

评论 (0)

取消