SPI(Service Provider Interface)详解

亦凉 2021-09-07 06:02 521阅读 0赞

介绍

SPI 全称为 (Service Provider Interface) ,是JVM内置的一种服务提供发现机制。Java在语言层面为我们提供了一种方便地创建可扩展应用的途径。我们只需要按照SPI的要求,在jar包中进行适当的配置,jvm就会在运行时通过懒加载,帮我们找到所需的服务并加载。如果我们一直不使用某个服务,那么它不会被加载,一定程度上避免了资源的浪费。

1、应用

熟悉JDBC的同学都知道,在jdbc4.0之前,在使用DriverManager获取DB连接之前,我们总是需要通过Class.forName显示的加载驱动(为了执行驱动类的static代码,注册驱动实例对象到DriverManager中),例如:

  1. Connection comm = null;
  2. Statement stmt = null;
  3. try {
  4. //注册mysql的jdbc驱动
  5. Class.forName("com.mysql.jdbc.Driver");
  6. //创建连接
  7. conn = DriverManagger.getConnection(url,user,pwd);
  8. }

在JDBC4.0开始,这个显式的初始化不再是必选项了,它存在的意义只是为了向上兼容。那么JDBC4.0之后,我们的应用是如何找到对应的驱动呢?答案就是SPI。

2、使用

  • 一个服务(Service)通常指的是已知的接口或者抽象类(SPI并没有强制要求服务必须是interface或abstract class,完全可以将class注册为SPI注册服务)
  • 服务提供方就是对这个接口或者抽象类的实现,按照SPI 标准,在资源路径META-INF/services目录下创建一个文件(文件的命名为该服务接口的全限定名);内容为实现类的全限定名,每行一个,可以使用#作为注释
  • 将服务的提供代码打包成jar,放到classpath下;

完成以上后,在程序运行时可使用ServiceLoader.load(Class class) api获取到接口服务的所有实现类。SPI底层原理:使用懒加载的方式,在运行时将该服务接口的实现类通过Class.forName的方式加载到JVM中,并做实例初始化。

示例

1)接口服务:

  1. package spi;
  2. public interface Animal {
  3. void eat();
  4. void sleep();
  5. }

2)服务实现:

  1. package spi;
  2. public class Dog implements Animal{
  3. @Override
  4. public void eat() {
  5. System.out.println("dog eat...");
  6. }
  7. @Override
  8. public void sleep() {
  9. System.out.println("dog sleep...");
  10. }
  11. }

3)SPI配置文件:

在resource下创建META-INf/services目录,然后创建文件spi.Animal,内容:spi.Dog

4)测试代码:

  1. package spi;
  2. import java.util.Iterator;
  3. import java.util.ServiceLoader;
  4. public class SPITest {
  5. public static void main(String[] args) {
  6. ServiceLoader<Animal> animals = ServiceLoader.load(Animal.class);
  7. for (Animal animal : animals) {//增强型for循环等价于下面的iterator迭代
  8. animal.eat();
  9. animal.sleep();
  10. }
  11. Iterator<Animal> iterator = animals.iterator();
  12. while(iterator.hasNext()) {
  13. Animal animal = iterator.next();
  14. animal.eat();
  15. animal.sleep();
  16. }
  17. }
  18. }

说明:本示例是在一个工程下演示的,可以将Animal接口打包成animal.jar,Dog实现工程中实现引入animal.jar,并且配置META-INF文件,在打包成dog.jar。最后创建一个test工程,引入dog.jar即可进行测试。

源码解读

1、创建ServiceLoader实例:

  1. public final class ServiceLoader<S> implements Iterable<S> {
  2. //...
  3. //重新load指定serivice的实现。通过LazyIterator实现懒加载。
  4. public void reload() {
  5. providers.clear();//是个LinkedHashMap 类型
  6. lookupIterator = new LazyIterator(service, loader);
  7. }
  8. //私有ServiceLoader构造函数,须通过ServiceLoader.load(Class<?>)静态方法创建ServiceLoader实例
  9. private ServiceLoader(Class<S> svc, ClassLoader cl) {
  10. service = Objects.requireNonNull(svc, "Service interface cannot be null");
  11. loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
  12. acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
  13. reload();
  14. }
  15. //构建ServiceLoader实例
  16. public static <S> ServiceLoader<S> load(Class<S> service,
  17. ClassLoader loader)
  18. {
  19. return new ServiceLoader<>(service, loader);
  20. }
  21. //通过service的class创建ServiceLoader实例,默认使用上下文classloader
  22. public static <S> ServiceLoader<S> load(Class<S> service) {
  23. ClassLoader cl = Thread.currentThread().getContextClassLoader();
  24. return ServiceLoader.load(service, cl);
  25. }
  26. }

说明:

  • 通过load静态方法创建ServiceLoader实例,构造完ServiceLoader后,并不会立刻扫描当前进程中的服务实现,而是创建一个LazyIterator懒加载迭代器,在实际使用时采取扫描服务实现类并加载;
  • 默认使用当前线程的上下文classLoader,后面会用该classLoader加载服务实现类;

2、ServiceLoader的遍历:

ServiceLoader实现Iterable<?>接口,调用load方法(创建ServiceLoader实例)返回了一个服务类型的迭代器,接下来使用iterator(或者增强型for循环)遍历ServiceLoader实例时,才会真正的扫描、加载对应的服务实现类。

  1. public final class ServiceLoader<S> implements Iterable<S> {
  2. //...
  3. //缓存的service provider,按照初始化顺序排列。
  4. private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
  5. //当前的LazyIterator迭代器指针,服务懒加载迭代器
  6. private LazyIterator lookupIterator;
  7. //增强型for循环或者直接iterator()方法遍历ServiceLoader
  8. public Iterator<S> iterator() {
  9. return new Iterator<S>() {
  10. //创建Iterator迭代器时的ServiceLoader.providers快照,
  11. //因此在首次迭代时,iterator总是会通过LazyIterator进行懒加载
  12. Iterator<Map.Entry<String,S>> knownProviders
  13. = providers.entrySet().iterator();
  14. public boolean hasNext() {
  15. // 如果已经扫描过,则对providers进行迭代;
  16. if (knownProviders.hasNext())
  17. return true;
  18. // 如果没有扫描过,则通过lookupIterator进行扫描和懒加载
  19. return lookupIterator.hasNext();
  20. }
  21. public S next() {
  22. // 如果已经扫描过,则对providers进行迭代;
  23. if (knownProviders.hasNext())
  24. return knownProviders.next().getValue();
  25. // 如果没有扫描过,则通过lookupIterator进行扫描和懒加载
  26. return lookupIterator.next();
  27. }
  28. public void remove() {
  29. throw new UnsupportedOperationException();
  30. }
  31. };
  32. }
  33. }

说明:

  • 首次迭代时,因为ServiceLoader.providers中没有任何缓存,总是会通过LazyIterator进行懒加载,并将service实现的全限定名与加载的service实例作为key-value缓存到ServiceLoader.providers中。
  • 之后再进行迭代时,总是在ServiceLoader.providers中进行。

3、懒加载迭代器LazyIterator:

  1. public final class ServiceLoader<S> implements Iterable<S> {
  2. private static final String PREFIX = "META-INF/services/";
  3. //...
  4. private class LazyIterator implements Iterator<S> {//内部类
  5. Class<S> service;
  6. ClassLoader loader;
  7. Enumeration<URL> configs = null;
  8. Iterator<String> pending = null;//配置文件中的服务实现类的迭代器(每行一个)
  9. String nextName = null;
  10. private LazyIterator(Class<S> service, ClassLoader loader) {
  11. this.service = service; this.loader = loader;
  12. }
  13. private boolean hasNextService() {
  14. if (nextName != null) {
  15. return true;
  16. }
  17. if (configs == null) {//首次迭代时
  18. try {
  19. String fullName = PREFIX + service.getName();
  20. if (loader == null)//通过ClassLoader.getResources()获得资源URL集合
  21. configs = ClassLoader.getSystemResources(fullName);
  22. else
  23. configs = loader.getResources(fullName);
  24. } catch (IOException x) {
  25. fail(service, "Error locating configuration files", x);
  26. }
  27. }
  28. while ((pending == null) || !pending.hasNext()) {
  29. if (!configs.hasMoreElements()) {
  30. return false;
  31. }
  32. pending = parse(service, configs.nextElement());
  33. }
  34. nextName = pending.next();
  35. return true;
  36. }
  37. private S nextService() {
  38. if (!hasNextService()) throw new NoSuchElementException();
  39. String cn = nextName;
  40. try {
  41. Class<?> c = Class.forName(cn, false, loader);
  42. S p = service.cast(c.newInstance());
  43. providers.put(cn, p);
  44. return p;
  45. } catch (ClassNotFoundException x) {
  46. fail(service,"Provider " + cn + " not found");
  47. }
  48. throw new Error(); // This cannot happen
  49. }
  50. public boolean hasNext() {
  51. if (acc == null) {
  52. return hasNextService();
  53. } else {
  54. PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
  55. public Boolean run() { return hasNextService(); }
  56. };
  57. return AccessController.doPrivileged(action, acc);
  58. }
  59. }
  60. public S next() {
  61. if (acc == null) {
  62. return nextService();
  63. } else {
  64. PrivilegedAction<S> action = new PrivilegedAction<S>() {
  65. public S run() { return nextService(); }
  66. };
  67. return AccessController.doPrivileged(action, acc);
  68. }
  69. }
  70. public void remove() {
  71. throw new UnsupportedOperationException();
  72. }
  73. }
  74. }

说明:

  • hasNextService方法实现了资源的查找,通过classLoader的getResources方法实现;
  • nextService方法实现了服务实现类的实例化,通过Class.forName实现;
  • LazyIterator中除了hasNextService、nextService方法外还有Iterator的hasNext、next方法,其内部调用了hasNextService和nextService。

4、其他:

ServiceLoader类并不复杂,实现了SPI的所有逻辑,内部出了上面说的一些方法外,还有parse及parseLine的代码,可以发现,parseLine中会对服务实现类进行去重,所以在一个或多个services配置文件中配置多次的服务实现类只会被处理一次。

应用

1、jdbc:

在https://blog.csdn.net/liuxiao723846/article/details/112397128 已经介绍过一次jdbc驱动的加载过程,这里再简单重复一下。

DriverManager的static代码块中会执行loadInitialDrivers方法,该方法内部首先通过jdbc.drivers配置加载驱动,然后会通过SPI来加载驱动,如下:

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpdXhpYW83MjM4NDY_size_16_color_FFFFFF_t_70

在mysql驱动库中实现了SPI标准,并且在static代码块中创建驱动实例,注册到jdbc的DriverManager中。接下来,就可以通过DriverManager来创建connection了。

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpdXhpYW83MjM4NDY_size_16_color_FFFFFF_t_70 1

2、slf4j

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpdXhpYW83MjM4NDY_size_16_color_FFFFFF_t_70 2

参考:

https://www.jianshu.com/p/27c837293aeb

https://linxiaobai.github.io/2018/05/18/ClassLoader%E5%8E%9F%E7%90%86%E4%BB%A5%E5%8F%8ASPI/

发表评论

表情:
评论列表 (有 0 条评论,521人围观)

还没有评论,来说两句吧...

相关阅读