0. 引言
我们经常可以看到很多系统支持热插拔的插件功能,支持客户二次开发扩展,而无需对源码进行侵入。那么这样的功能是如何实现的呢?今天我们一起来探究清楚。
1. 思路
首先我们要明白,我们做插件扩展,实际上就是针对接口类做实现类的扩展,这就需要原程序本身预留了接口类,方便后续对接口进行二开。而这样的需求,实际上很契合java的SPI机制,或者说SPI本身就是为了这样的需求而创建的,那么SPI是什么呢?
2. SPI简介
SPI是java提供的一直接口提供服务,或者说接口动态定制机制,之前我们介绍过java SPI实现接口扩展,而热插拔插件实际上也是基于SPI来实现的。SPI有3个核心步骤:
(1)对接口类实现扩展的实现类
(2)在资源目录中声明实现类路径
(3)通过ServiceLoader.load
加载扩展的实现类,从而实现调用
如果完全对SPI没有了解的可以先查看我之前的文章,介绍的更加详细,我们下面的演示也将基于之前的项目来开展:
Java进阶:利用SPI机制不侵入源码而实现定制功能【附带源码】
3. 实现步骤
我们整体上会分为3个模块实现:
- 工具模块
这是需要提供给第三方开发人员的公共服务,内部提供了接口类,可能会有自带的默认实现类,方便第三方开发人员参考实现,接口类用于声明实现
- 主程序模块
这是主要的程序,提供了对插件模块的兼容调用,这块的代码对第三方开发人员来说是隐藏的
- 插件模块
第三方开发人员实现的插件模块,包含声明了接口类的扩展实现类,需要通过jar包或者反编译的形式发布到主程序中
3.1 工具模块实现
首先我们先明确我们要实现的效果,设定我们有一个原程序spi_demo
,该程序有一个创建文件服务器目录的接口,原生支持minio, oss, obs服务,现在我们需要开发一个插件,来增加对ftp服务的支持。
1、首先我们创建一个工具模块spi_demo_import
,将接口类声明在该模块中,这个模块就是需要暴露给客户的开发人员的,提供给客户进行插件扩展,可以发布到maven中央仓库,然后客户通过pom坐标引入,或者提供jar包给客户
注意这里与之前的文章对比,增加了type()
方法,目的是为了给不同的实现类声明对应的名称,这在后续根据名称获取对应实现类实例时起到作用
public interface IFileService {
String type();
String makeBucket(String bucketName);
boolean existBucket(String bucketName);
boolean removeBucket(String bucketName);
boolean setBucketExpires(String bucketName, int days);
void upload(String bucketName, String fileName, InputStream stream);
}
2、我们可以在该模块中创建一些默认支持的实现类,将其作为一个功能模块,这里我们默认实现一个Obs的实现类
public class ObsService implements IFileService{
@Override
public String type() {
return "obs";
}
@Override
public String makeBucket(String bucketName) {
return "obs create " + bucketName + " bucket success";
}
@Override
public boolean existBucket(String bucketName) {
return false;
}
@Override
public boolean removeBucket(String bucketName) {
return false;
}
@Override
public boolean setBucketExpires(String bucketName, int days) {
return false;
}
@Override
public void upload(String bucketName, String fileName, InputStream stream) {
}
}
3、因为我们要根据名称在加载对应的实现类实例,所以我们需要先书写一个getService(String name)
方法
先实现一个类加载器FileServiceLoader
,将所有实现类都加载到Map中,利用本地缓存减少加载次数
public class FileServiceLoader {
private static final Map<Class<?>, Collection<Class<?>>> SERVICES = new ConcurrentHashMap<>();
public static <T> Collection<T> load(final Class<T> service) {
if (SERVICES.containsKey(service)) {
return newServiceInstances(service);
}
Collection<T> result = new LinkedHashSet<>();
for (T each : ServiceLoader.load(service)) {
result.add(each);
cacheServiceClass(service, each);
}
return result;
}
private static <T> void cacheServiceClass(final Class<T> service, final T instance) {
SERVICES.computeIfAbsent(service, k -> new LinkedHashSet<>()).add(instance.getClass());
}
public static <T> Collection<T> newServiceInstances(final Class<T> service) {
return SERVICES.containsKey(service) ? newServiceInstancesFromCache(service) : Collections.<T>emptyList();
}
@SuppressWarnings("unchecked")
private static <T> Collection<T> newServiceInstancesFromCache(Class<T> service) {
Collection<T> result = new LinkedHashSet<>();
for (Class<?> each : SERVICES.get(service)) {
result.add((T) newServiceInstance(each));
}
return result;
}
private static Object newServiceInstance(final Class<?> clazz) {
try {
return clazz.getDeclaredConstructor().newInstance();
}catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
然后再实现获取实现类的管理器FileServiceManager
public class FileServiceManager {
private static final FileServiceManager MANAGER = new FileServiceManager();
private final Map<String, IFileService> SERVICE_MAP = new HashMap<>();
public FileServiceManager() {
init();
}
private void init(){
// FileServiceLoader通过反射加载和实例化
System.out.println("初始化...");
Collection<IFileService> authPluginServices = FileServiceLoader.load(IFileService.class);
for (IFileService service : authPluginServices) {
System.out.println(service.type()+"初始化成功");
SERVICE_MAP.put(service.type(), service);
}
}
/**
* 获取单例
* @return
*/
public static FileServiceManager getInstance(){
return MANAGER;
}
/**
* 获取对应的实现类
* @param type
* @return
*/
public IFileService getService(String type){
return SERVICE_MAP.get(type);
}
}
注,这里代码参考文章:https://blog.csdn.net/zh19940106/article/details/129037643
这里的类加载器和管理器的代码如果不想暴露给客户的话,将其迁移到其他模块即可,客户需要的只是接口类
3.2 主程序实现
3、然后我们创建一个spi_demo
模块,该项目中引入spi_demo_import
,该项目作为主程序,实现一个调用接口
添加一个配置项,用于声明默认的服务类型,如果后续我们增加了类型,也可通过修改配置项中对应的值来实现调整的目的
file:
service:
type: obs
4、除了工具模块自带的ObsService,我们再在spi_demo
实现两个实现类OssService
和MinioService
public class MinioService implements IFileService {
@Override
public String type() {
return "minio";
}
@Override
public String makeBucket(String bucketName) {
return "minio create " + bucketName + " bucket success";
}
@Override
public boolean existBucket(String bucketName) {
return false;
}
@Override
public boolean removeBucket(String bucketName) {
return false;
}
@Override
public boolean setBucketExpires(String bucketName, int days) {
return false;
}
@Override
public void upload(String bucketName, String fileName, InputStream stream) {
}
}
public class OssService implements IFileService {
@Override
public String type() {
return "oss";
}
@Override
public String makeBucket(String bucketName) {
return "oss create " + bucketName + " bucket success";
}
@Override
public boolean existBucket(String bucketName) {
return false;
}
@Override
public boolean removeBucket(String bucketName) {
return false;
}
@Override
public boolean setBucketExpires(String bucketName, int days) {
return false;
}
@Override
public void upload(String bucketName, String fileName, InputStream stream) {
}
}
5、然后在spi_demo
项目的resource目录下创建META-INF/services
文件夹,再创建以IFileService
包名命名的文本文件com.example.file.IFileService
,并将所有已有的实现类声明
com.example.spi_demo.service.MinioService
com.example.spi_demo.service.OssService
com.example.file.ObsService
6、实现调用接口,这里为了方便测试将type
作为入参,实际实现插件时,可以将其配置到数据库,通过后台页面选择默认的类型,然后从数据库或缓存加载选择的类型值,从而进行调用
这里需要注意,我们的调用流程本身就要预留好对扩展插件的接入,比如这里使用的是IFileService
接口来承装,而不是具体的实现类,并且通过类型名称来获取对应实例,这就预留了后续扩展的空间
@RestController
public class DemoController {
@Value("${file.service.type}")
private String defaultType;
@GetMapping("create")
public String create(String name, String type){
if(type == null || type.length() == 0){
type = defaultType;
}
IFileService service = FileServiceManager.getInstance().getService(type);
return service.makeBucket(name);
}
}
7、然后我们启动项目,通过调整type参数依次访问这几种类型的实现类
type=minio
type=oss
type=obs
type=ftp
可以看到type=ftp时报错了,因为我们还没有实现针对ftp的实例,接下来我们开始正式实现插件的扩展
8、最后调整pom中的打包设置<layout>ZIP</layout>
,否则无法将第三方jar引入进去,方便后续我们能引入插件jar
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.example.spi_demo.SpiDemoApplication</mainClass>
<layout>ZIP</layout>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
9、执行打包语句,将主程序打包
mvn clean package -Dmaven.test.skip=true
3.3 插件实现
1、我们创建一个spi_demo_extend
项目,用于实现我们的扩展插件,增加一个ftp实现类FtpService
public class FtpService implements IFileService {
@Override
public String type() {
return "ftp";
}
@Override
public String makeBucket(String bucketName) {
return "ftp create " + bucketName + " bucket success";
}
@Override
public boolean existBucket(String bucketName) {
return false;
}
@Override
public boolean removeBucket(String bucketName) {
return false;
}
@Override
public boolean setBucketExpires(String bucketName, int days) {
return false;
}
@Override
public void upload(String bucketName, String fileName, InputStream stream) {
}
}
2、同时也需要在该项目的资源目录下创建META-INF/services
目录,同样创建com.example.file.IFileService
文本文件,内容上就只声明你这里创建的这个实现类即可(实际上原有的你应该也不知道路径)
com.example.spi_demo_extend.service.FtpService
3、将该模块打包成jar
4、在主程序spi_demo
包同级目录下创建一个plugins
目录,然后将插件spi_demo_extend
打包出的jar包添加到plugins
目录下(当然这里的plugins目录你可以自定义,只要后续的指定路径一致即可)
5、启动主程序时通过-Dloader.path
参数指定插件路径
java -Dloader.path=./plugins -jar spi_demo-0.0.1-SNAPSHOT.jar
6、这次我们再访问ftp,则可正常访问了,说明我们的插件已经生效
当然,原有的实现类也是可以继续使用的
4. 总结
到这里我们针对java热插拔插件的实现就完成了,跟着操作下来你会发现神秘的热插拔功能或许没有那么难以实现
本文演示源码可见:
https://gitee.com/wuhanxue/wu_study/tree/master/demo/spi_demo
https://gitee.com/wuhanxue/wu_study/tree/master/demo/spi_demo_extend