使用java的SPI机制实现插件功能

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/m0_38001814/article/details/89318552

前言

本文主要针对SPI工作机制的学习作记录,顺便分享下如何使用spi实现插件需求

1、SPI是什么?

SPI全称Service Provider Interface,我理解下就是一套服务发现机制,即为某个接口寻找它的实现类,整体机制图如下:

系统里抽象的各个模块,往往有不同的实现方案。在面向对象设计中,一般推荐模块之间基于接口进行编程,模块之间不对实现类硬编码,一旦代码里涉及了具体实现,就违反了可拔插原则,如果需要替换一种实现,就需要修改源代码,这很不可取。为了使模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。

SPI应运而生,它就是一种为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,所以SPI的核心思想就是解耦。

看到这里是不是发现SPI不就是面向接口思想的具体应用嘛,是的就是如此,举个通俗易懂的例子

sun公司在起初时只有一组jdbc类,那么后来这么多数据库厂商该如何实现相同的功能?这就依赖SPI机制了,sun公司开发了一组jdbc接口,只要每个数据库厂商均实现该接口,并按规定的SPI标准打包,最后再放到classpath下就ok啦,没有一点代码的侵入性

 

2、SPI的常见使用场景

准确来说,SPI适用于:调用者根据实际需求启用、扩展、或者替换框架的实现策略

2.1、如上文所说,数据库驱动加载实现类的加载,例:jdbc加载不同类型数据库的驱动

2.2、日志门面接口实现类加载,例:sla4j加载不同提供商的日志实现类

2.3、Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等

扫描二维码关注公众号,回复: 6010349 查看本文章

2.4、Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口

 

3、SPI的使用约定

要是有SPI机制需满足如下约定:

当服务提供者提供了某个接口的具体实现后,需在jar包的resource/META-INF/services目录下创建一个以“接口全限定名”为命名的文件,文件里填上该实现类的全路径。这样当外部程序装配这个模块的时候,就能通过该jar包中META-INF/services下的配置找到具体的实现类,并装载实例化,完成模块的注入。

基于这样一个约定,外部程序能就能很好的找到服务接口的实现类,而无需在代码里指定,jdk提供服务实现查找的工具类为:

java.util.ServiceLoader

4、SPI的具体使用demo

4.1、首先定义标准接口,如下:

public interface People {

    void say();
}

4.2、然后基于该接口,各服务提供者提供不同的实现,如:

public class Boy implements People {

    @Override
    public void say() {
        System.out.println("i'm a boy");
    }
}

public class Girl implements People {

    @Override
    public void say() {
        System.out.println("i'm a girl");
    }
}

4.3、提供者需在resouce目录下新建META-INF/services目录,然后新建一个以“接口全限定名”的文件,文件中填上该接口的具体的实现类,每个实现占一行,如下:

文件中内容如下:

impl.Girl
impl.Boy

4.4、外部程序装配时通过ServiceLoad装载类进行装载,如下:

    public void test01() {
        ServiceLoader<People> serviceLoader = ServiceLoader.load(People.class);
        serviceLoader.forEach(People::say);
    }

4.5、最后输出结果如下:

i'm a girl
i'm a boy

Process finished with exit code 0

5、SPI源码解读

主要就是针对ServiceLoad的源码进行分析,代码量很少看起来很容易,首先看下该类的各成员变量:

public final class ServiceLoader<S> implements Iterable<S> {

    // 即SPI约定路径
    private static final String PREFIX = "META-INF/services/";

    // 目标接口
    private final Class<S> service;

    // 指定类加载器
    private final ClassLoader loader;

    // 创建ServiceLoader时采用的访问控制上下文
    private final AccessControlContext acc;

    // 缓存providers,按实例化的顺序排列
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 懒查找迭代器,具体加载过程就在该类中实现
    private LazyIterator lookupIterator;

demo中使用的是ServiceLoader.load(Class<S> service)方法,未传入特定的classLoader,稍后讲插件时会用到另外一个重载方法,那么我们先看下源码:

    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 获取当前线程的类加载器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

进入重载方法时并未作什么处理,依然只是对ServiceLoader的一系列初始化操作:

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

应用程序通过迭代器接口获取对象实例时先判断ServiceLoader中的缓存providers是否有缓存实例对象,如果有则直接返回,如果没有则需执行类的加载,执行过程如下:

1、读取META-INF/services/目录下的配置文件,获取所有能被实例化的类的名称,值得注意的是,ServiceLoader可以跨越jar包获取META-INF下的配置文件,我们的插件demo就是基于此特性,此处的加载代码如下:

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

2、通过反射方法Class.forName()加载类对象,然后通过instance()方法实例化对象,因此实现类必须拥有一个无参构造方法,反射知识请走:反射和注解总结

3、把实例化后的类放进缓存map中,然后返回该类

6、使用SPI实现扩展插件小demo

有了上文的引入,不知各位对实现插件是不是心中已经有了想法?该需求正是基于ServiceLoader可以跨越jar包获取配置文件的特性,通过在加载时传入特定的classLoader来实现,那么问题即为如何构造该类加载器呢?一般使用到URLClassLoader类来访问外部jar包并封装classLoader,代码大致如下:

// 这里的localPath即为上传后的插件所在的本地服务器地址,针对分布式环境,一般可采用统一上传到mongo,在进行插件解析时下载指定插件并上传到本地
URL url = new URL("file:" + localPath);
// 使用URLClassLoader访问外部jar包并构建classLoader
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url}, Thread.currentThread().getContextClassLoader());

最后将该classLoader作为参数传入load()方法即可,上传下载方法请走:上传文件实现下载方法实现

7、SPI总结

优点:

使用java的SPI机制可以很好地实现解耦,使得第三方服务模块的装配控制逻辑可以与调用者的业务代码分离,而不是耦合在一起,应用程序可以根据实际业务情况启用框架扩展或替换框架组件

缺点:

1、虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类

2、并发多线程使用ServiceLoader类的实例是不安全的

到这里分享记录就完啦 ~ Winter is coming !  喜爱的美剧今天终于更新啦,最后一季第一集开刷!

猜你喜欢

转载自blog.csdn.net/m0_38001814/article/details/89318552