详细分析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();
    }
}
、
相关推荐
七星静香25 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员26 分钟前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU26 分钟前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea
stewie629 分钟前
在IDEA中使用Git
java·git
Elaine20239144 分钟前
06 网络编程基础
java·网络
G丶AEOM1 小时前
分布式——BASE理论
java·分布式·八股
落落鱼20131 小时前
tp接口 入口文件 500 错误原因
java·开发语言
想要打 Acm 的小周同学呀1 小时前
LRU缓存算法
java·算法·缓存
镰刀出海1 小时前
Recyclerview缓存原理
java·开发语言·缓存·recyclerview·android面试
阿伟*rui3 小时前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel