当前位置: 首页 > article >正文

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. 插件接口与实现分析
  2. 插件工厂初始化分析
  3. 插件项包装类解析
  4. 插件工厂方法解析
  5. 插件加载与资源释放机制

1. 插件接口与实现分析

在这里插入图片描述

插件机制的核心接口是IPluginIPlugin继承了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 {
    }
}

主要方法解析

  1. execute 方法

    • execute(Object main, Class<T> cls, Object... parameters):此方法接收一个对象、一个返回值类型和一组参数,执行插件方法后返回指定类型的结果。
    • execute(Object main, Map<String, Object> parameter, Class<T> cls):此方法接收一个对象、一个参数Map和一个返回值类型,执行插件方法后返回指定类型的结果。
  2. convertResult 方法

    • 该方法用于将执行结果转换为指定类型。它首先判断结果是否为简单类型,如果是则直接转换;否则将结果转换为JSON对象,再将JSON对象转换为指定类型。
  3. close 方法

    • 实现了AutoCloseable接口,用于在系统关闭时释放插件资源。默认实现为空方法。

2. 插件工厂初始化分析

插件工厂类PluginFactory负责插件的初始化和管理 ,实现了ApplicationContextInitializerApplicationListener接口,用于在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);
            }
        }
    }
}

在这里插入图片描述


http://www.kler.cn/a/428298.html

相关文章:

  • C 语言实现计算一年中指定日期是第几天 题】
  • 算法中的移动窗帘——C++滑动窗口算法详解
  • 自然语言处理——从原理、经典模型到应用
  • Kubectl 与 Helm 详解
  • 技术之翼,创作之心
  • 2025年美赛C题:奥运奖牌榜模型 解析及Python代码实现
  • 如何批量去除 PDF 中的特定文字?5种批量去除pdf中特定文字的方法。
  • web斗地主游戏实现指北
  • Windows环境中Python脚本开机自启动及其监控自启动
  • 机器学习—测量纯度
  • qt QCryptographicHash详解
  • SQL中限制一定数量行的实现
  • 企业网站管理系统(多语言)PHP+Uniapp
  • 实战:MyBatis适配多种数据库:MySQL、Oracle、PostGresql等
  • Javaweb 前端 ajax
  • linux什么命令通常用于查找所有与 Java 相关的进程
  • threejs相机辅助对象cameraHelper
  • 【linux】cgroup配置在机器OOM时选择进程原理讲解
  • 视频监控中使用usb摄像头(MJPG)代替OV5640
  • Shell 流程控制
  • python中什么叫做脚本
  • 前端下载文件后,文件损坏,无法打开?
  • 05 在 Linux 使用 AXI DMA
  • 【模电】【补充】稳幅电路分析
  • 集合ArrayList
  • 基于Spring Boot和Vue的人脸识别项目(源码)