Java spi浅析

1.什么是SPI

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。
JAVA SPI = 基于接口的编程+策略模式+配置文件 的动态加载机制。

2.SPI和API的使用场景

API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。

SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。

(1)JDBC加载不同类型的数据库驱动 (2)日志门面接口实现类加载,SLF4J加载不同提供商的日志实现类 (3)Spring中大量使用了SPI。

3.SPI的简单实现

下面我们来简单实现一个jdk的SPI的简单实现。项目地址:https://github.com/hhhhhzj/java-spi-demo.git
首先第一步,定义一组接口:

package org.spi.demo
public interface IShout {
   public void shout();
}

这个接口分别有两个实现:

package org.spi.demo.impl
public class Cat implements IShout {
    public void shout() {
        System.out.println("miaomiao");
    }
}

package org.spi.demo.impl
public class Dog implements IShout {
    public void shout() {
        System.out.println("wangwang");
    }
}

然后需要在resources目录下新建META-INF/services目录,并且在这个目录下新建一个与上述接口的全限定名一致的文件,在这个文件中写入接口的实现类的全限定名.
文件位置

resources
    -META-INF
        -services
            -org.spi.demo.IShout

文件内容:

org.spi.demo.impl.Dog
org.spi.demo.impl.Cat

如果觉得写文件的方式比较麻烦,这里推荐引入google的autoService, 导入包和在接口类上添加@AutoService能够自动生成文件, 详细请看官方用例:https://github.com/google/auto/tree/master/service

4. 原理解析

通过上面简单的demo,可以看到最关键的实现就是ServiceLoader这个类,可以看下这个类的源码,如下:

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


    //扫描目录前缀
    private static final String PREFIX = "META-INF/services/";

    // 被加载的类或接口
    private final Class<S> service;

    // 用于定位、加载和实例化实现方实现的类的类加载器
    private final ClassLoader loader;

    // 上下文对象
    private final AccessControlContext acc;

    // 按照实例化的顺序缓存已经实例化的类
    private LinkedHashMap<String, S> providers = new LinkedHashMap<>();

    // 懒查找迭代器
    private java.util.ServiceLoader.LazyIterator lookupIterator;

    // 私有内部类,提供对所有的service的类的加载与实例化
    private class LazyIterator implements Iterator<S> {
        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        String nextName = null;

        //...
        private boolean hasNextService() {
            if (configs == null) {
                try {
                    //获取目录下所有的类
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    //...
                }
                //....
            }
        }

        private S nextService() {
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                //反射加载类
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
            }
            try {
                //实例化
                S p = service.cast(c.newInstance());
                //放进缓存
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                //..
            }
            //..
        }
    }
}

下面贴出比较直观的spi加载的主要流程供参考:


image.png

5. 总结

优点:

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

缺点:

  • 多个并发多线程使用ServiceLoader类的实例是不安全的
  • 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。

参考:
[1]. https://www.cnblogs.com/lbys/p/14286874.html
[2]. https://github.com/google/auto/tree/master/service
[3]. https://www.zhihu.com/question/486985113

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容