最近为项目整合动态多数据源,忽然想到一个问题:每种数据库需要特定的JDBC,在使用的时候只需要引入对应依赖包,那么java包是怎么引用的呢?这里至少需要解决这样的问题:
- 必须是由java定义统一接口,各个数据库厂商或者开源社区针对数据库提供实现,不然适配这么多数据库是完全不可能也不合理的,毫无扩展性可言
- 只能在运行时加载JDBC驱动实现类,所以需要提供一种方式,使得类加载器能够找到各个驱动的实现类完成加载
带着问题浏览了java.sql下的关键类,了解到这种机制称为SPI,即Service Provider Interface。这种模式适合为框架提供扩展点,不难联想到,spring通过spring.factories加载类也是SPI模式的应用
java.sql.DriverManager加载驱动类流程
DriverManager类为我们提供了一个使用SPI的很好的样例,所以还是从这里入手,将java SPI作为黑盒,先看下基于SPI完成jdbc驱动类加载和使用的整体流程
java.sql下关于驱动加载的类是java.sql.DriverManager和java.sql.Driver,其中,Driver类是为不同驱动提供的统一接口:The interface that every driver class must implement,而驱动类的加载是在DriverManager类中进行的
通过类的注释可以了解到,指定驱动类有方式:
- 通过JVM参数:the DriverManager class will attempt to load the driver classes referenced in the “jdbc.drivers” system property
- 通过JAVA标准的SPI机制:The DriverManager methods getConnection and getDrivers have been enhanced to support the Java Standard Edition Service Provider mechanism. JDBC 4.0 Drivers must include the file META-INF/services/java.sql.Driver
加载驱动类的关键代码在loadInitialDrivers函数中:
1 | private static void loadInitialDrivers() { |
在之前关于类加载器的文章java类加载器问题思考与简单模拟热部署中了解过,mysql等jdbc Driver类中,通过static代码块的方式,在Driver类加载时完成初始化,关键的代码为:
1 | static { |
这里进行了Driver类的实例化,最终调用回到了DriverManager类的registerDriver方法,这里关键的代码是记录了Driver实现类的信息
1 | /* Register the driver if it has not already been added to our list */ |
而在对数据库进行操作时,可以看到,getConnection()方法中有遍历选取Driver实现类的步骤:
1 | // 遍历所有已注册的Driver实现类,选择与当前连接的数据库匹配的执行操作 |
所以,整个的流程是:DriverManager类通过SPI机制加载所有的Driver实现类 –> Driver实现类通过类加载机制,在加载时将自身信息注册到DriverManager –> 连接数据库时,DriverManager遍历所有注册的Driver实现类,选取匹配的实现类执行
java SPI
下面继续来看下java SPI的使用方式
基本的使用很简单,首先定义接口和实现类,为了方便实现类直接一起放在了源代码里:
1 | // 接口MyInterface com.connorma.spi_learn.MyInterface |
然后在resources目录下新建目录META-INF/services,添加文件“com.connorma.spi_learn.MyInterface”,即接口类的全限定名,文件内容写入:
1 | com.connorma.spi_learn.MyInterfaceA |
即将两个实现类的全限定名按行写入
这样打包为jar之后,可以看到jar包内包含了META-INF/services/com.connorma.spi_learn.MyInterface文件
在代码中可以使用ServiceLoader类加载:
1 | ServiceLoader<MyInterface> serviceLoader = ServiceLoader.load(MyInterface.class); |
执行以上代码则输出:
1 | com.connorma.spi_learn.MyInterfaceA |
这只是一个简单的使用演示,真正使用的使用方式,那么需要像DriverManager一样,处理实现类的选择和使用等问题
ServiceLoader的加载方式
从上面演示代码的输出可以看出,ServiceLoader类在调用load函数时并不会完成实现类的加载,而是在以懒加载的形式,在通过迭代器获取类实例时才加载的——实际上看代码可以发现,ServiceLoader中定义了内部类LazyIterator,是在迭代时才去读取META-INF/services下的文件、加载类的
LazyIterator类的hasNext()方法最终调用了内部的hasNextService()方法,返回下一个实现类的类名,关键代码为:
1 | String fullName = PREFIX + service.getName(); |
而LazyIterator类的next()方法最终调用了内部的nextService()方法,在其中进行了类的加载和实例化,关键代码为:
1 | String cn = nextName; |
java SPI的问题
ServiceLoader类的实现简单清晰,但是真打算使用的时候,细细一想,感觉就是注释里说的:A simple service-provider loading facility.
ServiceLoader的实现并没有提供太多的自定义选项,功能上支持的很简单。
ServiceLoader只能一次性读取所有META-INF/services下的文件,之后在迭代中依次加载类和实例化。这样,无法有效的指定加载某个或某种实现类,也存在类加载、实例化的资源消耗。设想一种场景:在使用jdbc驱动类中,虽然依赖同时引入了mysql、pg的驱动,但是希望在运行中可以指定加载某种数据库的驱动类;想实现这种方式,最容易想到的是在指定驱动类类名时可以通过键值对进行,比如:
1 | mysql=com.mysql.cj.jdbc.Driver |
这样在运行时就可以根据配置参数决定要加载的实现类
但是,ServiceLoader类并没有开放这类自定义扩展的能力
这个例子并不太准确,毕竟mysql/pg的驱动类并不是可以相互替换的实现,只是用于说明问题
dubbo的SPI
之前了解了java SPI后也并没有继续去看其他框架的扩展点设计,直到最近看到了dubbo的SPI实现,很好的解决了上文提到的java SPI的问题,提供了以下特性(from dubbo文档):
- 按需加载。Dubbo 的扩展能力不会一次性实例化所有实现,而是用哪个扩展类则实例化哪个扩展类,减少资源浪费。
- 增加扩展类的 IOC 能力。Dubbo 的扩展能力并不仅仅只是发现扩展服务实现类,而是在此基础上更进一步,如果该扩展类的属性依赖其他对象,则 Dubbo 会自动的完成该依赖对象的注入功能。
- 增加扩展类的 AOP 能力。Dubbo 扩展能力会自动的发现扩展类的包装类,完成包装类的构造,增强扩展类的功能。
- 具备动态选择扩展实现的能力。Dubbo 扩展会基于参数,在运行时动态选择对应的扩展类,提高了 Dubbo 的扩展能力。
- 可以对扩展实现进行排序。能够基于用户需求,指定扩展实现的执行顺序。
- 提供扩展点的 Adaptive 能力。该能力可以使的一些扩展类在 consumer 端生效,一些扩展类在 provider 端生效。
在此学习了解下,这里使用的是dubbo 3.1的源代码
dubbo SPI的整体处理流程肯定还是相同的:首先查找、加载扩展类,然后完成类的实例化和后置处理(为扩展类进行依赖注入等)
dubbo SPI的使用
首先从用户视角看下如何使用dubbo SPI声明扩展类,相关的注解有:
- @Adaptive,自适应加载扩展类,一般在扩展点接口的函数上使用,需要函数包含一个org.apache.dubbo.common.URL类型的参数,将在运行时根据URL中的参数值确定加载的实现类
- @Activate,在扩展类上使用,可通过group、value配置扩展类的加载规则,可通过order指定扩展类的加载次序
dubbo SPI扩展类示例
下面以一个示例说明dubbo SPI的使用方式。假设存在一个接口定义了发起网络请求的方法,扩展类分别实现了使用http、https协议的请求方式
为了方便演示定义一个接口用于扩展
1 | // 接口 com.connorma.dubbo_spi_learn.RequestInterface |
其中,@SPI注解用于标记一个扩展点接口,http表示默认使用的扩展类的key
然后添加两个实现类:
1 | // 实现类1 http=com.connorma.dubbo_spi_learn.HttpRequestCaller |
并添加文件META-INF/dubbo/com.connorma.dubbo_spi_learn.RequestInterface,内容为:
1 | http=com.connorma.dubbo_spi_learn.HttpRequestCaller |
文件内容是按行的键值对,为每个扩展类指定一个key;之前@SPI注解指定的默认扩展类是key为http的com.connorma.dubbo_spi_learn.HttpRequestCaller
执行代码,加载扩展类和调用:
1 | ExtensionLoader<RequestInterface> loader = ExtensionLoader.getExtensionLoader(RequestInterface.class); |
输出为:
1 | com.connorma.dubbo_spi_learn.HttpRequestCaller |
这里演示的代码额外的引入了URL参数,实际在这个例子里并没有作用,可以结合使用@Adaptive的示例来看
使用@Adaptive根据URL参数动态选用扩展类
在以上的代码中提供了两个扩展类,key分别为http和https,并在接口定义时指定了默认使用key为http的扩展类
接下来将演示通过@Adaptive注解,根据URL携带的参数,动态选取扩展类
只需要为接口的方法上添加@Adaptive注解:
1 | // 接口 com.connorma.dubbo_spi_learn.RequestInterface |
加载扩展类时使用getAdaptiveExtension()方法:
1 | ExtensionLoader<RequestInterface> loader = ExtensionLoader.getExtensionLoader(RequestInterface.class); |
输出为:
1 | com.connorma.dubbo_spi_learn.HttpRequestCaller |
可以看到,当指定使用adaptive extension时,@Adaptive生效,根据URL参数type的值,分别调用了key为http和https的两个扩展类
使用@Activate动态选用扩展类
了解了@Adaptive的功能之后,@Activate注解的功能也就了然了:在根据URL参数选用扩展类之上,增加了优先级更高的使用group参数选取,并且多个扩展类之间可以指定次序
对应@Activate注解的字段,ExtensionLoader类的getActivateExtension函数允许指定group和key获取扩展类示例
dubbo SPI加载扩展类流程
dubbo扩展类的加载和实例化等操作在ExtensionLoader中进行,其中还涉及到几个类:
- LoadingStrategy,加载策略接口,可指定扩展类声明文件的路径、包含/排除的包名等;LoadingStrategy本身就由java SPI加载实现类来使用——所以说,只要我们自定义一个LoadingStrategy,就可以在其中自定义扩展类声明文件路径等信息
扩展类的查找由getExtensionClasses()进而调用loadExtensionClasses(),最终进入了loadDirectory() –> loadDirectoryInternal(),不过最终的查找、解析扩展类声明文件、加载类的方式与java SPI并无大的差异,只是在最终缓存类对象时,根据@Adaptive和@Activate注解使用的不同情况,分类进行了缓存
扩展类的实例化和后置处理
ExtensionLoader中关键的方法:
- 使用getExtension()方法,默认的加载方式
- 使用getAdaptiveExtension()方法,处理@Adaptive注解
- 使用getActivateExtension()方法,处理@Activate注解
扩展类的实例化和后置处理中,以createAdaptiveExtension()方法内部为例,关键代码为:
1 | // 实例化 |