谈谈JDK的SPI机制

392 阅读7分钟

前言

  最近在看朱政科的《HikariCP数据库连接池实战》,书中关于JDBC部分的讲解提到了JDK SPI,所以笔者就决定好好研究一下这方面的知识,并通过自己的理解输出一篇文章。如果有描述有误或者理解偏差的地方,欢迎各位大佬指正,谢谢!

JDK SPI是什么

维基百科给的定义:Service Provider Interface (SPI) is an API intended to be implemented or extended by a third party. It can be used to enable framework extension and replaceable components。

​ 翻译一下可知,SPI是**Service Provider Interface(服务提供者接口)**的简称,它是一个可以由第三方实现和扩展的API。

定义可能看起来不太直观,看完下面一个小节关于JDK SPI的使用 ,相信您对JDK SPI会有一个明确的理解。

JDK SPI的简单使用

  1. 首先定义一个Fruit接口

    public interface Fruit {
        String getName();
    }
    
  2. 分别定义两个实现类

    public class Apple implements Fruit {
        @Override
        public String getName() {
            return "apple";
        }
    }
    
    public class Orange implements Fruit {
        @Override
        public String getName() {
            return "orange";
        }
    }
    
  3. classpath下面创建META-INF/services文件夹,并在文件夹中创建一个文件cn.ajin.practical.java.spi.Fruit,并在文件中声明具体的实现类

    cn.ajin.practical.java.spi.Orange
    
  4. 编写测试代码查看输出结果

       ServiceLoader<Fruit> serviceLoader = ServiceLoader.load(Fruit.class);
       Iterator<Fruit> iterator = serviceLoader.iterator();
          while (iterator.hasNext()) {
              Fruit fruit = iterator.next();
              System.out.println(fruit.getName());
          }
    

    控制台输出结果:orange

    我们看到控制台输出的是我们定义在cn.ajin.practical.java.spi.Fruit文件中的Orange

JDK SPI原理探究

​ 通过JDK SPI机制,我们可以创建出一个可扩展的程序,只要接口定义好,再定义好接口的实现类,我们的应用程序需要哪些 实现类的时候,通过上面的步骤3定义好就可以了,而原有程序逻辑无需修改。

​ 类比一下Spring Web应用,通常定义好一个Service接口,然后编写Service实现类的逻辑,然后在实现类上标上注解@Componet就可以了,Controller类中只要注入Service接口即可 获取Service实现类了。

其实这是一种面向接口的编程,我们的应用程序无需关注实现类,但是最终却可以获取到我们需要的实现类。

​ 再说我们比较熟悉的Spring Boot,自动装配流程需要获取自动装配类,这个过程是通过一个工具类去查找所有jar下面的spring.factories文件,获取文件中org.springframework.boot.autoconfigure.EnableAutoConfiguration对应的自动装配类(如下所示)来实现的。这和JDK SPI机制的编程模式是不是有一点点相似呢?

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\

JDK SPI机制是一种服务发现机制,动态地为接口寻找服务实现。它的核心来自于ServiceLoader这个类。

ServiceLoader

这里再列出测试代码:

        ServiceLoader<Fruit> serviceLoader = ServiceLoader.load(Fruit.class);
        Iterator<Fruit> iterator = serviceLoader.iterator();
        while (iterator.hasNext()) {
            Fruit fruit = iterator.next();
            System.out.println(fruit.getName());
        }

ServiceLoader基本原理

在阅读下面的源码分析部分,读者可以参考上图。

ServiceLoader#load(java.lang.Class<S>)

 public static <S> ServiceLoader<S> load(Class<S> service) {
        // 获取当前线程的ClassLoader 
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
  }

ServiceLoader.load(service, cl);最终会调用下面的构造器

 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();
  }

reload()方法需要关注一下

  public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
  }

这里创建了LazyIterator,后面会使用它来遍历Fruit.class

总结一下

  • ServiceLoader#load(java.lang.Class<S>)
    • ServiceLoader.load(service, cl)
      • ServiceLoader#reload
        • new LazyIterator

可见,ServiceLoader#load(Class<S>)实际上是创建了一个LazyIterator迭代器对象。

serviceLoader.iterator()

 public Iterator<S> iterator() {
        return new Iterator<S>() {

            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();

            public boolean hasNext() {
                if (knownProviders.hasNext())
                    return true;
                // 核心
                return lookupIterator.hasNext();
            }

            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                // 核心
                return lookupIterator.next();
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }

        };
    }

这里创建了一个Iterator,当我们调用它的hasNext方法时,debug走下来看,其实就是调用LazyIterator#hasNext方法,然后会调用hasNextService方法。

public boolean hasNext() {
        if (acc == null) {
             return hasNextService();
         } else {
             PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                 public Boolean run() { return hasNextService(); }
             };
             return AccessController.doPrivileged(action, acc);
         }
}

不忙往下看,这里先总结一下,通过serviceLoader.iterator()方法创建的Iterator对象,它的hasNext方法和next方法实际上是调用了LazyIterator中的对应方法,所以真正的主角就是LazyIterator对象。

LazyIterator#hasNextService
private boolean hasNextService() {
        if (nextName != null) {
            return true;
         }
    	  // 如果不是第一次执行,configs !=null ,不仅会进入第一个判断
         if (configs == null) {
             try {
                  // 文件名:META-INF/services/cn.ajin.practical.java.spi.Fruit
                  // service.getName() : 类名,这里就是 cn.ajin.practical.java.spi.Fruit
                  String fullName = PREFIX + service.getName();
                  if (loader == null)
                      configs = ClassLoader.getSystemResources(fullName);
                   else
                      // 根据fullName获取Enumeration对象
                      configs = loader.getResources(fullName);
              } catch (IOException x) {
                   ...
             }
         }
    	 
         while ((pending == null) || !pending.hasNext()) {
             // 判断元素是否存在
              if (!configs.hasMoreElements()) {
                return false;
             }
             pending = parse(service, configs.nextElement());
         }
    	 // cn.ajin.practical.java.spi.Orange
         nextName = pending.next();
         return true;
}
  1. 获取fullName(其实就是我们在META-INF/services目录下定义的文件名)

  2. 返回CompoundEnumeration

  3. 判断CompoundEnumeration中是否有元素

    1. 如果不存在元素,直接返回false

    2. 反之,则去解析出下一个元素(如果是第一次执行,则会去解析第一个元素)

      pending = parse(service, configs.nextElement());

    3. pending中的第一个元素赋值给nextName引用变量

下面是LazyIterator中比较重要的实例变量:

  // 根据fullName查找到的 Enumeration集合
 Enumeration<URL> configs = null;
 // 通过config解析出的迭代器
 Iterator<String> pending = null;
 // 下一个元素
 String nextName = null;
总结LazyIterator迭代原理

原理总结

到这里,我想大家对于ServiceLoader加载定义好的实现类的原理应该有比较清晰的了解吧,其实就是先获取文件名,并根据文件名获取一个Enumeration对象,再去迭代,最终创建实现类对象。

JDBC与SPI

接下来,我们再来看看JDBC中的SPI机制。

原生JDBC的使用

        Class.forName("com.mysql.jdbc.Driver");
        Connection connection = null;
        Statement statement = null;
        ResultSet rs = null;
        try {

            connection = DriverManager.getConnection(url, userName, password);

            statement = connection.createStatement();

            rs = statement.executeQuery("select id,name,age from s_user");

            while (rs.next()) {
                int id = rs.getInt("id");
                String name = rs.getString("name");
                int age = rs.getInt("age");
            }
        } finally {
            if (rs != null) {
                rs.close();
            }

            if (statement != null) {
                statement.close();
            }

            if (connection != null) {
                connection.close();
            }
        }

原生JDBC的写法需要Class.forName方法来指定驱动类,但是这样显然是很不灵活的,今天是mysql,明天是oracle,这样的写法无法做到无缝切换数据库驱动。通过JDBC的SPI机制,可以很轻松的对接不同数据库厂商的JDBC驱动。

JDBC SPI机制

DriverManager#getConnection(url, userName, password)方法的首次调用会触发静态代码块的执行。

 static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
  }

DriverManager#loadInitialDrivers

 private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
     
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
				// 使用SPI机制加载Driver驱动类
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                try{
                    while(driversIterator.hasNext()) {
                        // 反射加载驱动类
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                  // Do nothing
                }
                return null;
            }
        });
		
        ...
    }

driversIterator.next()方法会触发驱动类的static代码块的执行

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            // 将Driver注册到DriverManager中去
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

JDK SPI机制的优缺点

  • 优点:JDK SPI使得我们可以面向接口编程,无需硬编码的方式即可引入实现类
  • 缺点:
    • 不能按需加载,虽然 ServiceLoader 做了延迟载入,但是基本只能通过遍历全部获取,也就是接口的实现类得全部载入并实例化一遍。就是接口的实现类得全部载入并实例化一遍。如果你并不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
    • 多线程并发场景下加载是线程不安全的

鉴于SPI的这些缺点,Dubbo SPI做了一些优化,读者如果感兴趣,可以了解一下.

全文总结

本文首先介绍了JDK SPI的基本概念,并通过一个Demo让读者了解SPI的使用,然后探索了SPI机制的底层原理,接着引申到JDBC中的SPI使用,最后还稍微探索了SPI的优缺点,算是一个比较完整的讲述吧。

使用SPI机制,我们可以很灵活地引入接口的实现类,同时SPI也存在一定的缺陷。笔者对Dubbo的底层原理可以说不太清楚,所以就没有去介绍Dubbo中的SPI,后面如果学习到Dubbo这块的内容,应该会补上一篇文章来讲述,读者也可以自己去探索学习。谢谢大家的阅读,周末愉快!

参考资料

  1. docs.oracle.com/javase/tuto…

  2. docs.oracle.com/javase/tuto…

  3. zhuanlan.zhihu.com/p/28909673

  4. www.cnblogs.com/xrq730/p/11…

  5. en.wikipedia.org/wiki/Servic…

  6. zhoukaibo.com/2019/03/16/…

  7. HikariCP数据库连接池实战