Plugin - 插件开发06_开源项目JPom中的插件实现机制
文章目录
- Pre
- 工程结构
- 概述
- 1. 插件接口与实现分析
- 2. 插件工厂初始化分析
- 3. 插件项包装类解析
- 4. 插件工厂方法解析
- 5. 插件加载与资源释放机制
- 6. 实现类
- 小结
- 附PluginFactory
Pre
插件 - 通过SPI方式实现插件管理
插件 - 一份配置,离插件机制只有一步之遥
插件 - 插件机制触手可及
Plugin - 插件开发01_SPI的基本使用
Plugin - 插件开发02_使用反射机制和自定义配置实现插件化开发
Plugin - 插件开发03_Spring Boot动态插件化与热加载
Plugin - 插件开发04_Spring Boot中的SPI机制与Spring Factories实现
Plugin - 插件开发05_Solon中的插件实现机制
工程结构
概述
接下来我们主要对IPlugin接口及其实现,以及插件的加载和管理机制进行分析,分为如下几个点
- 插件接口与实现分析
- 插件工厂初始化分析
- 插件项包装类解析
- 插件工厂方法解析
- 插件加载与资源释放机制
1. 插件接口与实现分析
插件机制的核心接口是IPlugin
, IPlugin
继承了AutoCloseable
接口,确保插件可以被正确关闭。
此外,还有一个IDefaultPlugin
接口,继承了IPlugin,用于定义默认插件。
接口的主要代码如下:
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ClassUtil;
import com.alibaba.fastjson.JSONObject;
import java.util.HashMap;
import java.util.Map;
/**
* 插件模块接口
*
* @author bwcx_jzy
* @since 2021/12/22
*/
public interface IPlugin extends AutoCloseable {
/**
* 执行插件方法
*
* @param main 拦截到到对象
* @param parameter 执行方法传人的参数
* @return 返回值
* @throws Exception 异常
*/
Object execute(Object main, Map<String, Object> parameter) throws Exception;
/**
* 执行插件方法
*
* @param main 主参数
* @param parameters 其他参数
* @return 结果
* @throws Exception 异常
*/
default Object execute(Object main, Object... parameters) throws Exception {
// 处理参数
int length = parameters.length;
Map<String, Object> map = new HashMap<>(length / 2);
for (int i = 0; i < length; i += 2) {
map.put(parameters[i].toString(), parameters[i + 1]);
}
return this.execute(main, map);
}
/**
* 执行插件方法
*
* @param main 拦截到到对象
* @param parameters 其他参数
* @param <T> 泛型
* @param cls 返回值类型
* @return 返回值
* @throws Exception 异常
*/
default <T> T execute(Object main, Class<T> cls, Object... parameters) throws Exception {
Object execute = this.execute(main, parameters);
return this.convertResult(execute, cls);
}
/**
* 执行插件方法
*
* @param main 拦截到到对象
* @param parameter 执行方法传人的参数
* @param <T> 泛型
* @param cls 返回值类型
* @return 返回值
* @throws Exception 异常
*/
default <T> T execute(Object main, Map<String, Object> parameter, Class<T> cls) throws Exception {
Object execute = this.execute(main, parameter);
return this.convertResult(execute, cls);
}
/**
* 转换结果
*
* @param execute 结果
* @param cls 返回值类型
* @param <T> 泛型
* @return 返回值类型
*/
@SuppressWarnings("unchecked")
default <T> T convertResult(Object execute, Class<T> cls) {
if (execute == null) {
return null;
}
Class<?> aClass = execute.getClass();
if (ClassUtil.isSimpleValueType(aClass)) {
return (T) Convert.convert(aClass, execute);
}
// json 数据
Object o = JSONObject.toJSON(execute);
if (o instanceof JSONObject) {
JSONObject jsonObject = (JSONObject) o;
return jsonObject.toJavaObject(cls);
}
return (T) execute;
}
/**
* 系统关闭,插件资源释放
*
* @throws Exception 异常
*/
@Override
default void close() throws Exception {
}
}
主要方法解析
-
execute 方法:
execute(Object main, Class<T> cls, Object... parameters)
:此方法接收一个对象、一个返回值类型和一组参数,执行插件方法后返回指定类型的结果。execute(Object main, Map<String, Object> parameter, Class<T> cls)
:此方法接收一个对象、一个参数Map和一个返回值类型,执行插件方法后返回指定类型的结果。
-
convertResult 方法:
- 该方法用于将执行结果转换为指定类型。它首先判断结果是否为简单类型,如果是则直接转换;否则将结果转换为JSON对象,再将JSON对象转换为指定类型。
-
close 方法:
- 实现了
AutoCloseable
接口,用于在系统关闭时释放插件资源。默认实现为空方法。
- 实现了
2. 插件工厂初始化分析
插件工厂类PluginFactory
负责插件的初始化和管理 ,实现了ApplicationContextInitializer
和ApplicationListener
接口,用于在Spring上下文初始化和关闭时进行插件的加载和资源释放。
初始化核心方法的主要代码如下:
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
init();
// 扫描插件 实现
Set<Class<?>> classes = ClassUtil.scanPackage("io.jpom", IPlugin.class::isAssignableFrom);
List<PluginItemWrap> pluginItemWraps = classes
.stream()
.filter(aClass -> ClassUtil.isNormalClass(aClass) && aClass.isAnnotationPresent(PluginConfig.class))
.map(aClass -> new PluginItemWrap((Class<? extends IPlugin>) aClass))
.filter(pluginItemWrap -> {
if (StrUtil.isEmpty(pluginItemWrap.getName())) {
DefaultSystemLog.getLog().warn("plugin config name error:{}", pluginItemWrap.getClassName());
return false;
}
return true;
})
.collect(Collectors.toList());
//
Map<String, List<PluginItemWrap>> pluginMap = CollStreamUtil.groupByKey(pluginItemWraps, PluginItemWrap::getName);
pluginMap.forEach((key, value) -> {
// 排序
value.sort((o1, o2) -> Comparator.comparingInt((ToIntFunction<PluginItemWrap>) value1 -> {
Order order = value1.getClassName().getAnnotation(Order.class);
if (order == null) {
return 0;
}
return order.value();
}).compare(o1, o2));
PLUGIN_MAP.put(key, value);
});
log.debug("load plugin count:{}", pluginMap.keySet().size());
}
3. 插件项包装类解析
PluginItemWrap
类用于封装插件的相关信息,如插件配置、插件名、插件类名和插件对象,实现了对插件的包装和插件实例的创建。
import cn.hutool.core.util.ReflectUtil;
import cn.jiangzeyin.common.spring.SpringUtil;
import lombok.Getter;
/**
* 插件端对象
*
* @author bwcx_jzy
* @since 2021/12/24
*/
@Getter
public class PluginItemWrap {
/**
* 配置相关
*/
private final PluginConfig pluginConfig;
/**
* 插件名
*/
private final String name;
/**
* 插件类名
*/
private final Class<? extends IPlugin> className;
/**
* 插件对象
*/
private volatile IPlugin plugin;
public PluginItemWrap(Class<? extends IPlugin> className) {
this.className = className;
this.pluginConfig = className.getAnnotation(PluginConfig.class);
this.name = this.pluginConfig.name();
}
public IPlugin getPlugin() {
if (plugin == null) {
synchronized (className) {
if (plugin == null) {
//
boolean nativeObject = this.pluginConfig.nativeObject();
if (nativeObject) {
plugin = ReflectUtil.newInstance(className);
} else {
plugin = SpringUtil.getBean(className);
}
}
}
}
return plugin;
}
}
4. 插件工厂方法解析
插件工厂提供了若干静态方法供外部使用,如获取插件对象、判断是否包含某个插件以及获取插件数量等。
public static IPlugin getPlugin(String name) {
List<PluginItemWrap> pluginItemWraps = PLUGIN_MAP.get(name);
PluginItemWrap first = CollUtil.getFirst(pluginItemWraps);
Assert.notNull(first, "对应找到对应到插件:" + name);
return first.getPlugin();
}
public static boolean contains(String name) {
return PLUGIN_MAP.containsKey(name);
}
public static int size() {
return PLUGIN_MAP.size();
}
5. 插件加载与资源释放机制
在PluginFactory
文件中,实现了插件的加载和资源释放机制。在init
方法中,扫描指定包下的插件并加载。在onApplicationEvent
方法中,监听Spring上下文关闭事件并释放插件资源。
这两个方法的主要代码如下:
private static void init() {
File runPath = JpomManifest.getRunPath().getParentFile();
File plugin = FileUtil.file(runPath, "plugin");
if (!plugin.exists() || plugin.isFile()) {
return;
}
// 加载二级插件包
File[] dirFiles = plugin.listFiles(File::isDirectory);
if (dirFiles != null) {
for (File file : dirFiles) {
File lib = FileUtil.file(file, "lib");
if (!lib.exists() || lib.isFile()) {
continue;
}
File[] listFiles = lib.listFiles((dir, name) -> StrUtil.endWith(name, FileUtil.JAR_FILE_EXT, true));
if (listFiles == null || listFiles.length <= 0) {
continue;
}
for (File listFile : listFiles) {
addPlugin(file.getName(), listFile);
}
}
}
// 加载一级独立插件端包
File[] files = plugin.listFiles(pathname -> FileUtil.isFile(pathname) && FileUtil.JAR_FILE_EXT.equalsIgnoreCase(FileUtil.extName(pathname)));
if (files != null) {
for (File file : files) {
addPlugin(file.getName(), file);
}
}
}
@Override
public void onApplicationEvent(ContextClosedEvent event) {
Collection<List<PluginItemWrap>> values = PLUGIN_MAP.values();
for (List<PluginItemWrap> value : values) {
for (PluginItemWrap pluginItemWrap : value) {
IPlugin plugin = pluginItemWrap.getPlugin();
IoUtil.close(plugin);
}
}
}
6. 实现类
我们以DefaultDbH2PluginImpl
插件实现分析插件的视线 ,它实现了IPlugin
接口中定义的方法。
@PluginConfig(name = "db-h2")
public class DefaultDbH2PluginImpl implements IDefaultPlugin {
@Override
public Object execute(Object main, Map<String, Object> parameter) throws Exception {
String method = StrUtil.toString(main);
if (StrUtil.equals("backupSql", method)) {
String url = (String) parameter.get("url");
String user = (String) parameter.get("user");
String password = (String) parameter.get("pass");
String backupSqlPath = (String) parameter.get("backupSqlPath");
List<String> tableNameList = (List<String>) parameter.get("tableNameList");
this.backupSql(url, user, password, backupSqlPath, tableNameList);
} else if (StrUtil.equals("restoreBackupSql", method)) {
String backupSqlPath = (String) parameter.get("backupSqlPath");
DataSource dataSource = (DataSource) parameter.get("dataSource");
if (dataSource == null) {
// 加载数据源
dataSource = DSFactory.get();
}
this.restoreBackupSql(backupSqlPath, dataSource);
} else if (StrUtil.equals("recoverToSql", method)) {
File dbPath = (File) parameter.get("dbPath");
String dbName = (String) parameter.get("dbName");
File recoverBackup = (File) parameter.get("recoverBackup");
return this.recover(dbPath, dbName, recoverBackup);
} else {
throw new IllegalArgumentException("不支持的类型");
}
return "done";
}
/**
* 恢复
*
* @param dbPath 数据库路径
* @param dbName 数据库名
* @param recoverBackup 恢复到哪个路径
* @return 返回恢复到 sql 文件
* @throws SQLException sql
*/
private File recover(File dbPath, String dbName, File recoverBackup) throws SQLException {
String dbLocalPath = FileUtil.getAbsolutePath(dbPath);
ArrayList<String> list = FileLister.getDatabaseFiles(dbLocalPath, dbName, true);
if (CollUtil.isEmpty(list)) {
return null;
}
FileUtil.mkdir(recoverBackup);
// 备份数据
for (String s : list) {
FileUtil.move(FileUtil.file(s), recoverBackup, true);
}
String absolutePath = FileUtil.getAbsolutePath(recoverBackup);
Console.log("h2 db recover backup path,{}", absolutePath);
// 恢复数据
Recover recover = new Recover();
recover.runTool("-dir", absolutePath, "-db", dbName);
return FileUtil.file(recoverBackup, dbName + ".h2.sql");
}
/**
* 备份 SQL
*
* @param url jdbc url
* @param user user
* @param password password
* @param backupSqlPath backup SQL file path, absolute path
* @param tableNameList backup table name list, if need backup all table, use null
*/
private void backupSql(String url, String user, String password,
String backupSqlPath, List<String> tableNameList) throws SQLException {
// 备份 SQL
String sql = StrUtil.format("SCRIPT DROP to '{}'", backupSqlPath);
// 判断是否部分部分表
if (!CollectionUtils.isEmpty(tableNameList)) {
String tableNames = StrUtil.join(StrUtil.COMMA, tableNameList.toArray());
sql = StrUtil.format("{} TABLE {}", sql, tableNames);
}
DefaultSystemLog.getLog().debug("backup SQL is: {}", sql);
// 执行 SQL 备份脚本
Shell shell = new Shell();
/*
url 表示 h2 数据库的 jdbc url
* user 表示登录的用户名
* password 表示登录密码
* driver 是 jdbc 驱动
* sql 是备份的 sql 语句
* - 案例:script drop to ${fileName1} table ${tableName1},${tableName2}...
* - script drop to 表示备份数据库,drop 表示建表之前会先删除表
* - ${fileName1} 表示备份之后的文件名
* - table 表示需要备份的表名称,后面跟多个表名,用英文逗号分割
*/
String[] params = new String[]{
"-url", url,
"-user", user,
"-password", password,
"-driver", "org.h2.Driver",
"-sql", sql
};
try (FastByteArrayOutputStream arrayOutputStream = new FastByteArrayOutputStream()) {
try (PrintStream printStream = new PrintStream(arrayOutputStream)) {
shell.setOut(printStream);
shell.runTool(params);
}
}
}
/**
* 还原备份 SQL
*
* @param backupSqlPath backup SQL file path, absolute path
* @throws SQLException SQLException
* @throws IOException FileNotFoundException
*/
private void restoreBackupSql(String backupSqlPath, DataSource dataSource) throws SQLException, IOException {
Assert.notNull(dataSource, "Restore Backup sql error...H2 DataSource not null");
try (Connection connection = dataSource.getConnection()) {
// 读取数据库备份文件,执行还原
File backupSqlFile = FileUtil.file(backupSqlPath);
try (FileReader fileReader = new FileReader(backupSqlFile)) {
RunScript.execute(connection, fileReader);
}
}
}
}
小结
在Jpom项目中,IPlugin
接口是插件系统的核心接口,它定义了插件需要实现的基本方法。 在插件工厂类PluginFactory
中,通过扫描类路径加载插件并进行初始化:
IPlugin
接口及其实现为Jpom的插件系统提供了灵活的扩展能力。通过定义通用的执行方法和结果转换方法,IPlugin
接口简化了插件的开发和集成过程。而插件工厂类PluginFactory
则通过扫描和加载机制,实现了插件的自动发现和初始化,使得插件系统具有高度的可扩展性和灵活性。
通过对插件接口IPlugin、插件工厂PluginFactory、插件项包装类PluginItemWrap以及插件的加载和资源释放机制的分析,可以清晰地了解整个工程的插件机制。这为后续的插件开发和管理提供了坚实的基础。
附PluginFactory
@Slf4j
public class PluginFactory implements ApplicationContextInitializer<ConfigurableApplicationContext>, ApplicationListener<ContextClosedEvent> {
// private static final List<FeatureCallback> FEATURE_CALLBACKS = new ArrayList<>();
private static final Map<String, List<PluginItemWrap>> PLUGIN_MAP = new ConcurrentHashMap<>();
// /**
// * 添加回调事件
// *
// * @param featureCallback 回调
// */
// public static void addFeatureCallback(FeatureCallback featureCallback) {
// FEATURE_CALLBACKS.add(featureCallback);
// }
//
// public static List<FeatureCallback> getFeatureCallbacks() {
// return FEATURE_CALLBACKS;
// }
/**
* 获取插件端
*
* @param name 插件名
* @return 插件对象
*/
public static IPlugin getPlugin(String name) {
List<PluginItemWrap> pluginItemWraps = PLUGIN_MAP.get(name);
PluginItemWrap first = CollUtil.getFirst(pluginItemWraps);
Assert.notNull(first, "对应找到对应到插件:" + name);
return first.getPlugin();
}
/**
* 判断是否包含某个插件
*
* @param name 插件名
* @return true 包含
*/
public static boolean contains(String name) {
return PLUGIN_MAP.containsKey(name);
}
/**
* 插件数量
*
* @return 当前加载的插件数量
*/
public static int size() {
return PLUGIN_MAP.size();
}
/**
* 正式环境添加依赖
*/
private static void init() {
File runPath = JpomManifest.getRunPath().getParentFile();
File plugin = FileUtil.file(runPath, "plugin");
if (!plugin.exists() || plugin.isFile()) {
return;
}
// 加载二级插件包
File[] dirFiles = plugin.listFiles(File::isDirectory);
if (dirFiles != null) {
for (File file : dirFiles) {
File lib = FileUtil.file(file, "lib");
if (!lib.exists() || lib.isFile()) {
continue;
}
File[] listFiles = lib.listFiles((dir, name) -> StrUtil.endWith(name, FileUtil.JAR_FILE_EXT, true));
if (listFiles == null || listFiles.length <= 0) {
continue;
}
addPlugin(file.getName(), lib);
}
}
// 加载一级独立插件端包
File[] files = plugin.listFiles(pathname -> FileUtil.isFile(pathname) && FileUtil.JAR_FILE_EXT.equalsIgnoreCase(FileUtil.extName(pathname)));
if (files != null) {
for (File file : files) {
addPlugin(file.getName(), file);
}
}
}
private static void addPlugin(String pluginName, File file) {
DefaultSystemLog.getLog().info("加载:{} 插件", pluginName);
ClassLoader contextClassLoader = ClassLoaderUtil.getClassLoader();
JarClassLoader.loadJar((URLClassLoader) contextClassLoader, file);
}
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
init();
// 扫描插件 实现
Set<Class<?>> classes = ClassUtil.scanPackage("io.jpom", IPlugin.class::isAssignableFrom);
List<PluginItemWrap> pluginItemWraps = classes
.stream()
.filter(aClass -> ClassUtil.isNormalClass(aClass) && aClass.isAnnotationPresent(PluginConfig.class))
.map(aClass -> new PluginItemWrap((Class<? extends IPlugin>) aClass))
.filter(pluginItemWrap -> {
if (StrUtil.isEmpty(pluginItemWrap.getName())) {
DefaultSystemLog.getLog().warn("plugin config name error:{}", pluginItemWrap.getClassName());
return false;
}
return true;
})
.collect(Collectors.toList());
//
Map<String, List<PluginItemWrap>> pluginMap = CollStreamUtil.groupByKey(pluginItemWraps, PluginItemWrap::getName);
pluginMap.forEach((key, value) -> {
// 排序
value.sort((o1, o2) -> Comparator.comparingInt((ToIntFunction<PluginItemWrap>) value1 -> {
Order order = value1.getClassName().getAnnotation(Order.class);
if (order == null) {
return 0;
}
return order.value();
}).compare(o1, o2));
PLUGIN_MAP.put(key, value);
});
log.debug("load plugin count:{}", pluginMap.keySet().size());
}
@Override
public void onApplicationEvent(ContextClosedEvent event) {
Collection<List<PluginItemWrap>> values = PLUGIN_MAP.values();
for (List<PluginItemWrap> value : values) {
for (PluginItemWrap pluginItemWrap : value) {
IPlugin plugin = pluginItemWrap.getPlugin();
IoUtil.close(plugin);
}
}
}
}