java SPI机制

可以参考的视频:

这个看了视频和文档都没有看懂呢?感觉现在的状态也太差了吧?有点奇怪了!

我觉得现在还是需要去指定一些目标!现在我不太想追求速度了,现在追求扎实的基础!(优先去先将思维导图的内容先学了!其他的若是实在看不懂也可以先放着!)

虽然看不懂,所以我觉得还是优先将流程保存下来,要么多看几遍,要么就放着。

什么是 SPI?

SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。

SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。

很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。

与 API 的区别

../../../../ZZZ-Misc/Z-Attachment/images/Pasted image 20241216185425.png|450

SLF4J 示例

../../../../ZZZ-Misc/Z-Attachment/images/Pasted image 20241205190953.png|475

Service Provider Interface

先建立 Service Provider Interface 项目:

│  service-provider-interface.iml
│
├─.idea
│  │  .gitignore
│  │  misc.xml
│  │  modules.xml
│  └─ workspace.xml
│
└─src
    └─edu
        └─jiangxuan
            └─up
                └─spi
                        Logger.java
                        LoggerService.java
                        Main.class

根据要求将 spijava 文件写好,运行 main 函数之后会发现:

此时我们只是空有接口,并没有为 Logger 接口提供任何的实现,所以输出结果中没有按照预期打印相应的结果。

这个时候将项目打包为 jar 包。

Service Provider

建立新项目 Service Provider:需要导入上面的jar

│  service-provider.iml
│
├─.idea
│  │  .gitignore
│  │  misc.xml
│  │  modules.xml
│  └─ workspace.xml
│
├─lib
│      service-provider-interface.jar
│
└─src
    ├─edu
    │  └─jiangxuan
    │      └─up
    │          └─spi
    │              └─service
    │                      Logback.java
    │
    └─META-INF
        └─services
                edu.jiangxuan.up.spi.Logger

新建 Logback 类来继承 Logger 类。

然后在新建 SPI 的全类名(edu.jiangxuan.up.spi.Logger),写入 Logback 的全类名:edu.jiangxuan.up.spi.service.Logback

Note

Java 中的 SPI 机制就是在每次类加载的时候会先去找到: class 相对目录下的 META-INF 文件夹下的 services 文件夹下的文件(即 edu.jiangxuan.up.spi.Logger),将这个文件夹下面的所有文件先加载到内存中,然后根据这些文件的文件名和里面的文件内容找到相应接口的具体实现类 (即 Logback.java),找到实现类后就可以通过反射去生成对应的对象,保存在一个 list 列表里面,所以可以通过迭代或者遍历的方式拿到对应的实例对象,生成不同的实现。

一些规范要求:

然后一样将 service-provider 项目打包成 jar 包,即服务提供方的实现。

通常我们导入 maven 的 pom 依赖就有点类似这种,只不过我们现在没有将这个 jar 包发布到 maven 公共仓库中,所以在需要使用的地方只能手动的添加到项目中。

测试

LoggerService loggerService = LoggerService.getService();
loggerService.info("你好");
loggerService.debug("测试Java SPI 机制");

输出:

//生效
Logback info 打印日志:你好
Logback debug 打印日志:测试 Java SPI 机制

//若不导入具体实现的包
info 中没有发现 Logger 服务提供者
debug 中没有发现 Logger 服务提供者

这里若想要换一种实现,只需要换一个实现(service-provider)的 jar 包(即 SLF4J 原理)。

ServiceLoader(SPI 的关键)

主要流程:

一个简易实现版本的 ServiceLoader

package edu.jiangxuan.up.service;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

public class MyServiceLoader<S> {

    // 对应的接口 Class 模板
    private final Class<S> service;

    // 对应实现类的 可以有多个,用 List 进行封装
    private final List<S> providers = new ArrayList<>();

    // 类加载器
    private final ClassLoader classLoader;

    // 暴露给外部使用的方法,通过调用这个方法可以开始加载自己定制的实现流程。
    public static <S> MyServiceLoader<S> load(Class<S> service) {
        return new MyServiceLoader<>(service);
    }

    // 构造方法私有化
    private MyServiceLoader(Class<S> service) {
        this.service = service;
        this.classLoader = Thread.currentThread().getContextClassLoader();
        doLoad();
    }

    // 关键方法,加载具体实现类的逻辑
    private void doLoad() {
        try {
            // 读取所有 jar 包里面 META-INF/services 包下面的文件,这个文件名就是接口名,然后文件里面的内容就是具体的实现类的路径加全类名
            Enumeration<URL> urls = classLoader.getResources("META-INF/services/" + service.getName());
            // 挨个遍历取到的文件
            while (urls.hasMoreElements()) {
                // 取出当前的文件
                URL url = urls.nextElement();
                System.out.println("File = " + url.getPath());
                // 建立链接
                URLConnection urlConnection = url.openConnection();
                urlConnection.setUseCaches(false);
                // 获取文件输入流
                InputStream inputStream = urlConnection.getInputStream();
                // 从文件输入流获取缓存
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                // 从文件内容里面得到实现类的全类名
                String className = bufferedReader.readLine();

                while (className != null) {
                    // 通过反射拿到实现类的实例
                    Class<?> clazz = Class.forName(className, false, classLoader);
                    // 如果声明的接口跟这个具体的实现类是属于同一类型,(可以理解为Java的一种多态,接口跟实现类、父类和子类等等这种关系。)则构造实例
                    if (service.isAssignableFrom(clazz)) {
                        Constructor<? extends S> constructor = (Constructor<? extends S>) clazz.getConstructor();
                        S instance = constructor.newInstance();
                        // 把当前构造的实例对象添加到 Provider的列表里面
                        providers.add(instance);
                    }
                    // 继续读取下一行的实现类,可以有多个实现类,只需要换行就可以了。
                    className = bufferedReader.readLine();
                }
            }
        } catch (Exception e) {
            System.out.println("读取文件异常。。。");
        }
    }

    // 返回spi接口对应的具体实现类列表
    public List<S> getProviders() {
        return providers;
    }
}

总结

SPI 的本质还是通过反射来完成的:我们按照规定将要暴露对外使用的具体实现类在 META-INF/services/ 文件下声明。

优点:通过 SPI 机制能够大大地提高接口设计的灵活性

缺点: