欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JAVA SPI详解

程序员文章站 2022-07-09 13:47:21
...

SPI是什么?

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现

使用场景 

  • 数据库驱动加载接口实现类的加载

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

  • 日志门面接口实现类加载

              SLF4J加载不同提供商的日志实现类

  • Spring

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

  • Dubbo

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

使用介绍 

要使用Java SPI,需要遵循如下约定:

1、当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;

2、接口实现类所在的jar包放在主程序的classpath中;

3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;

4、SPI的实现类必须携带一个不带参数的构造方法;

说明

jar包中的META-INF文件有什么作用?

META-INF文件夹存在的话,是用来存储包和扩展的配置数据,包含安全,版本,扩展和服务。

1、MANIFEST.MF

           MANIFEST文件用来定义扩展和包相关的数据。

2、INDEX.LIST

           如果使用了jar工具的 "-i"选项,这个文件就会自动生成.文件包含了路径信息和应用或扩展的包定义.它是部分jar索         引的实现方式,可用来提高类加载器的加载速度。

3、x.SF

           jar包的签名文件,包含清单信息,SF表示signature file, "x" 是文件名。

4、x.DSA

           DSA是一种非对称的数字签名算法.可简单理解为"私钥加密生成数字签名,公钥验证数据及签名", x.DSA                   是"x.SF"文件关联的同名的"签名块文件",里面存着x.SF的数字签名. SF签名文件和DSA签名块文件可用"jarsigner"命令           生成.其实还支持RSA算法,对应的是.RSA的 后缀名。

5、services/

           目录文件,存放 服务提供者 的配置文件。

6、平常还有一种情况,通过maven打包的话,在META-INF 下面默认还会包含maven目录,maven目录下会含有pom相关的配置信息。

示例代码请见:https://download.****.net/download/baidu_28370189/12665506

示例目录

JAVA SPI详解

示例代码 

步骤1、定义一组接口 (假设是spi.com.spi.IShout),并写出接口的一个或多个实现,(假设是spi.com.spi.Dog、spi.com.spi.Cat)

public interface IShout {

	public abstract void shout();
}

public class Dog implements IShout {

	@Override
	public void shout() {
		System.out.println("wang wang");
	}

}

public class Cat implements IShout {

	@Override
	public void shout() {
		System.out.println("miao miao");
	}

}

步骤2、

在 src/main/resources/ 下建立 /META-INF/services 目录, 新增一个以接口命名的文件 (spi.com.spi.IShout文件),内容是要应用的实现类(这里是spi.com.spi.Dog和spi.com.spi.Cat,每行一个类)

JAVA SPI详解

 步骤3、使用 ServiceLoader 来加载配置文件中指定的实现

public class SpiMain {

	public static void main(String[] args) {
		ServiceLoader<IShout> serviceLoader = ServiceLoader.load(IShout.class);
		for (IShout shout : serviceLoader) {
			shout.shout();
		}
	}

}

输出:

wang wang
ji ji
miao miao

SPI原理解析

通过上面的代码可以知道最关键的实现就是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) {
                //..
            }
            //..
        }
    }
}

实现流程如下:

1、应用程序调用ServiceLoader.load方法。

ServiceLoader.load方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量,包括:

  • loader(ClassLoader类型,类加载器)

  • acc(AccessControlContext类型,访问控制器)

  • providers(LinkedHashMap<String,S>类型,用于缓存加载成功的类)

  • lookupIterator(实现迭代器功能)

2、应用程序通过迭代器接口获取对象实例

ServiceLoader先判断成员变量providers对象中(LinkedHashMap<String,S>类型)是否有缓存实例对象,如果有缓存,直接返回。
如果没有缓存,执行类的装载,实现如下:

(1)、读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称,值得注意的是,ServiceLoader可以跨越jar包获取META-INF下的配置文件,具体加载配置的实现代码如下:

    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)、把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型)
然后返回实例对象。

总结 

优点:

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

相比使用提供接口jar包,供第三方服务模块实现接口的方式,SPI的方式使得源框架,不必关心接口的实现类的路径,可以不用通过下面的方式获取接口实现类:

  • 代码硬编码import 导入实现类 。
  • 指定类全路径反射获取:例如在JDBC4.0之前,JDBC中获取数据库驱动类需要通过Class.forName("com.mysql.jdbc.Driver"),类似语句先动态加载数据库相关的驱动,然后再进行获取连接等的操作。

  • 第三方服务模块把接口实现类实例注册到指定地方,源框架从该处访问实例。

通过SPI的方式,第三方服务模块实现接口后,在第三方的项目代码的META-INF/services目录下的配置文件指定实现类的全路径名,源码框架即可找到实现类 。

缺点: 

 

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