Java进阶:常常听到的热插拔插件功能原来这么简单

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实现两个实现类OssServiceMinioService

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

  • 26
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wu@55555

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值