核心概念

SPI(Service Provider Interface) 是一种服务发现机制,用于在运行时动态加载接口的实现类,实现面向接口编程可插拔扩展

  • JDK SPI:Java 原生提供的 SPI 机制
  • Dubbo SPI:Dubbo 框架自己实现的增强版 SPI

核心目标:实现框架的高扩展性,让用户可以替换或增加功能模块,而不修改框架源码。

JDK SPI 机制

1. 实现方式

核心类java.util.ServiceLoader

使用步骤

(1)定义接口

package com.example;

public interface DataStorage {
    void save(String data);
}

(2)实现接口

// MySQL 实现
package com.example.impl;

public class MySQLStorage implements DataStorage {
    @Override
    public void save(String data) {
        System.out.println("保存到 MySQL: " + data);
    }
}

// Redis 实现
package com.example.impl;

public class RedisStorage implements DataStorage {
    @Override
    public void save(String data) {
        System.out.println("保存到 Redis: " + data);
    }
}

(3)配置文件

# 文件路径:META-INF/services/com.example.DataStorage
# 文件内容(每行一个实现类的全限定名)
com.example.impl.MySQLStorage
com.example.impl.RedisStorage

(4)加载使用

public class JdkSpiDemo {
    
    public static void main(String[] args) {
        // 加载所有实现类
        ServiceLoader<DataStorage> loader = ServiceLoader.load(DataStorage.class);
        
        // 遍历并使用
        for (DataStorage storage : loader) {
            storage.save("test data");
        }
        
        // 输出:
        // 保存到 MySQL: test data
        // 保存到 Redis: test data
    }
}

2. JDK SPI 的源码实现

public final class ServiceLoader<S> implements Iterable<S> {
    
    private static final String PREFIX = "META-INF/services/";
    
    private final Class<S> service;
    private final ClassLoader loader;
    private LinkedHashMap<String, S> providers = new LinkedHashMap<>();
    
    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return new ServiceLoader<>(service, cl);
    }
    
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = svc;
        loader = cl;
        reload();
    }
    
    public void reload() {
        providers.clear();
        // 1. 读取配置文件
        String fullName = PREFIX + service.getName();
        Enumeration<URL> configs = loader.getResources(fullName);
        
        // 2. 解析配置文件
        while (configs.hasMoreElements()) {
            URL url = configs.nextElement();
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(url.openStream(), "UTF-8"));
            
            String line;
            while ((line = reader.readLine()) != null) {
                // 3. 反射创建实例
                Class<?> clazz = Class.forName(line, false, loader);
                S provider = service.cast(clazz.newInstance());
                providers.put(line, provider);
            }
        }
    }
    
    @Override
    public Iterator<S> iterator() {
        return providers.values().iterator();
    }
}

3. JDK SPI 的缺点

(1)一次性加载所有实现

// 问题:即使只需要一个实现,也会加载并实例化所有实现类
ServiceLoader<DataStorage> loader = ServiceLoader.load(DataStorage.class);

// 假设有 10 个实现类,全部被实例化
// 造成资源浪费

(2)不支持按需加载

// 无法指定加载某个实现
// 只能遍历所有实现,手动筛选

for (DataStorage storage : loader) {
    if (storage instanceof MySQLStorage) {
        // 找到需要的实现
        storage.save("data");
        break;
    }
}

(3)不支持依赖注入

// 实现类的构造函数必须是无参的
public class MySQLStorage implements DataStorage {
    // 无法通过构造函数注入依赖
    public MySQLStorage() { }
}

// 如果需要依赖,只能硬编码
public class MySQLStorage implements DataStorage {
    private DataSource dataSource = new HikariDataSource(...);  // 硬编码
}

(4)不支持 AOP 和 IOC

// 实例化的对象是普通对象,没有经过 Spring 容器管理
// 无法使用 @Autowired、@Transactional 等注解

(5)获取扩展失败时,处理不友好

// 如果某个实现类加载失败,整个 ServiceLoader 都失败
// 无法跳过错误的实现,继续加载其他实现

Dubbo SPI 机制

1. 实现方式

核心类org.apache.dubbo.common.extension.ExtensionLoader

使用步骤

(1)定义接口(添加 @SPI 注解)

package com.example;

import org.apache.dubbo.common.extension.SPI;

@SPI("mysql")  // 指定默认实现
public interface DataStorage {
    void save(String data);
}

(2)实现接口

// MySQL 实现
package com.example.impl;

public class MySQLStorage implements DataStorage {
    @Override
    public void save(String data) {
        System.out.println("保存到 MySQL: " + data);
    }
}

// Redis 实现
package com.example.impl;

public class RedisStorage implements DataStorage {
    @Override
    public void save(String data) {
        System.out.println("保存到 Redis: " + data);
    }
}

(3)配置文件(Key-Value 格式)

# 文件路径:META-INF/dubbo/com.example.DataStorage
# 文件内容(Key=Value 格式)
mysql=com.example.impl.MySQLStorage
redis=com.example.impl.RedisStorage

(4)加载使用

public class DubboSpiDemo {
    
    public static void main(String[] args) {
        // 1. 获取 ExtensionLoader
        ExtensionLoader<DataStorage> loader = 
            ExtensionLoader.getExtensionLoader(DataStorage.class);
        
        // 2. 按名称加载指定实现(按需加载)
        DataStorage mysql = loader.getExtension("mysql");
        mysql.save("data1");
        
        DataStorage redis = loader.getExtension("redis");
        redis.save("data2");
        
        // 3. 获取默认实现
        DataStorage defaultStorage = loader.getDefaultExtension();
        defaultStorage.save("data3");
        
        // 输出:
        // 保存到 MySQL: data1
        // 保存到 Redis: data2
        // 保存到 MySQL: data3  (默认实现)
    }
}

2. Dubbo SPI 的核心源码

public class ExtensionLoader<T> {
    
    // 扩展加载器缓存
    private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = 
        new ConcurrentHashMap<>();
    
    // 扩展实例缓存(单例)
    private final ConcurrentMap<String, Holder<Object>> cachedInstances = 
        new ConcurrentHashMap<>();
    
    // 扩展类缓存
    private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
    
    /**
     * 获取 ExtensionLoader(单例)
     */
    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        // 从缓存获取
        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        
        if (loader == null) {
            // 创建新的 ExtensionLoader
            loader = new ExtensionLoader<>(type);
            EXTENSION_LOADERS.putIfAbsent(type, loader);
        }
        
        return loader;
    }
    
    /**
     * 按名称获取扩展实例(单例 + 延迟加载)
     */
    public T getExtension(String name) {
        // 1. 从缓存获取
        Holder<Object> holder = cachedInstances.get(name);
        if (holder == null) {
            holder = new Holder<>();
            cachedInstances.putIfAbsent(name, holder);
        }
        
        Object instance = holder.get();
        if (instance == null) {
            synchronized (holder) {
                instance = holder.get();
                if (instance == null) {
                    // 2. 创建扩展实例
                    instance = createExtension(name);
                    holder.set(instance);
                }
            }
        }
        
        return (T) instance;
    }
    
    /**
     * 创建扩展实例
     */
    private T createExtension(String name) {
        // 1. 加载扩展类
        Class<?> clazz = getExtensionClasses().get(name);
        
        // 2. 反射创建实例
        T instance = (T) clazz.newInstance();
        
        // 3. 依赖注入(IOC)
        injectExtension(instance);
        
        // 4. AOP 包装
        instance = wrapExtension(instance);
        
        return instance;
    }
    
    /**
     * 依赖注入
     */
    private T injectExtension(T instance) {
        // 遍历所有 setter 方法
        for (Method method : instance.getClass().getMethods()) {
            if (method.getName().startsWith("set") 
                && method.getParameterTypes().length == 1) {
                
                // 获取依赖类型
                Class<?> pt = method.getParameterTypes()[0];
                
                try {
                    // 自动注入扩展依赖
                    Object object = objectFactory.getExtension(pt, name);
                    method.invoke(instance, object);
                } catch (Exception e) {
                    // 注入失败,继续
                }
            }
        }
        
        return instance;
    }
    
    /**
     * AOP 包装
     */
    private T wrapExtension(T instance) {
        List<Class<?>> wrapperClasses = cachedWrapperClasses;
        
        if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
            for (Class<?> wrapperClass : wrapperClasses) {
                // 用 Wrapper 类包装原始实例
                instance = (T) wrapperClass.getConstructor(type).newInstance(instance);
            }
        }
        
        return instance;
    }
    
    /**
     * 加载扩展类
     */
    private Map<String, Class<?>> getExtensionClasses() {
        Map<String, Class<?>> classes = cachedClasses.get();
        
        if (classes == null) {
            synchronized (cachedClasses) {
                classes = cachedClasses.get();
                if (classes == null) {
                    // 从配置文件加载
                    classes = loadExtensionClasses();
                    cachedClasses.set(classes);
                }
            }
        }
        
        return classes;
    }
    
    /**
     * 从配置文件加载扩展类
     */
    private Map<String, Class<?>> loadExtensionClasses() {
        Map<String, Class<?>> extensionClasses = new HashMap<>();
        
        // 1. 读取配置文件(支持多个目录)
        String[] directories = new String[] {
            "META-INF/dubbo/internal/",
            "META-INF/dubbo/",
            "META-INF/services/"
        };
        
        for (String directory : directories) {
            String fileName = directory + type.getName();
            Enumeration<URL> urls = classLoader.getResources(fileName);
            
            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                
                // 2. 解析配置文件(Key=Value 格式)
                Properties properties = new Properties();
                properties.load(url.openStream());
                
                for (Map.Entry<Object, Object> entry : properties.entrySet()) {
                    String name = (String) entry.getKey();
                    String className = (String) entry.getValue();
                    
                    // 3. 加载类
                    Class<?> clazz = Class.forName(className, true, classLoader);
                    extensionClasses.put(name, clazz);
                }
            }
        }
        
        return extensionClasses;
    }
}

3. Dubbo SPI 的增强特性

(1)按需加载(延迟加载)

ExtensionLoader<DataStorage> loader = 
    ExtensionLoader.getExtensionLoader(DataStorage.class);

// 只加载需要的实现,不会加载所有实现
DataStorage mysql = loader.getExtension("mysql");

(2)依赖注入(IOC)

@SPI
public interface DataStorage {
    void save(String data);
}

public class MySQLStorage implements DataStorage {
    
    // 自动注入依赖(通过 setter)
    private Logger logger;
    
    public void setLogger(Logger logger) {
        this.logger = logger;
    }
    
    @Override
    public void save(String data) {
        logger.info("保存数据: " + data);
    }
}

(3)AOP 支持(Wrapper 机制)

// 原始实现
public class MySQLStorage implements DataStorage {
    @Override
    public void save(String data) {
        System.out.println("保存到 MySQL: " + data);
    }
}

// Wrapper 包装类(自动识别并包装)
public class MonitorWrapper implements DataStorage {
    
    private final DataStorage storage;
    
    public MonitorWrapper(DataStorage storage) {
        this.storage = storage;
    }
    
    @Override
    public void save(String data) {
        long start = System.currentTimeMillis();
        
        storage.save(data);  // 调用原始实现
        
        long cost = System.currentTimeMillis() - start;
        System.out.println("耗时: " + cost + "ms");
    }
}

// 配置文件
// META-INF/dubbo/com.example.DataStorage
// mysql=com.example.impl.MySQLStorage

// 调用时自动包装
DataStorage storage = loader.getExtension("mysql");
storage.save("data");
// 输出:
// 保存到 MySQL: data
// 耗时: 10ms

(4)自适应扩展(@Adaptive)

@SPI("mysql")
public interface DataStorage {
    
    // 根据 URL 参数动态选择实现
    @Adaptive({"storage.type"})
    void save(URL url, String data);
}

// 使用
URL url = URL.valueOf("dubbo://localhost/DataStorage?storage.type=redis");
DataStorage storage = loader.getAdaptiveExtension();
storage.save(url, "data");  // 动态选择 RedisStorage

(5)自动激活(@Activate)

@SPI
public interface Filter {
    void filter(Invocation invocation);
}

@Activate(group = "consumer", order = -1)
public class ConsumerFilter implements Filter {
    // Consumer 端自动激活
}

@Activate(group = "provider", order = 1)
public class ProviderFilter implements Filter {
    // Provider 端自动激活
}

// 根据条件自动激活
List<Filter> filters = loader.getActivateExtension(url, "filter", "consumer");

JDK SPI vs Dubbo SPI 对比

维度 JDK SPI Dubbo SPI
加载方式 一次性加载所有实现 按需加载(延迟加载)
配置格式 实现类全限定名(列表) Key=Value 格式
获取方式 遍历所有实现 按名称获取指定实现
依赖注入 ❌ 不支持 ✅ 支持(IOC)
AOP ❌ 不支持 ✅ 支持(Wrapper)
自适应扩展 ❌ 不支持 ✅ 支持(@Adaptive)
自动激活 ❌ 不支持 ✅ 支持(@Activate)
扩展缓存 ❌ 无缓存 ✅ 单例缓存
失败处理 一个失败全部失败 跳过失败的扩展
性能 低(重复加载) 高(缓存 + 延迟加载)

实际应用

JDK SPI 的应用

// JDBC 驱动加载
// META-INF/services/java.sql.Driver
// com.mysql.cj.jdbc.Driver
// org.postgresql.Driver

Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection(url, user, password);

Dubbo SPI 的应用

// 负载均衡策略
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
    <T> Invoker<T> select(List<Invoker<T>> invokers, Invocation invocation);
}

// META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance
// random=org.apache.dubbo.rpc.cluster.loadbalance.RandomLoadBalance
// roundrobin=org.apache.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance
// leastactive=org.apache.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance

// 使用
@Reference(loadbalance = "leastactive")
private UserService userService;

答题总结

Dubbo SPI 和 JDK SPI 的核心区别

1. 加载方式

  • JDK SPI:一次性加载所有实现,造成资源浪费
  • Dubbo SPI:按需加载,延迟加载,性能更高

2. 配置格式

  • JDK SPIMETA-INF/services/接口全限定名,文件内容是实现类列表
  • Dubbo SPIMETA-INF/dubbo/接口全限定名,文件内容是 Key=Value 格式

3. 获取方式

  • JDK SPI:遍历所有实现,手动筛选
  • Dubbo SPI:按名称直接获取指定实现

4. 增强特性

  • 依赖注入(IOC):Dubbo SPI 支持,JDK SPI 不支持
  • AOP(Wrapper):Dubbo SPI 支持,JDK SPI 不支持
  • 自适应扩展(@Adaptive):Dubbo SPI 支持,动态选择实现
  • 自动激活(@Activate):Dubbo SPI 支持,根据条件自动加载
  • 单例缓存:Dubbo SPI 缓存扩展实例,JDK SPI 每次都创建新实例

5. 使用对比

// JDK SPI
ServiceLoader<DataStorage> loader = ServiceLoader.load(DataStorage.class);
for (DataStorage storage : loader) {
    // 遍历所有实现
}

// Dubbo SPI
ExtensionLoader<DataStorage> loader = 
    ExtensionLoader.getExtensionLoader(DataStorage.class);
DataStorage storage = loader.getExtension("mysql");  // 按名称获取

面试技巧:强调 Dubbo SPI 是对 JDK SPI 的全面增强,解决了 JDK SPI 的性能问题(一次性加载)、灵活性问题(无法按需加载)和功能缺失(不支持 IOC、AOP)。可以结合 Dubbo 的实际应用(负载均衡、协议、序列化等)说明 SPI 机制的重要性。