JAVA SPI 是怎么实现的?

2020-06-04 16:58:22 浏览数 (1)

JAVA SPI 是怎么实现的?

SPI 是什么?

SPI(Service Provider Interface) ,是 JDK 内置的一种提供发现机制。SPI 是一种动态替换发现的机制。

SPI类图

JAVA SPI 实现

  1. 定义一组接口,接口有多种实现
代码语言:javascript复制
public interface IShout {
    void shout();
}
public class Cat implements IShout {
    @Override
    public void shout() {
        System.out.println("miao miao");
    }
}
public class Dog implements IShout {
    @Override
    public void shout() {
        System.out.println("wang wang");
    }
}
  1. 在 src/main/resources/ 下建立 /META-INF/services 目录, 新增一个以接口命名的文件 (org.foo.demo.IShout文件),内容是要应用的实现类(这里是org.foo.demo.animal.Dog和org.foo.demo.animal.Cat,每行一个类)。

文件位置

代码语言:javascript复制
- src
    -main
        -resources
            - META-INF
                - services
                    - org.foo.demo.IShout

文件内容

代码语言:javascript复制
org.foo.demo.animal.Dog
org.foo.demo.animal.Cat
  1. ServiceLoaader 加载配置文件中指定的实现
代码语言:javascript复制
public class SPIMain {
    public static void main(String[] args) {
        ServiceLoader<IShout> shouts = ServiceLoader.load(IShout.class);
        for (IShout s : shouts) {
            s.shout();
        }
    }
}

输出:

代码语言:javascript复制
wang wang
miao miao

Java SPI 实现原理

  1. 应用程序调用 ServiceLoader.load 方法创建一个 ServiceLoader,并实例化类中的成员变量。
  • load(Classloader 类型,类加载器)
  • acc(AccessControlContext 类型,访问控制器)
  • providers(LinkedHashMap<String,S>)类型,用于缓存加载成功的类
  • lookupIterators (实现迭代器功能)
代码语言:javascript复制
 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();
    }
 public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }
  1. 应用程序通过迭代器接口 读取META-INF/services/下的配置文件,然后实例化:

1).读取配置文件

代码语言:javascript复制
 if (configs == null) {
                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对象中

JAVA SPI 应用场景

数据库DriverManager、Spring、ConfigurableBeanFactory等都用到了SPI机制。

在JDBC4.0之前,连接数据库的时候,通常会用Class.forName("com.mysql.jdbc.Driver")这句先加载数据库相关的驱动,然后再进行获取连接等的操作。而JDBC4.0之后不需要Class.forName来加载驱动,直接获取连接即可,这里使用了Java的SPI扩展机制来实现。

  1. 在mysql-connector-java-5.1.45.jar中,META-INF/services目录下会有一个名字为java.sql.Driver的文件:
代码语言:javascript复制
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
  1. Mysql DriverManager实现

DriverManager中有一个静态代码块如下:

代码语言:javascript复制
static {
	loadInitialDrivers();
	println("JDBC DriverManager initialized");
}

loadInitialDrivers方法,loadInitialDrivers用法用到了上文提到的spi工具类ServiceLoader:

代码语言:javascript复制
 public void run() {

        ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
        Iterator<Driver> driversIterator = loadedDrivers.iterator();

...
}

总结

Java的SPI机制就是为某个接口寻找到相关的服务实现

程序员开发者社区

0 人点赞