JAVA SPI详解

1 篇文章 0 订阅

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.csdn.net/download/baidu_28370189/12665506

示例目录

示例目录

示例代码 

步骤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,每行一个类)

META-INF目录

 步骤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类的实例是不安全的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值