在之前的文章 Java插件化开发 中分享了利用配置文件读取插件的方式,本文将会介绍如何以 java 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 启动主程序,输出结果如下: