详细分析Java中的SPI机制(附Demo)

目录

  • 前言
  • [1. 基本知识](#1. 基本知识)
  • [2. Demo](#2. Demo)
  • [3. 解读源码](#3. 解读源码)

前言

相关的Java知识推荐阅读:

  1. java框架 零基础从入门到精通的学习路线 附开源项目面经等(超全)
  2. 【Java项目】实战CRUD的功能整理(持续更新)

1. 基本知识

SPI(Service Provider Interface) 是一种服务发现机制,允许 Java 应用程序动态地加载和使用服务实现

SPI 机制是 Java 提供的服务加载机制的一部分,可以在运行时找到和加载实现接口的服务提供者,而不需要在编译时将这些实现硬编码到代码中

基本的知识点如下:

  • 定义 SPI 接口:SPI 需要定义一个接口或抽象类,作为服务的规范

  • 提供服务的实现:创建实现该接口的具体服务实现类

  • 配置服务提供者: 在 META-INF/services/ 目录下创建一个文件,文件名为接口的完全限定名,文件内容是实现该接口的类的完全限定名

  • 加载服务实现:使用 ServiceLoader 类来动态加载服务实现

了解基本的知识点,结合Demo进行了解

2. Demo

定义服务接口:

java 复制代码
public interface GreetingService {
    void greet(String name);
}

提供服务的实现类1:

java 复制代码
public class EnglishGreetingService implements GreetingService {
    @Override
    public void greet(String name) {
        System.out.println("Hello, " + name);
    }
}

实现类2:

java 复制代码
public class SpanishGreetingService implements GreetingService {
    @Override
    public void greet(String name) {
        System.out.println("Hola, " + name);
    }
}

配置服务提供者:

在 META-INF/services/ 目录下创建一个文件 com.example.GreetingService,内容为服务实现的完全限定名:

java 复制代码
com.example.EnglishGreetingService
com.example.SpanishGreetingService

最终加载服务实现类:

java 复制代码
import java.util.ServiceLoader;

public class GreetingServiceLoader {
    public static void main(String[] args) {
        // 使用 ServiceLoader 加载 GreetingService 接口的所有实现类
        ServiceLoader<GreetingService> loader = ServiceLoader.load(GreetingService.class);
        
        // 遍历所有加载的服务实现
        for (GreetingService service : loader) {
            // 调用服务实现的方法
            service.greet("码农研究僧");
        }
    }
}

执行截图如下:

之所以读取此处的配置文件,可以通过源码查看:

基本的运行机制如下:

  1. 编写服务接口:

    定义一个接口或抽象类,该接口定义了服务提供者需要实现的功能

  2. 实现服务接口:

    创建一个或多个类实现该服务接口

    这些类是服务的实际提供者

  3. 配置服务提供者:

    META-INF/services/ 目录下创建一个文件,文件名为接口的完全限定名(即接口的全类名)

    在文件中列出所有实现该接口的服务提供者的完全限定名,每行一个类名

  4. 运行时加载服务:

    4.1 当应用程序运行并调用 ServiceLoader.load(GreetingService.class) 时,ServiceLoader 会读取 META-INF/services/com.example.GreetingService 文件,获取所有服务实现的类名

    ServiceLoader 通过反射机制动态加载这些类,并创建它们的实例

    4.2 使用反射 (Class.forName()) 加载服务实现类,通过 Class.newInstance() 创建服务实例,将服务实例缓存到 providers 中

  5. 使用服务:

    使用 ServiceLoader 的 iterator() 方法遍历所有实现,并调用它们的方法

3. 解读源码

解读部分源码:

reload方法:

  • 清空缓存的服务提供者 providers
  • 重新创建一个 LazyIterator 实例,用于重新查找和加载服务实现
java 复制代码
public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

parse方法:

  • parse() 方法读取并解析配置文件中的服务实现类名
  • 使用 BufferedReader 按行读取文件,并调用 parseLine() 处理每一行
  • 将有效的类名添加到 names 列表中,并返回一个迭代器
java 复制代码
private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError {
    InputStream in = null;
    BufferedReader r = null;
    ArrayList<String> names = new ArrayList<>();
    try {
        in = u.openStream();
        r = new BufferedReader(new InputStreamReader(in, "utf-8"));
        int lc = 1;
        while ((lc = parseLine(service, u, r, lc, names)) >= 0);
    } catch (IOException x) {
        fail(service, "Error reading configuration file", x);
    } finally {
        try {
            if (r != null) r.close();
            if (in != null) in.close();
        } catch (IOException y) {
            fail(service, "Error closing configuration file", y);
        }
    }
    return names.iterator();
}

parseLine方法:

  • parseLine() 方法处理服务配置文件中的每一行
  • 忽略注释(以 # 开头的行)和空行
  • 验证类名是否合法,并将有效的类名添加到 names 列表
java 复制代码
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc, List<String> names) throws IOException, ServiceConfigurationError {
    String ln = r.readLine();
    if (ln == null) {
        return -1;
    }
    int ci = ln.indexOf('#');
    if (ci >= 0) ln = ln.substring(0, ci);
    ln = ln.trim();
    int n = ln.length();
    if (n != 0) {
        if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
            fail(service, u, lc, "Illegal configuration-file syntax");
        int cp = ln.codePointAt(0);
        if (!Character.isJavaIdentifierStart(cp))
            fail(service, u, lc, "Illegal provider-class name: " + ln);
        for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
            cp = ln.codePointAt(i);
            if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
        }
        if (!providers.containsKey(ln) && !names.contains(ln))
            names.add(ln);
    }
    return lc + 1;
}

LazyIterator 内部类:

  • LazyIterator 负责延迟加载服务提供者
  • hasNextService() 方法查找和加载服务配置文件中的服务实现类
  • nextService() 方法使用反射创建服务实例,并返回它
  • hasNext() 和 next() 方法实现了 Iterator 接口,用于遍历服务提供者
java 复制代码
private class LazyIterator implements Iterator<S> {
    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null;

    private LazyIterator(Class<S> service, ClassLoader loader) {
        this.service = service;
        this.loader = loader;
    }

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

    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
    }

    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() {
        return nextService();
    }
}
、
相关推荐
带刺的坐椅几秒前
Solon TeamAgent 协作协议:从 SEQUENTIAL 流水线到 HIERARCHICAL 主管团队
java·ai·llm·agent·solon
wuqingshun3141599 分钟前
SpringBoot是如何实现自动配置的
java·spring boot·后端
用户0537040314312 分钟前
我的学习记录泛型
java
zhouhui00114 分钟前
AI帮我写了个Spring Boot校验,线上漏掉了这组边界条件
java·spring boot·redis·ai编程
JRedisX21 分钟前
Java从零手写企业级内存数据库
java
萧瑟余晖23 分钟前
JDK 20 新特性详解
java·开发语言
hdsoft_huge25 分钟前
SpringBoot系列17:日志体系Logback配置,日志分割、脱敏、异常堆栈收集规范(生产级落地)
java·spring boot·logback
吃饱了得干活28 分钟前
扫码登录如何实现?从原理到实战
java·spring boot·redis
JRedisX31 分钟前
JRedisX 项目骨架搭建指南 从零开始 — 手把手教你搭
java
zx11545037 分钟前
Java 反射 (SpringBoot篇)
java·架构