立即打开
Java是如何实现自己的SPI机制的?

Java是如何实现自己的SPI机制的?

1 引言

本篇我们来探究Java的SPI机制的相关源码。

2 什么是SPI机制

那么,什么是SPI机制呢?

SPI是Service Provider Interface 的简称,即服务提供者接口的意思。根据字面意思我们可能还有点困惑,SPI说白了就是一种扩展机制,我们在相应配置文件中定义好某个接口的实现类,然后再根据这个接口去这个配置文件中加载这个实例类并实例化,其实SPI就是这么一个东西。说到SPI机制,我们最常见的就是Java的SPI机制,此外,还有Dubbo和SpringBoot自定义的SPI机制。

有了SPI机制,那么就为一些框架的灵活扩展提供了可能,而不必将框架的一些实现类写死在代码里面。

那么,某些框架是如何利用SPI机制来做到灵活扩展的呢?下面举几个栗子来阐述下:

上面的三个栗子先让我们直观感受下某些框架利用SPI机制是如何做到灵活扩展的。

3 如何使用Java的SPI?

我们先来看看如何使用Java自带的SPI。先定义一个Developer接口

 

再定义两个Developer接口的两个实现类:

 
 

然后再在项目resources目录下新建一个META-INF/services文件夹,然后再新建一个以Developer接口的全限定名命名的文件,文件内容为:

 

最后我们再新建一个测试类JdkSPITest:

 

运行上面那个测试类,运行成功结果如下截图所示:

Java是如何实现自己的SPI机制的?

由上面简单的Demo我们知道了如何使用Java的SPI机制来实现扩展点加载。

4 Java的SPI机制的源码分析

通过前面扩展Developer接口的简单Demo,我们看到Java的SPI机制实现跟ServiceLoader这个类有关,那么我们先来看下ServiceLoader的类结构代码:

 

可以看到,ServiceLoader实现了Iterable接口,覆写其iterator方法能产生一个迭代器;同时ServiceLoader有一个内部类LazyIterator,而LazyIterator又实现了Iterator接口,说明LazyIterator是一个迭代器。

4.1 ServiceLoader.load方法,为加载服务提供者实现类做前期准备

那么我们现在开始探究Java的SPI机制的源码, 先来看JdkSPITest的第一句代码ServiceLoader<Developer> serviceLoader = ServiceLoader.load(Developer.class);中的ServiceLoader.load(Developer.class)的源码:

 

我们再来看下ServiceLoader.load(service, cl);方法:

 

继续看new ServiceLoader<>(service, loader);是如何构建的?

 

可以看到在构建ServiceLoader对象时除了给其成员属性赋值外,还调用了reload方法:

 

可以看到在reload方法中又新建了一个LazyIterator对象,然后赋值给lookupIterator。

 

可以看到在构建LazyIterator对象时,也只是给其成员变量service和loader属性赋值呀,我们一路源码跟下来,也没有看到去META-INF/services文件夹加载Developer接口的实现类!这就奇怪了,我们都被ServiceLoader的load方法名骗了。

还记得分析前面的代码时新建了一个LazyIterator对象吗?Lazy顾名思义是懒的意思,Iterator就是迭代的意思。我们此时猜测那么LazyIterator对象的作用应该就是在迭代的时候再去加载Developer接口的实现类了。

4.2 ServiceLoader.iterator方法,实现服务提供者实现类的懒加载

我们现在再来看JdkSPITest的第二句代码serviceLoader.forEach(Developer::sayHi);,执行这句代码后最终会调用serviceLoader的iterator方法:

 

可以看到调用serviceLoader的iterator方法会返回一个匿名的迭代器对象,而这个匿名迭代器对象其实相当于一个门面类,其覆写的hasNext和next方法又分别委托LazyIterator的hasNext和next方法来实现了。

我们继续调试,发现接下来会进入LazyIterator的hasNext方法:

 

继续跟进hasNextService方法:

 

可以看到在执行LazyIterator的hasNextService方法时最终将去META-INF/services/目录下加载接口文件的内容即加载服务提供者实现类的全限定名,然后取出一个服务提供者实现类的全限定名赋值给LazyIterator的成员变量nextName。到了这里,我们就明白了LazyIterator的作用真的是懒加载,在用到的时候才会真正去加载服务提供者实现类。

思考:为何这里要用懒加载呢?懒加载的思想是怎样的呢?懒加载有啥好处呢?你还能举出其他懒加载的案例吗?

同样,执行完LazyIterator的hasNext方法后,会继续执行LazyIterator的next方法:

 

我们继续跟进nextService方法:

 

可以看到LazyIterator的nextService方法最终将实例化之前加载的服务提供者实现类,并放进providers集合中,随后再调用服务提供者实现类的方法(比如这里指JavaDeveloper的sayHi方法)。注意,这里是加载一个服务提供者实现类后,若main函数中有调用该服务提供者实现类的方法的话,紧接着会调用其方法;然后继续实例化下一个服务提供者类。

因此,我们看到了ServiceLoader.iterator方法真正承担了加载并实例化META-INF/services/目录下的接口文件里定义的服务提供者实现类。

设计模式:可以看到,Java的SPI机制实现代码中应用了迭代器模式,迭代器模式屏蔽了各种存储对象的内部结构差异,提供一个统一的视图来遍历各个存储对象(存储对象可以为集合,数组等)。java.util.Iterator也是迭代器模式的实现:同时Java的各个集合类一般实现了Iterable接口,实现了其iterator方法从而获得Iterator接口的实现类对象(一般为集合内部类),然后再利用Iterator对象的实现类的hasNext和next方法来遍历集合元素。

5 JDBC驱动加载源码解读

前面分析了Java的SPI机制的源码实现,现在我们再来看下Java的SPI机制的实际案例的应用。

我们都知道,JDBC驱动加载是Java的SPI机制的典型应用案例。JDBC主要提供了一套接口规范,而这套规范的api在java的核心库(rt.jar)中实现,而不同的数据库厂商只要编写符合这套JDBC接口规范的驱动代码,那么就可以用Java语言来连接数据库了。

java的核心库(rt.jar)中跟JDBC驱动加载的最核心的接口和类分别是java.sql.Driver接口和java.sql.DriverManager类,其中java.sql.Driver是各个数据库厂商的驱动类要实现的接口,而DriverManager是用来管理数据库的驱动类的,值得注意的是DriverManager这个类有一个registeredDrivers集合属性,用来存储Mysql的驱动类。

 

这里以加载Mysql驱动为例来分析JDBC驱动加载的源码。

我们的项目引入mysql-connector-java依赖(这里的版本是5.1.47)后,那么Mysql的驱动实现类文件如下图所示:

Java是如何实现自己的SPI机制的?

可以看到Mysql的驱动包中有两个Driver驱动类,分别是com.mysql.jdbc.Driver和com.mysql.fabric.jdbc.FabricMySQLDriver,默认情况下一般我们只用到前者。

5.1 利用Java的SPI加载Mysql的驱动类

那么接下来我们就来探究下JDBC驱动加载的代码是如何实现的。

先来看一下一个简单的JDBC的测试代码:

 

在JdbcTest的main函数调用DriverManager的getConnection方法时,此时必然会先执行DriverManager类的静态代码块的代码,然后再执行getConnection方法,那么先来看下DriverManager的静态代码块:

 

继续跟进loadInitialDrivers的代码:

 

在上面的代码中,我们可以看到Mysql的驱动类加载主要是利用Java的SPI机制实现的,即利用ServiceLoader来实现加载并实例化Mysql的驱动类。

5.2 注册Mysql的驱动类

那么,上面的代码只是Mysql驱动类的加载和实例化,那么,驱动类又是如何被注册进DriverManager的registeredDrivers集合的呢?

这时,我们注意到com.mysql.jdbc.Driver类里面也有个静态代码块,即实例化该类时肯定会触发该静态代码块代码的执行,那么我们直接看下这个静态代码块做了什么事情:

 

可以看到,原来就是Mysql驱动类com.mysql.jdbc.Driver在实例化的时候,利用执行其静态代码块的时机时将自己注册进DriverManager的registeredDrivers集合中。

好,继续跟进DriverManager的registerDriver方法:

 

分析到了这里,我们就明白了Java的SPI机制是如何加载Mysql的驱动类的并如何将Mysql的驱动类注册进DriverManager的registeredDrivers集合中的。

5.3 使用之前注册的Mysql驱动类连接数据库

既然Mysql的驱动类已经被注册进来了,那么何时会被用到呢?

我们要连接Mysql数据库,自然需要用到Mysql的驱动类,对吧。此时我们回到JDBC的测试代码JdbcTest类的connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc", "root", "123456");这句代码中,看一下getConnection的源码:

 

继续跟进getConnection方法:

 

可以看到,DriverManager的getConnection方法会从registeredDrivers集合中拿出刚才加载的Mysql驱动类来连接数据库。

好了,到了这里,JDBC驱动加载的源码就基本分析完了。

6 线程上下文类加载器

前面基本分析完了JDBC驱动加载的源码,但是还有一个很重要的知识点还没讲解,那就是破坏类加载机制的双亲委派模型的线程上下文类加载器。

我们都知道,JDBC规范的相关类(比如前面的java.sql.Driver和java.sql.DriverManager)都是在Jdk的rt.jar包下,意味着这些类将由启动类加载器(BootstrapClassLoader)加载;而Mysql的驱动类由外部数据库厂商实现,当驱动类被引进项目时也是位于项目的classpath中,此时启动类加载器肯定是不可能加载这些驱动类的呀,此时该怎么办?

由于类加载机制的双亲委派模型在这方面的缺陷,因此只能打破双亲委派模型了。因为项目classpath中的类是由应用程序类加载器(AppClassLoader)来加载,所以我们可否"逆向"让启动类加载器委托应用程序类加载器去加载这些外部数据库厂商的驱动类呢?如果可以,我们怎样才能做到让启动类加载器委托应用程序类加载器去加载classpath中的类呢?

答案肯定是可以的,我们可以将应用程序类加载器设置进线程里面,即线程里面新定义一个类加载器的属性contextClassLoader,然后在某个时机将应用程序类加载器设置进线程的contextClassLoader这个属性里面,如果没有设置的话,那么默认就是应用程序类加载器。然后启动类加载器去加载java.sql.Driver和java.sql.DriverManager等类时,同时也会从当前线程中取出contextClassLoader即应用程序类加载器去classpath中加载外部厂商提供的JDBC驱动类。因此,通过破坏类加载机制的双亲委派模型,利用线程上下文类加载器完美的解决了该问题。

此时我们再回过头来看下在加载Mysql驱动时是什么时候获取的线程上下文类加载器呢?

答案就是在DriverManager的loadInitialDrivers方法调用了ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);这句代码,而取出线程上下文类加载器就是在ServiceLoader的load方法中取出:

 

因此,到了这里,我们就明白了线程上下文类加载器在加载JDBC驱动包中充当的作用了。此外,我们应该知道,Java的绝大部分涉及SPI的加载都是利用线程上下文类加载器来完成的,比如JNDI,JCE,JBI等。

扩展:打破类加载机制的双亲委派模型的还有代码的热部署等,另外,Tomcat的类加载机制也值得一读。

7 扩展:Dubbo的SPI机制

前面也讲到Dubbo框架身上处处是SPI机制的应用,可以说处处都是扩展点,真的是把SPI机制应用的淋漓尽致。但是Dubbo没有采用默认的Java的SPI机制,而是自己实现了一套SPI机制。

那么,Dubbo为什么没有采用Java的SPI机制呢?

原因主要有两个:

由于以上原因,Dubbo自定义了一套SPI机制,用于加载自己的扩展点。关于Dubbo的SPI机制这里不再详述,感兴趣的小伙伴们可以去Dubbo官网看看是如何扩展Dubbo的SPI的?还有其官网也有Duboo的SPI的源码分析文章。

8 小结

好了,Java的SPI机制就解读到这里了,先将前面的知识点再总结下:

1,Java的SPI机制的使用;

2,Java的SPI机制的原理;

3,JDBC驱动的加载原理;

4,简述了Duboo的SPI机制。

【责任编辑:庞桂玉 TEL:(010)68476606】

热门评论

还没有评论,快来说两句

打开时讯快报  查看更多

推荐阅读