详细介绍Java的SPI机制
一、什么是SPI机制
SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,
其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现
SPI整体机制图如下:

当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件, 这个文件里的内容就是这个接口的具体的实现类。
当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,
就可以使用该服务了。JDK中查找服务的实现的工具类是:java.util.ServiceLoader。
二、SPI工作原理
1、定义服务接口:由框架或核心库定义
2、提供实现类:由第三方提供具体实现
3、配置文件声明:在META-INF/services目录下创建以接口全限定名命名的文件,内容为实现类的全限定名
4、服务加载:通过ServiceLoader类动态加载实现类
三、代码示例
1、定义服务接口
     public interface Search {
          public List<String> searchDoc(String keyword);   
     }
2、提供实现类
    public class FileSearch implements Search{
        @Override
        public List<String> searchDoc(String keyword) {
            System.out.println("文件搜索 "+keyword);
            return null;
        }
    }
    public class DatabaseSearch implements Search{
        @Override
        public List<String> searchDoc(String keyword) {
            System.out.println("数据搜索 "+keyword);
            return null;
        }
    }
3、创建配置文件
在资源目录下创建文件:
META-INF/services/com.cainiao.ys.spi.learn.Search
在文件内容中添加我们需要用到的实现类:
com.cainiao.ys.spi.learn.FileSearch
4、测试方法
public class TestCase {
    public static void main(String[] args) {
        ServiceLoader<Search> s = ServiceLoader.load(Search.class);
        Iterator<Search> iterator = s.iterator();
        while (iterator.hasNext()) {
           Search search =  iterator.next();
           search.searchDoc("hello world");
        }
    }
}
四、SPI核心类 - ServiceLoader
ServiceLoader 是SPI机制的核心类,主要方法:
 - load(Class<S> service):创建新的服务加载器
 - iterator():返回迭代器,用于遍历服务实现
 - stream():返回服务实现的流
 - reload():重新加载服务
五、SPI的应用场景
1、JDBC驱动加载:DriverManager使用SPI加载不同数据库驱动
2、日志框架:如SLF4J的桥接器实现
3、XML解析:JAXP中的SAXParserFactory和DocumentBuilderFactory
4、Spring框架:Spring Boot自动配置
5、Dubbo框架:扩展点加载机制
六、SPI机制 - JDBC DriverManager
在JDBC4.0之前,我们开发有连接数据库的时候,通常会用Class.forName(“com.mysql.jdbc.Driver”)这句先加载数据库相关的驱动,
然后再进行获取连接等的操作。而JDBC4.0之后不需要用Class.forName(“com.mysql.jdbc.Driver”)来加载驱动,直接获取连接就可以了,
现在这种方式就是使用了Java的SPI扩展机制来实现
1、JDBC接口定义
首先在java中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商来提供的
2、mysql实现
在mysql的jar包mysql-connector-java-6.0.6.jar中,可以找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver的文件,
文件内容是com.mysql.cj.jdbc.Driver,这里面的内容就是针对Java中定义的接口的实现
3、postgresql实现
同样在postgresql的jar包postgresql-42.0.0.jar中,也可以找到同样的配置文件,文件内容是org.postgresql.Driver,这是postgresql对Java的java.sql.Driver的实现
4、使用方法
上面说了,现在使用SPI扩展来加载具体的驱动,我们在Java中写连接数据库的代码的时候,不需要再使用Class.forName(“com.mysql.jdbc.Driver”)来加载驱动了,
而是直接使用如下代码:
  String url = "jdbc:xxxx://xxxx:xxxx/xxxx";
  Connection conn = DriverManager.getConnection(url,username,password);
  .....
这里并没有涉及到SPI机制的使用,接着看下面的解析
5、源码实现
上面的使用方法,就是我们普通的连接数据库的代码,并没有涉及到SPI机制的东西,但是有一点我们可以确定的是,我们没有写有关具体驱动的硬编码Class.forName(“com.mysql.jdbc.Driver”)
上面的代码可以直接获取数据库连接进行操作,但是跟SPI有啥关系呢?上面代码没有了加载驱动的代码,我们怎么去确定使用哪个数据库连接的驱动呢 ?
这里就涉及到使用Java的SPI扩展机制来查找相关驱动的东西了,关于驱动的查找其实都在DriverManager中,DriverManager是Java中的实现,用来获取数据库连接,
在DriverManager中有一个静态代码块如下:
static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}
可以看到是加载实例化驱动的,接着看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的ServiceLoader来加载接口的实现
              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;
          }
      });
      println("DriverManager.initialize: jdbc.drivers = " + drivers);
      if (drivers == null || drivers.equals("")) {
          return;
      }
      String[] driversList = drivers.split(":");
      println("number of Drivers:" + driversList.length);
      for (String aDriver : driversList) {
          try {
              println("DriverManager.Initialize: loading " + aDriver);
              Class.forName(aDriver, true,
                      ClassLoader.getSystemClassLoader());
          } catch (Exception ex) {
              println("DriverManager.Initialize: load failed: " + ex);
          }
      }
  }
上面的代码主要步骤是:
1、从系统变量中获取有关驱动的定义
2、使用SPI来获取驱动的实现
3、遍历使用SPI获取到的具体实现,实例化各个实现类
4、根据第一步获取到的驱动列表来实例化具体实现类
我们主要关注2,3步,这两步是SPI的用法,首先看第二步,使用SPI来获取驱动的实现,对应的代码是:
  ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
这里没有去META-INF/services目录下查找配置文件,也没有加载具体实现类,做的事情就是封装了我们的接口类型和类加载器,并初始化了一个迭代器
接着看第三步,遍历使用SPI获取到的具体实现,实例化各个实现类,对应的代码如下:
  //获取迭代器
  Iterator<Driver> driversIterator = loadedDrivers.iterator();
  //遍历所有的驱动实现
  while(driversIterator.hasNext()) {
      driversIterator.next();
  }
在遍历的时候,首先调用driversIterator.hasNext()方法,这里会搜索classpath下以及jar包中所有的META-INF/services目录下的java.sql.Driver文件,
并找到文件中的实现类的名字,此时并没有实例化具体的实现类(ServiceLoader具体的源码实现在下面)。然后是调用driversIterator.next()方法,
此时就会根据驱动名字具体实例化各个实现类了。现在驱动就被找到并实例化了
可以看下截图,我在测试项目中添加了两个jar包,mysql-connector-java-6.0.6.jar和postgresql-42.0.0.0.jar,跟踪到DriverManager中之后:


可以看到此时迭代器中有两个驱动,mysql和postgresql的都被加载了
 
                    
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号