深入探讨Java SPI机制及其应用场景

 更新时间:2023年04月26日 10:17:40   作者:顽石九变  
本文深入介绍了Java SPI机制的原理和实现方式,以及在Java中如何使用SPI实现可插拔的组件化架构,包括示例代码和应用场景分析,帮助读者更好地理解和应用该机制

一、什么是SPI

SPI全称Service Provider Interface,是Java提供的一种服务发现机制。实现服务接口和服务实现的解耦。

Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,实现不修改任何代码的情况下切换不同的实现。

二、使用场景

很多开源第三方jar包都有基于SPI的实现,在jar包META-INF/services中都有相关配置文件。

如下几个常见的场景:

1)JDBC加载不同类型的数据库驱动

2)Slf4j日志框架

3)Dubbo框架

三、使用步骤示例

假设有个上传附件的场景,可以上传到不同的云储存(如阿里云OSS,亚马逊S3),那么基于Java SPI机制的实现,我们应该做如下步骤:

步骤1、创建4个工程

SPI的核心就是实现服务接口和服务实现的解耦,所以我们不能将接口和实现放在一个工程里面。

  • spi-file-upload,在这定义附件上传接口IFileUpload
  • spi-file-upload-oss,实现附件上传到oss,FileUploadOss实现接口IFileUpload
  • spi-file-upload-s3,实现附件上传到s3,FileUploadS3实现接口IFileUpload
  • spi-file-upload-test,通过ServiceLoader加载接口实现,进行测试

步骤2、 在工程spi-file-upload创建接口IFileUpload

接口代码示例

package com.hj.test.file.oss;
/**
 * 文件上传接口
 */
public interface IFileUpload {
    void upload(String fileName);
}

步骤3、分别创建接口实现类FileUploadOss、FileUploadS3

1)FileUploadOss

在工程的 spi-file-upload-oss 的 resources目录下创建目录META-INF/services,并在该目录中创建以接口IFileUpload全路径命名的文件(com.hj.test.file.IFileUpload),文件内容是接口实现类 com.hj.test.file.oss.FileUploadOss

package com.hj.test.file.oss;
import com.hj.test.file.IFileUpload;
public class FileUploadOss implements IFileUpload {
    @Override
    public void upload(String fileName) {
        System.out.println("上传到阿里云OSS..." + fileName);
    }
}

2)FileUploadS3

在工程的 spi-file-upload-s3 的 resources目录下创建目录META-INF/services,并在该目录中创建以接口IFileUpload全路径命名的文件(com.hj.test.file.IFileUpload),文件内容是接口实现类 com.hj.test.file.s3.FileUploadS3

package com.hj.test.file.s3;
import com.hj.test.file.IFileUpload;
public class FileUploadS3 implements IFileUpload {
    @Override
    public void upload(String fileName) {
        System.out.println("上传到亚马逊s3..." + fileName);
    }
}

步骤4、在工程spi-file-upload-test中创建测试调用类

1)在pom.xml中引入3个依赖工程

<dependency>
    <groupId>com.hj</groupId>
    <artifactId>spi-file-upload</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>com.hj</groupId>
    <artifactId>spi-file-upload-oss</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>com.hj</groupId>
    <artifactId>spi-file-upload-s3</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

2)测试实现

package com.hj.test.file.test;
import com.hj.test.file.IFileUpload;
import java.util.Iterator;
import java.util.ServiceLoader;
public class FileTest{
    public static void main(String[] args) {
        ServiceLoader<IFileUpload> loader = ServiceLoader.load(IFileUpload.class);
        for(Iterator<IFileUpload> it = loader.iterator(); it.hasNext();){
            IFileUpload file = it.next();
            file.upload("测试文件上传");
        }
    }
}

控制台输出

上传到阿里云OSS...测试文件上传
上传到亚马逊s3...测试文件上传

如果哪天不想要传到s3,只需要把jar包依赖去掉就可以,无需改代码

四、原理解析

1、SPI的核心就是ServiceLoader.load()方法

总结如下:

  1. 调用ServiceLoader.load(),创建一个ServiceLoader实例对象
  2. 创建LazyIterator实例对象lookupIterator
  3. 通过lookupIterator.hasNextService()方法读取固定目录META-INF/services/下面service全限定名文件,放在Enumeration对象configs
  4. 解析configs得到迭代器对象Iterator<String> pending
  5. 通过lookupIterator.nextService()方法初始化读取到的实现类,通过Class.forName()初始化

从上面的步骤可以总结以下几点

  • 实现类工程必须创建定目录META-INF/services/,并创建service全限定名文件,文件内容是实现类全限定名
  • 实现类必须有一个无参构造函数

2、ServiceLoader核心代码介绍

public final class ServiceLoader<S>
    implements Iterable<S>
{
    private static final String PREFIX = "META-INF/services/";
    // The class or interface representing the service being loaded
    private final Class<S> service;
    // The class loader used to locate, load, and instantiate providers
    private final ClassLoader loader;
    // The access control context taken when the ServiceLoader is created
    private final AccessControlContext acc;
    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // The current lazy-lookup iterator
    private LazyIterator lookupIterator;
public static <S> ServiceLoader<S> load(Class<S> service,
                                        ClassLoader loader)
{
    return new ServiceLoader<>(service, loader);
}
public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

通过方法iterator()生成迭代器,内部调用LazyIterator实例对象

public Iterator<S> iterator() {
    return new Iterator<S>() {
        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();
        public boolean hasNext() {
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }
        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }
        public void remove() {
            throw new UnsupportedOperationException();
        }
    };
}

内部类LazyIterator,读取配置文件META-INF/services/

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() {
        if (acc == null) {
            return nextService();
        } else {
            PrivilegedAction<S> action = new PrivilegedAction<S>() {
                public S run() { return nextService(); }
            };
            return AccessController.doPrivileged(action, acc);
        }
    }
    public void remove() {
        throw new UnsupportedOperationException();
    }
}

到此这篇关于深入探讨Java SPI机制及其应用场景的文章就介绍到这了,更多相关Java SPI机制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 基于java中cookie和session的比较

    基于java中cookie和session的比较

    下面小编就为大家带来一篇基于java中cookie和session的比较。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-08-08
  • Java 基于雪花算法生成分布式id

    Java 基于雪花算法生成分布式id

    SnowFlake 算法(雪花算法), 是Twitter开源的分布式id生成算法。其核心思想就是: 使用一个64 bit的long型的数字作为全局唯一id。本文讲述Java 基于雪花算法生成分布式id的方法
    2021-06-06
  • 基于Java Agent的premain方式实现方法耗时监控问题

    基于Java Agent的premain方式实现方法耗时监控问题

    javaagent是在JDK5之后提供的新特性,也可以叫java代理,这篇文章主要介绍了基于Java Agent的premain方式实现方法耗时监控问题,需要的朋友可以参考下
    2022-10-10
  • 浅谈spring容器中bean的初始化

    浅谈spring容器中bean的初始化

    下面小编就为大家带来一篇浅谈spring容器中bean的初始化。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-06-06
  • Java中LinkedHashMap的实现详解

    Java中LinkedHashMap的实现详解

    LinkedHashMap是Java中的一个Map容器,它继承自HashMap,并且还可以对元素进行有序存储,本文将介绍LinkedHashMap的实现原理以及使用方法,并且提供相应的测试用例和全文小结,需要的可以参考下
    2023-09-09
  • Java反射技术原理与用法实例分析

    Java反射技术原理与用法实例分析

    这篇文章主要介绍了Java反射技术原理与用法,结合实例形式分析了Java反射技术的基本概念、功能、原理、用法及操作注意事项,需要的朋友可以参考下
    2020-04-04
  • Java实现多项式除法的代码示例

    Java实现多项式除法的代码示例

    今天小编就为大家分享一篇关于Java实现多项式除法的代码示例,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2018-10-10
  • 一文精通Java 多线程之全方位解读

    一文精通Java 多线程之全方位解读

    Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务,多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销
    2021-10-10
  • 探讨Java 将Markdown文件转换为Word和PDF文档

    探讨Java 将Markdown文件转换为Word和PDF文档

    这篇文章主要介绍了Java 将Markdown文件转换为Word和PDF文档,本文通过分步指南及代码示例展示了如何将 Markdown 文件转换为 Word 文档和 PDF 文件,需要的朋友可以参考下
    2024-07-07
  • RocketMQ整合SpringBoot实现生产级二次封装

    RocketMQ整合SpringBoot实现生产级二次封装

    本文主要介绍了RocketMQ整合SpringBoot实现生产级二次封装,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-06-06

最新评论