在之前的文章 Java插件化开发 中分享了利用配置文件读取插件的方式,本文将会介绍如何以 java SPI 机制加载插件

SPI 简介

SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和Oracle都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java 中 SPI 机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦。

SPI整体机制图如下:

在这里插入图片描述
关于 java SPI 机制本文不展开描述,网上文章很多,大家可以自行了解

主程序代码

主程序需要提供一个插件服务的接口,在插件的项目中实现这个接口来让主程序能够加载到插件

/**
 * 插件服务接口,插件项目需要提供实现这个接口的类,才能被主程序加载
 *
 * @author Nonoas
 * @date 2022/10/16
 */
public interface IPluginService {

    /**
     * 插件功能主入口方法
     */
    void service();

    /**
     * 插件名成,通常用于展示在界面
     *
     * @return 插件名称
     */
    String name();

    /**
     * 表示当前插件的版本
     *
     * @return 插件版本号
     */
    String version();

}

插件加载类,通过指定插件路径,从插件路径下读取所有插件的 jar,利用 java 的 SPI机制,从这些 jar 中读取 IPluginService 的实现类

/**
 * 插件加载器,用于从插件目录加载所有插件的 IPluginService 实现类
 *
 * @author Nonoas
 * @date 2022/10/16
 */
public class PluginLoader {
    /**
     * 插件加载的相对路径:这里表示所有的插件jar都放在主程序jar同级目录的 {@code PLUGIN_PATH} 文件夹下
     */
    public static final String PLUGIN_PATH = "plugins";

    public static List<IPluginService> loadPlugins() throws MalformedURLException {
        List<IPluginService> plugins = new ArrayList<>();

        File parentDir = new File(PLUGIN_PATH);
        File[] files = parentDir.listFiles();
        if (null == files) {
            return Collections.emptyList();
        }
        
        // 从目录下筛选出所有jar文件
        List<File> jarFiles = Arrays.stream(files)
                .filter(file -> file.getName().endsWith(".jar"))
                .collect(Collectors.toList());

        URL[] urls = new URL[jarFiles.size()];
        for (int i = 0; i < jarFiles.size(); i++) {
            // 加上 "file:" 前缀表示本地文件
            urls[i] = new URL("file:" + jarFiles.get(i).getAbsolutePath());
        }
        
        URLClassLoader urlClassLoader = new URLClassLoader(urls);
        // 使用 ServiceLoader 以SPI的方式加载插件包中的 IPluginService 实现类
        ServiceLoader<IPluginService> serviceLoader = ServiceLoader.load(IPluginService.class, urlClassLoader);
        for (IPluginService iPluginService : serviceLoader) {
            plugins.add(iPluginService);
        }
        return plugins;
    }
}

主程序的 main 方法,用于打印查看插件的加载结果,已经插件方法的调用结果

public class Main {

    public static void main(String[] args) throws MalformedURLException {

        System.out.println("开始加载插件");
        List<IPluginService> services = PluginLoader.loadPlugins();
        System.out.println(services.size() + "个插件加载成功\n");

        for (int i = 0; i < services.size(); i++) {
            IPluginService service = services.get(i);
            System.out.println("===插件" + i + "===");
            System.out.println("插件名:" + service.name());
            System.out.println("版本号:" + service.version());
            System.out.println("插件服务启动:");
            service.service();
        }

    }
}

编写好以上代码之后,将主程序打成 jar 包。

插件代码

下面用两个插件项目来实现插件,分别叫 「插件一」「插件二」,插件项目不需要定义 main 方法,但需要定义 IPluginService 的实现类,插件的功能是由主程序加载之后,调用 service 方法触发的。

在插件项目中,把打包好的主程序 jar 作为依赖引入,因为 IPluginService 是在主程序中定义的
在这里插入图片描述

插件一

/**
 * 插件一的 IPluginService 实现类,也是插件的主类
 *
 * @author Nonoas
 * @date 2022/10/16
 */
public class Plugin1Service implements IPluginService {
    @Override
    public void service() {
        // 这里可以做插件需要做的任何事情,这里仅用一句打印表示插件的功能被调用
        System.out.println(name() + "功能调用");
    }

    @Override
    public String name() {
        return "插件一";
    }

    @Override
    public String version() {
        return "1.2.5";
    }
}

在插件一项目下创建资源文件目录 resources(打包后目录中的文件会在jar包内的根目录), resources 目录下创建目录:META-INF/services,目录下新建文件,文件名为主程序中 IPluginService 的全类名,文件内容为当前插件项目中,实现了 IPluginService 接口的类的全类名

在这里插入图片描述

插件二

/**
 * 插件二的 IPluginService 实现类,也是插件的主类
 *
 * @author Nonoas
 * @date 2022/10/16
 */
public class Plugin2Service implements IPluginService {
    @Override
    public void service() {
        // 这里可以做插件需要做的任何事情,这里仅用一句打印表示插件的功能被调用
        System.out.println(name() + "功能调用");
    }

    @Override
    public String name() {
        return "插件二";
    }

    @Override
    public String version() {
        return "2.2.5";
    }
}

在插件二项目下创建资源文件目录 resources(打包后目录中的文件会在 jar 包内的根目录), resources 目录下创建目录:META-INF/services,目录下新建文件,文件名为主程序中 IPluginService 的全类名,文件内容为当前插件项目中,实现了 IPluginService 接口的类的全类名
在这里插入图片描述

打包和运行

将主程序和两个插件项目打成 jar 包,插件的jar放在和主程序jar同级目录的 plugins 文件夹下

JAVA插件DEMO
│ 
│  MainApp.jar
│
└─plugins
        Plugin1.jar
        Plugin2.jar

使用 cmd 启动主程序,输出结果如下:

在这里插入图片描述