javaSPI扩展

介绍

  SPI就是一种服务发现机制。
  SPI 是一种将服务接口与服务实现分离以达到解耦、大大提升了程序可扩展性的机制。引入服务提供者就是引入了 spi 接口的实现者,通过本地的注册发现获取到具体的实现类,轻松可插拔。
  Java SPI 就是把某个接口的 指定实现类 (通过在指定文件配置的方式)给实例化出来了。


使用

  需要准备一个maven工程,以此为蓝本,进行开发。

创建接口及实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 接口
public interface HelloService {

void sayHello();

}

// 实现类
public class HelloServiceImpl1 implements HelloService {
@Override
public void sayHello() {
System.out.println("hello1");
}
}
public class HelloServiceImpl2 implements HelloService {
@Override
public void sayHello() {
System.out.println("hello2");
}
}

创建META-INF/service文件夹,建立文件

  这里建立的文件名与接口的全限定名相同,文件的内容为接口实现类的全限定名。

1
2
com.lin.javaspi.service.impl.HelloServiceImpl1
com.lin.javaspi.service.impl.HelloServiceImpl2

启动方法

1
2
3
4
5
6
7
8
9
10
11
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
ServiceLoader<HelloService> load = ServiceLoader.load(HelloService.class);
Iterator<HelloService> iterator = load.iterator();
while (iterator.hasNext()){
HelloService next = iterator.next();
next.sayHello();
}
// Class<?> aClass = Class.forName("com.lin.javaspi.service.impl.HelloServiceImpl1");
// HelloServiceImpl1 o = (HelloServiceImpl1) aClass.newInstance();
// o.sayHello();
}

流程

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

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

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

    ServiceLoader先判断成员变量providers对象中LinkedHashMap<String,S>类型是否有缓存实例对象,如果有缓存,直接返回。

    1. 读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称,值得注意的是,ServiceLoader可以跨越jar包获取META-INF下的配置文件。
    2. 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。
    3. 把实例化后的类缓存到providers对象中,LinkedHashMap<String,S>类型然后返回实例对象。

要求

  1. 必须要有接口
  2. 对应要有实现类,且具备空参构造方法。
  3. 文件夹META-INF/services放置classpath目录下。
  4. 以“接口全限定名”命名的文件。
  5. 文件内容为接口实现类的全路径。

场景

  调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略

  1. jdbc

    不同的数据库使用不同的jdbc驱动,他们实现了相同的接口,java.sql.Driver

  2. 日志门面接口实现类加载

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

  3. spring

  4. dubbo


总结

优点

  使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。
  相比使用提供接口jar包,供第三方服务模块实现接口的方式,SPI的方式使得源框架,不必关心接口的实现类的路径,可以不用通过下面的方式获取接口实现类:

  1. 代码硬编码import 导入实现类。
  2. 指定类全路径反射获取,Class.forName()
  3. 第三方服务模块把接口实现类实例注册到指定地方,源框架从该处访问实例。

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

缺点

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