详细分析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();
    }
}
、
相关推荐
天天扭码29 分钟前
五天SpringCloud计划——DAY1之mybatis-plus的使用
java·spring cloud·mybatis
程序猿小柒35 分钟前
leetcode hot100【LeetCode 4.寻找两个正序数组的中位数】java实现
java·算法·leetcode
不爱学习的YY酱1 小时前
【操作系统不挂科】<CPU调度(13)>选择题(带答案与解析)
java·linux·前端·算法·操作系统
丁总学Java1 小时前
Maven项目打包,com.sun.tools.javac.processing
java·maven
kikyo哎哟喂1 小时前
Java 代理模式详解
java·开发语言·代理模式
duration~2 小时前
SpringAOP模拟实现
java·开发语言
小码ssim2 小时前
IDEA使用tips(LTS✍)
java·ide·intellij-idea
潜洋2 小时前
Spring Boot教程之五:在 IntelliJ IDEA 中运行第一个 Spring Boot 应用程序
java·spring boot·后端
暮志未晚Webgl2 小时前
109. UE5 GAS RPG 实现检查点的存档功能
android·java·ue5
小叶lr3 小时前
idea 配置 leetcode插件 代码模版
java·leetcode·intellij-idea