本文描述一种Java SPI机制与Groovy相结合的方式,实现中借鉴了Dubbo SPI的思想,旨在提供一种更加动态灵活的集成方式。抛砖引玉。
SPI,想必大家对此耳熟能详,全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器(java.util.ServiceLoader
)读取配置文件,加载实现类。这样可以在运行时动态地为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。
SPI机制的约定:
在META-INF/services/目录中创建以接口全限定名命名的文件该文件内容为Api具体实现类的全限定名
使用ServiceLoader类动态加载META-INF中的实现类
如SPI的实现类为Jar则需要放在主程序classPath中
API具体实现类必须有一个不带参数的构造方法
以JDBC MySQL connector为例,在mysql-connector-java.jar中, META-INF/services/java.sql.Driver
文件中写明了
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
java.sql.DriverManager
类初始化时会调用loadInitialDrivers
方法,该方法通过ServiceLoader#load
遍历所有的Driver,
从而加载到MySQL driver的实现类。
Dubbo 并未使用原生的Java SPI,而是重新实现了一套功能更强的 SPI 机制。
Dubbo框架本身提供了很多的扩展点加载。例如协议、负载均衡、序列化、线程池等等,在具体使用时,只需要使用对应的参数配置即可引入实现。
例如,对于发布服务时的引用协议,protocol的值可以是dubbo、hessian、http、thrift等等,也可以是用户定义并实现的某一种协议。
<dubbo:provider protocol="xxx1" />
那么在具体的实现中,所有的服务协议实现自Protocol
接口,不同的名称的协议通过ExtensionLoader加载,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类,而Dubbo SPI 所需的配置文件则放置在 META-INF/dubbo 路径下,对于com.alibaba.dubbo.rpc.Protocol,
其配置内容如下
mock=com.alibaba.dubbo.rpc.support.MockProtocol
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol
thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
...
在文件中,key为不同协议的名称,value为对应的实现。
如果是用户自己的实现的Protocol,则需要将相应实现打成jar在classpath下,并且jar文件的 META-INF/dubbo目录下包含对应的com.alibaba.dubbo.rpc.Protocol
接口文件和配置内容。
总结一下,Java SPI 和 Dubbo SPI都首先需要定义一个接口,然后通过一个Loader去加载指定目录下该接口文件名所定义的实现类。
现如今,SpringBoot大行其道,我们的应用war/jar包往往都是Fat Jar, 那么就意味着如果想要使用SPI,那么实现类的Jar则需要提前打到Fat Jar中,这样classLoader才能加载到Jar中的类,那么就需要预先引入依赖进行控制,而令这个SPI机制变得不那么的可插拔了。
退一步讲,即使是不使用SpringBoot,那么一个新的实现类引入,必须要放到classpath下的指定目录,然后免不了重启这个应用去加载。
那么能不能更加地动态灵活呢?
由此引出我的项目,基于Groovy实现的SPI,spi-groovy-integration简称SGI(https://github.com/timestatic/spi-groovy-integration),通过Groovy的动态灵活的特性,可以随时随地加载实现类,从文件、数据库.(关于Groovy的简介和Java项目的集成可参考上一篇文章Groovy简介与使用)。
下面举个例子:
首先使用 @SGI
注解,定义一个接口,(旅行可以有多种方式)
@SGI
public interface TravelStrategy {
String travel(String destination);
}
实现接口,并使用 @Impl
注解,在注解中指定名称(可以骑自行车去旅行)
@GImpl(value = "bike")
public class BikeTravelStrategy implements TravelStrategy {
@Override
public String travel(String destination) {
return "go to " + destination + " by bike ~";
}
}
使用 ExtensionLoader
,查找对应的SGI接口实现类并加载,然后根据名称调用相应方法(骑自行车去hangzhou)
ExtensionLoader<TravelStrategy> loader = ExtensionLoader.getExtensionLoader(TravelStrategy.class);
for (Class<?> clazz : PackageScanner.findGImplClass(TravelStrategy.class)) {
loader.addExtension(clazz);
}
loader.getExtension("bike").travel("hangzhou")
也可以从外部文件中加载实现类, 例如将如下内容保存为NuoyafangzhuoTravelStrategy.groovy
package com.github.sgi.example.service
@GImpl(value = "nuoyafangzhou")
class NuoYaFangZhouTrave implements TravelStrategy {
@Override
String travel(String destination) {
return "go to " + destination + " by nuo ya fang zhou ~ .";
}
}
在应用中读取该文件, 并加载(这样就可以及时应对发大水的情况了)
String path = "/root/file/NuoyafangzhuoTravelStrategy.groovy";
loader.addExtension(FileScanner.readFile(path));
loader.getExtension("nuoyafangzhou").travel("hangzhou")
当然, 也可以开启一个定时任务, 定时读取最新的文件内容重新加载
也可以将实现类注册为spring bean
@GImpl(isSpringBean = true, value = "flight")
// ....
// register
ExtensionSpringLoader.getExtensionSpringLoader(TravelStrategy.class).addExtensionSpringContext(script)
// get bean
beanFactory.getBean("flight")
应用场景
结合业务系统的参数, 不同的参数可以调用不同的实现类, 基于Groovy动态的特性, 实现类本身也可以随时地变化,让系统更灵活,快速响应业务变化。
在某些环境下,比如需要对接甲方的业务系统,那么在我们自己的环境中是无法搭建相同的环境,同时对接文档可能也不够完善,难以测试,更是无法debug,全靠打日志推敲以解决问题。因此在对接的过程中免不了要反复的去传文件重启调试,当然也可以通过热部署或者像是Arthas的redfine重新加载class(用过之后依然感觉还是不够方便)。那么在使用了这种SPI机制之后,可以让代码的修改和类加载更加快速有效。