使用 Java 写一个简易插件框架

47

很久之前折腾的插件框架, 下面介绍一个简易实现

这个 Demo 使用 JDK21, 用 Java Platform Module System 和 Intellj IDEA Build System 构建, 如果用 IDEA以外的工具打开需要自行配置模块 (不用工具的话可以自己打命令)

这个 Demo 的位置在: https://github.com/Erzbir/plugin-demo

类加载原理

在启动时先启动 JVM, 之后会由 Bootstrap ClassLoader 来加载 main 启动类

在使用前会经历三个步骤: 加载, 链接, 初始化

类的生命周期

这是开始顺序而不是进行顺序, 并不是一步一步往下的. 加载, 验证, 准备, 初始化和卸载的开始的顺序是确定的, 但是解析则不确定, 在某些情况下在初始化阶段之后再开始, 这是为了支持 Java 语言的运行时绑定特性

对于 "初始化" 阶段, 有六种情况必须立即对类进行初始化 (加载, 验证, 准备需在此前开始)

Loading 阶段, JVM 完成三件事:

  1. 通过一个类的全限定名来获取此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的 java.lang.Class 对象, 作为方法区这个类的各种数据访问入口

第一条说获取类的字节流, 也就是获取 .class 格式数据的字节码, Java 虚拟机规范 中只说了"通过全限定类名来获取字节流", 但并未说如何获取, 我们可以从我们想到的任意一个位置比如文件系统的某个目录下, 某个压缩包中或者从网络上

加载一个类的大致过程是, 先获取了字节流, 然后通过类加载器将其转换为 JVM 可识别的类对象

在 JVM 的角度上来说, 实际上只存在两种类加载器: Bootstrap ClassLoader (在 JVM 中) 和虚拟机外的所有继承自 java.lang.ClassLoader 的类加载器

提供给开发者的就是这个 java.lang.ClassLoader, 其中有几个 defineClass 方法, 通过 defineClass 方法可以将一个类的字节流加载到 JVM 中. 这个 defineClass 方法是一个 native 方法, 由 JVM 来执行

ClassLoader 中定义类的方法:

static native Class<?> defineClass1(ClassLoader loader, 
                                    String name, 
                                    byte[] b, 
                                    int off, 
                                    int len, 
                                    ProtectionDomain pd, 
                                    String source);

static native Class<?> defineClass2(ClassLoader loader, 
                                    String name, 
                                    java.nio.ByteBuffer b, 
                                    int off, 
                                    int len, 
                                    ProtectionDomain pd, 
                                    String source);
                                    
static native Class<?> defineClass0(ClassLoader loader,
                                    Class<?> lookup,
                                    String name,
                                    byte[] b, int off, int len,
                                    ProtectionDomain pd,
                                    boolean initialize,
                                    int flags,
                                    Object classData);

双亲委派

在开发人员的角度来说, 存在三层类加载器

  • Bootstrap Class Loader: 加载 $JAVA_HOME/lib 目录, 或者 -Xbootclasspath 参数指定的路径
  • Platform Class Loader: 用于加载 Java 平台模块中非核心但对应用可见的类库, 比如 java.sql, java.logging 等模块的类
  • Application Class Loader: 这个也叫做系统类加载器, 负责加载用户 classpath 的类库

双亲委派就是子类加载器先委派给父类加载器, 当父类加载器无法加载时, 再由自己尝试加载

这样的设计避免了重复加载, 也防止类被篡改 (不同加载器加载的类即使名字一样也不是同一个)

BootstrapClassLoader
    ↑
PlatformClassLoader
    ↑
ApplicationClassLoader(SystemClassLoader)
    ↑
User Custom Class ClassLoader

双亲委派模型实现:

public class CustomClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 检查有没有被这个类加载器加载过
        Class<?> c = findLoadedClass(name);

        if (c == null) {
            try {
                // 委托给父类加载器加载
                ClassLoader parent = getParent();
                if (parent != null) {
                    c = parent.loadClass(name);
                } else {
                    c = findSystemClass(name);
                }
            } catch (ClassNotFoundException ignore) {
                // 父类加载器没有加载到这个类
            }
            if (c == null) {
                // 通过自身加载
                c = findClass(name);
            }
        }
        if (c == null) {
            // 最终也没有加载到这个类, 抛错
            throw new ClassNotFoundException(name);
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

但如果是用于加载插件, 我们就不能严格按照这种机制, 需要打破

比如我们可能会这么写:

    @Override
    public Class<?> loadClass(String className) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(className)) {
            // 判断是不是内置的类, 交给 AppClassLoader 加载
            if (className.startsWith(JAVA_PACKAGE_PREFIX) || className.startsWith(JAVAX_PACKAGE_PREFIX)) {
                return findSystemClass(className);
            }
            Class<?> loadedClass = findLoadedClass(className);
            if (loadedClass != null) {
                return loadedClass;
            }
            try {
                Class<?> c = findClass(className);

                if (c != null) {
                    return c;
                }
            } catch (ClassNotFoundException ignored) {

            }
        }
        throw new ClassNotFoundException(className);
    }

上面这样的写法就没有先交给父类加载器去加载, 因为如果是当作插件加载进来, 类是要隔离开的, 每次加载插件都是用的不同的类加载器

这里说的隔离的意思就是, 比如有两个插件都有一个 com.test.A, 如果按照双亲委派的模型, 就只能存在一个 com.test.A 但实际上可能两个类文件不同

插件加载原理

我们利用 Java 运行时可以加载一个类的特性, 就可以将字节码文件当作插件在运行时动态加载进来

一个 Jar 包, 一个 ZIP 包, 或是一堆 class 文件, 都可以成为一个插件, 但不管怎样都必需要有一个插件主类来让们加载插件实例以及执行插件的逻辑

为了简单方便, 自定义类加载器可以直接继承自 URLClassLoader, 我们只需要调用 addURLaddFile 就可以直接加载所有 class 了

这里需要注意一个问题, 继承自 URLClassLoader, 在调用 loadClass 中调用 findClass 时如果是一个新的类, 会尝试去 defineClass, 这时候会在 defineClass 中会解析这个类引入了哪些类, 然后递归调用 loadClass.所以在类加载时我们需要让非插件本身的类用系统类加载器或父类加载器加载, 比如 java.lang.System 以及框架的依赖等

比如一个插件打包时可能包含插件框架的依赖, 这部分应该交由系统类加载器加载, 而不是由加载插件插件类的类加载器重复加载, 因为这些类必须是唯一的; 在不包含插件框架依赖的情况下如果尝试自己加载, 我们这个自定义类加载器肯定是加载不到这个类的, 所以不管怎样我们自定义的类加载器不能加载这些类

我们通过类加载器加载插件主类, 并将这个插件的所有 class 插在进来, 就可以实例化这个插件主类, 调用生命周期回调就可以开始执行插件的自定义逻辑

在每次加载插件的时候都使用一个新的类加载器, 这样可以将插件隔离开, 并且也可以在卸载插件时卸载这个插件的所有类 (从 JVM 中将类卸载是 GC 的工作, 我们需要确保一个类的所有对象都没有强引用, 并且加载他的类加载器也没有强引用)

整体设计

分为两个模块 demo.plugin.apidemo.plugin.core, api 是接口层, core 是实现层, 这样可以方便切换实现

接口层

demo.plugin.api

PluginMnager

提供一个 PluginManager 接口, 以供用户可以操作插件的生命周期, 比如提供一些 loadPlugin, enablePlugin, unloadPlugin 方法, 以插件 ID 来操作对应插件. 实现层必须提供一个 PluginManager 的实现

Plugin

提供一个 Plugin 接口, 其中包含插件的生命周期回调, 并且提供一个抽象类实现 Plugin 接口, 让用户可以继承这个插件抽象类以便加载和标识. 这个接口以及抽象类都不提供生命周期的实现

一个插件必须要有描述, 其中包含 id, name, author, version, desc, idversion 是必填字段

直接将插件描述填到构造器中实际上是偷了一个懒, 这样做不需要在实现层去解析文件了, 但坏处就是必须成功加载这个插件的主类后才能验证这个插件的描述信息, 很多操作也没办法实现, 所以最好的方式是有一个插件描述文件

Exception

设计三个异常, 均继承自 PluginRuntimeException

  • PluginAlreadyLoadedException - 插件 ID 重复时抛出
  • PluginIllegalException - 加载插件失败或插件不是一个合法插件时抛出
  • PluginNotFoundException - 找不到插件的时候抛出

实现层

demo.plugin.core

规定了开放给用户的 API 后, 实现可以有很多种

这里主要说明从哪里加载, 可以加载什么, 以及怎么加载

加载位置

这里作为演示, 默认从运行目录下的 ./plugins 加载

可加载的插件形式

插件的形式实际上可以有很多种:

  • jar 包或 zip 包 (这两种文件的魔数相同)
  • class 文件
  • ...

这里我们就只提供一种插件形式的 PluginLoader, 加载 .jar 插件. 加载 .class 或其他的原理一样

加载插件通过插件加载器 PluginLoader 来实现, 这个 PluginLoader 通过一个自定义的类加载器, 来从文件中加载类

这个加载 .jarPluginLoader 其中用于加载类的类加载器是一个 URLClassLoader 的子类

插件约定

就像 JVM 加载有 main 方法的主类一样, 插件也需要一个主类, 必须得有一些约定

  • 一个插件主类必须继承自抽象插件类, 并且在类中提供一个 static INSTANCE 字段用于获取这个插件实例 (这样是把如何实例化交给用户, 框架不负责插件主类实例化)
  • 必须有一个唯一的 ID 用于标识插件
  • 必须在某个地方声明插件的主类, 可以用 Java SPI 机制的方式来声明插件主类

生命周期实现

生命周期由实现层来实现, 不开放给用户的原因是如果用户修改了内部变量会出现不可预测的行为, 所以这里以包装的形式来实现生命周期

详细实现

接口层

import com.demo.plugin.PluginManager;

/**
 * @author Erzbir
 * @since 1.0.0
 */
module demo.plugin.api {
    uses PluginManager;
    exports com.demo.plugin;
    exports com.demo.plugin.exception;
}

插件接口

Plugin.java

package com.demo.plugin;

/**
 * @author Erzbir
 * @since 1.0.0
 */
public interface Plugin {
    void onEnable();

    void onDisable();

    void onLoad();

    void onUnLoad();
}

提供了四种生命周期回调, 在启用插件后调用 onEnable, 禁用插件后调用 onDisable, 在加载插件成功后调用 onLoad, 在卸载插件后调用 onUnload

抽象插件类

JavaPlugin.java

package com.demo.plugin;

/**
 * @author Erzbir
 * @since 1.0.0
 */
public abstract class JavaPlugin implements Plugin {
    public final PluginDescription description;

    public JavaPlugin(final PluginDescription description) {
        this.description = description;
    }

    @Override
    public String toString() {
        return "JavaPlugin{" +
                "description=" + description +
                '}';
    }
}

这样当继承 JavaPlugin 实现插件时就可以强制填入插件描述

PluginDescription.class:

package com.demo.plugin;

/**
 * @author Erzbir
 * @since 1.0.0
 */
public record PluginDescription(String id, String name, String desc, String author, String version) {
    public PluginDescription(String id, String version) {
        this(id, "", "", "", version);
    }

    @Override
    public String toString() {
        return "PluginDescription{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", desc='" + desc + '\'' +
                ", author='" + author + '\'' +
                ", version='" + version + '\'' +
                '}';
    }
}

这个 id 就当作是插件的唯一标识, 用于 PluginManager 根据这个 id 来操作插件

插件管理器接口

PluginManager.java

package com.demo.plugin;

import java.nio.file.Path;

/**
 * @author Erzbir
 * @since 1.0.0
 */
public interface PluginManager {
    void loadPlugins();

    void loadPlugin(Path pluginPath);

    void enablePlugin(String pluginId);

    void enablePlugins();

    void disablePlugin(String pluginId);

    void disablePlugins();

    void unloadPlugins();

    void unloadPlugin(String pluginId);
}

这个接口定义了插件管理器能做什么

插件管理器实例加载器

我们需要提供给用户 PluginManager 的实现, 在 API 层通过 SPI 的方式来加载一个实现

PluginManagerProvider.java

package com.demo.plugin;


import java.util.*;

/**
 * @author Erzbir
 * @since 1.0.0
 */
public class PluginManagerProvider {
    public static final PluginManagerProvider INSTANCE = new PluginManagerProvider();
    private static final Map<String, PluginManager> PLUGIN_MANAGERS = new HashMap<>();

    static {
        ServiceLoader<PluginManager> serviceLoader = ServiceLoader.load(PluginManager.class);
        for (PluginManager pluginManager : serviceLoader) {
            PLUGIN_MANAGERS.put(pluginManager.getClass().getName(), pluginManager);
        }
    }

    public PluginManager getInstance(String key) {
        return PLUGIN_MANAGERS.get(key);
    }

    public PluginManager getInstance() {
        for (PluginManager pluginManager : PLUGIN_MANAGERS.values()) {
            return pluginManager;
        }
        throw new IllegalStateException("No PluginManager available");
    }

    public List<PluginManager> getInstances() {
        return new ArrayList<>(PLUGIN_MANAGERS.values());
    }
}

这里就是单纯用于加载实现用的, 因为在使用的时候, 实现层可能会被当成是 runtime 的依赖, 无法直接访问实现层

实现层

import com.demo.plugin.PluginManager;

/**
 * @author Erzbir
 * @since 1.0.0
 */
module demo.plugin.core {
    requires demo.plugin.api;
    provides PluginManager with com.demo.plugin.internal.JavaPluginManager;
}

这里我们一步一步来实现一个 PluginManager, 名字叫 JavaPluginManager 用于管理继承了 JavaPlugin 的插件

loadPlugin

首先我们来实现最核心的加载插件的方法

需要加载插件, 按照我们的设计, 需要用一个 PluginLoader 去加载

PluginLoader.java

package com.demo.plugin.internal.loader;

import com.demo.plugin.Plugin;

import java.nio.file.Path;

/**
 * @author Erzbir
 * @since 1.0.0
 */
public interface PluginLoader {
    Plugin loadPlugin(Path pluginPath);

    ClassLoader getClassLoader();
}

我们是通过 Jar 包来加到插件的, 所以我们实现一个加载 Jar 包的插件加载器 FatJarPluginLoader. 但在这之前, 这个插件加载器需要用到一个自定义的类加载器, 我们先实现一个 PluginClassLoader

我们主要是重写 loadClass 方法

@Override
public Class<?> loadClass(String className) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(className)) {
        // 先查找有没有这个类, 加载过的类就直接返回
        Class<?> loadedClass = findLoadedClass(className);
        if (loadedClass != null) {
            return loadedClass;
        }

        try {
            // 尝试加载这个类
            Class<?> c = findClass(className);
            if (c != null) {
                return c;
            }
        } catch (ClassNotFoundException ignored) {}
    }
    throw new ClassNotFoundException(className);
}

这个 loadClass 已经实现了加载类的功能, 但存在一个严重的问题, 在上面我们有提供到过 URLClassLoader 会在 defineClass 的时候递归加载引入的类, 所以我们必须将这部分类交给系统类加载器

我们需要交给系统类加载器的是:

  • Java 自带的类
  • 插件框架的依赖

我们通过包名来匹配:

private static final String JAVA_PACKAGE_PREFIX = "java.";
private static final String JAVAX_PACKAGE_PREFIX = "javax.";
// 这个是插件框架的依赖
private static final String PLUGIN_PACKAGE_PREFIX = "com.demo.plugin.";

最后我们得到的代码:

PluginClassLoader.java

package com.demo.plugin.internal.loader;

import com.demo.plugin.exception.PluginRuntimeException;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;

/**
 * @author Erzbir
 * @since 1.0.0
 */
public class PluginClassLoader extends URLClassLoader {
    private static final String JAVA_PACKAGE_PREFIX = "java.";
    private static final String JAVAX_PACKAGE_PREFIX = "javax.";
    private static final String PLUGIN_PACKAGE_PREFIX = "com.demo.plugin.";

    public PluginClassLoader() {
        super(new URL[0]);
    }

    @Override
    public void addURL(URL url) {
        super.addURL(url);
    }

    public void addFile(File file) {
        try {
            addURL(file.getCanonicalFile().toURI().toURL());
        } catch (IOException e) {
            throw new PluginRuntimeException(e);
        }
    }

    @Override
    public Class<?> loadClass(String className) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(className)) {
            // 如果是 Java 自带的系统类就交给 SystemClassLoader 来加载
            if (className.startsWith(JAVA_PACKAGE_PREFIX) || className.startsWith(JAVAX_PACKAGE_PREFIX)) {
                return findSystemClass(className);
            }

            // 如果是插件框架的依赖, 交给父类加载器
            if (className.startsWith(PLUGIN_PACKAGE_PREFIX)) {
                ClassLoader parent = getParent();
                if (parent != null) {
                    return parent.loadClass(className);
                }
            }
            
            // 先查找有没有这个类, 加载过的类就直接返回
            Class<?> loadedClass = findLoadedClass(className);
            if (loadedClass != null) {
                return loadedClass;
            }
            
            try {
                Class<?> c = findClass(className);

                if (c != null) {
                    return c;
                }
            } catch (ClassNotFoundException ignored) { }
        }
        throw new ClassNotFoundException(className);
    }
}

实现了类加载器后, 我们就可以开始实现插件加载器了

我们的插件约定是使用 SPI 的方式来标识插件主类, 所以我们需要读取 SPI 文件, 其在 Jar 包中的相对路径是:

protected final static String SERVICE_PATH = "META-INF/services/com.demo.plugin.Plugin";

FatJarPluginLoader.java

package com.demo.plugin.internal.loader;


import com.demo.plugin.Plugin;
import com.demo.plugin.exception.PluginIllegalException;
import com.demo.plugin.internal.util.FileTypeDetector;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.nio.file.Path;
import java.util.jar.JarFile;

/**
 * @author Erzbir
 * @since 1.0.0
 */
public class FatJarPluginLoader implements PluginLoader {
    protected final PluginClassLoader classLoader;
    protected final static String SERVICE_PATH = "META-INF/services/com.demo.plugin.Plugin";

    public FatJarPluginLoader() {
        this.classLoader = new PluginClassLoader();
    }

    @Override
    public Plugin loadPlugin(Path pluginPath) {
        // 判断是否是一个 jar 文件, 通过文件头和后缀判断
        if (!FileTypeDetector.isJarFile(pluginPath)) {
            throw new PluginIllegalException("The file " + pluginPath.toAbsolutePath() + " is not a jar file");
        }
        File file = pluginPath.toFile();
        // 加载这个插件的所有字节码文件
        loadPluginClass(file);
        // 读取插件的主类名
        String pluginClassName = readPluginMainClassName(file);
        // 获取插件的主类, 在这个方法里真正开始加载类
        Class<?> pluginClass = getPluginClass(pluginClassName);
        // 返回插件实例
        return resolvePluginClass(pluginClass);
    }

    @Override
    public ClassLoader getClassLoader() {
        return classLoader;
    }

    private void loadPluginClass(File file) {
        classLoader.addFile(file);
    }

    private Class<?> getPluginClass(String className) {
        try {
            return Class.forName(className, false, classLoader);
        } catch (ClassNotFoundException e) {
            throw new PluginIllegalException(e);
        }
    }

    private Plugin resolvePluginClass(Class<?> pluginClass) {
        if (Plugin.class.isAssignableFrom(pluginClass)) {
            try {
                // 由于使用了模块化, 并且动态加载的 jar 是一个 unnamed module 所以不能使用 MethodHandle 
                Field instance = pluginClass.getDeclaredField("INSTANCE");
                instance.setAccessible(true);
                return (Plugin) instance.get(null);
            } catch (Throwable e) {
                throw new PluginIllegalException(String.format("Failed to find INSTANCE in plugin: %s", pluginClass.getName()), e);
            }
        }
        throw new PluginIllegalException("Should never get here");
    }

    private String readPluginMainClassName(File file) {
        String line;
        try (JarFile jarFile = new JarFile(file); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(jarFile.getInputStream(jarFile.getEntry(SERVICE_PATH))))) {
            line = bufferedReader.readLine();
        } catch (IOException e) {
            throw new PluginIllegalException("Failed to read service file", e);
        }
        return line;
    }
}

这样我们就实现了一个加载 .jar 插件的插件加载器了, 接下来实现 PluginManagerloadPlugin 方法

public void loadPlugin(Path pluginPath) {
    if (!pluginPath.isAbsolute()) {
        pluginPath = Path.of(PLUGIN_DIR).resolve(pluginPath);
    }

    PluginLoader pluginLoader = new FatJarPluginLoader();

    Plugin plugin = pluginLoader.loadPlugin(pluginPath);

    // 判断这个插件是否继承了 JavaPlugin
    if (!isJavaPlugin(plugin)) {
        throw new PluginIllegalException(String.format("Plugin [%s] Not a JavaPlugin", pluginPath.getFileName()));
    }

    PluginDescription description = ((JavaPlugin) plugin).description;

    // 插件 id 重复
    if (plugins.containsKey(description.id())) {
        throw new PluginAlreadyLoadedException(String.format("Plugin [%s] already loaded with id [%s]", pluginPath.getFileName(), description.id()));
    }

    // 包装成一个 PluginWrapper
    PluginWrapper pluginWrapper = new PluginWrapper(plugin, pluginLoader.getClassLoader(), pluginPath, description);
    // 注册到内部的容器中
    registerPlugin(pluginWrapper, pluginLoader.getClassLoader());
}

这个 PluginWrapper 是一个包装器, 实现了插件的生命周期, 也保存了一些插件的上下文

package com.demo.plugin.internal;

import com.demo.plugin.Plugin;
import com.demo.plugin.PluginDescription;

import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @author Erzbir
 * @since 1.0.0
 */
public class PluginWrapper implements Plugin {
    public final PluginDescription description;
    public final ClassLoader pluginContext;
    public final Path pluginPath;
    private final Plugin delegate;
    private final AtomicBoolean enable = new AtomicBoolean(false);

    public PluginWrapper(Plugin plugin, ClassLoader classLoader, Path pluginPath, PluginDescription description) {
        this.delegate = plugin;
        this.pluginContext = classLoader;
        this.pluginPath = pluginPath;
        this.description = description;
    }

    @Override
    public void onEnable() {
        delegate.onEnable();
    }

    @Override
    public void onDisable() {
        delegate.onDisable();
    }

    @Override
    public void onLoad() {
        delegate.onLoad();
    }

    @Override
    public void onUnLoad() {
        delegate.onUnLoad();
    }

    public boolean isEnable() {
        return enable.get();
    }

    public void enable() {
        enable.set(true);
    }

    public void disable() {
        enable.set(false);
    }
}

这个实际上可以被叫做代理模式, 如果不了解可以去看看设计模式

这样我们就完成了插件加载, 基本执行流程是这样: 插件管理器加载 .jar 插件 -> 插件加载器从这个 Jar 包获取插件主类实例 -> 插件管理器将这个实例包装成 PluginWrapper -> 将包装的插件和加载这个插件的类加载器注册到内部的容器中

unloadPlugin

在实现了 loadPlugin 后, 还有一个比较重要的实现就是 unloadPlugin, 这里我们要做的不仅仅是把这个插件实例存内部容器移除, 还要让 JVM 卸载掉加载这个插件时所加载的类

我们通过插件的 id 来卸载

@Override
public void unloadPlugin(String pluginId) {
	PluginWrapper plugin = plugins.remove(pluginId);
	if (plugin == null) {
	    throw new PluginNotFoundException(String.format("Plugin [%s] Not found", pluginId));
	}
	plugin.disable();
	plugin.onUnLoad();
	// 移除类加载器
	destroyPlugin(plugin);
	// 帮助 GC 回收这个 plugin, 如果不置空会在未来更远的时间才能被卸载
	plugin = null;
    System.gc();
}

private void destroyPlugin(PluginWrapper plugin) {
    try {
        if (pluginClassLoaders.get(plugin.description.id()) instanceof Closeable closeable) {
            closeable.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    pluginClassLoaders.remove(plugin.description.id());
}

这里要注意的是, 方法中的临时变量也会影响 GC 回收, 因为我们在 unloadPlugin 方法的最后使用 System.gc 建议 GC 现在开始回收, 如果这时候调用栈还没有销毁, 那么临时变量同样也是一个强引用, 所以 GC不会回收这些临时变量指向的对象

JavaPluginManager

主要就是那两个方法的实现, 这里给出完整的实现

完整的 JavaPluginManager:

JavaPluginManager.java

package com.demo.plugin.internal;

import com.demo.plugin.JavaPlugin;
import com.demo.plugin.Plugin;
import com.demo.plugin.PluginDescription;
import com.demo.plugin.PluginManager;
import com.demo.plugin.exception.PluginAlreadyLoadedException;
import com.demo.plugin.exception.PluginIllegalException;
import com.demo.plugin.exception.PluginNotFoundException;
import com.demo.plugin.exception.PluginRuntimeException;
import com.demo.plugin.internal.loader.FatJarPluginLoader;
import com.demo.plugin.internal.loader.PluginLoader;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author Erzbir
 * @since 1.0.0
 */
public class JavaPluginManager implements PluginManager {
    private final static String PLUGIN_DIR = "plugins";
    private final Map<String, PluginWrapper> plugins = new HashMap<>();
    private final Map<String, ClassLoader> pluginClassLoaders = new HashMap<>();

    @Override
    public void loadPlugins() {
        loadPlugins(PLUGIN_DIR);
    }

    private void loadPlugins(String pluginDir) {
        File[] plugins = new File(pluginDir).listFiles((dir, name) -> name.endsWith(".jar") || name.endsWith(".class"));
        if (plugins == null || plugins.length == 0) {
            throw new PluginRuntimeException("Plugin directory '" + pluginDir + "' not found");
        }
        for (File plugin : plugins) {
            loadPlugin(plugin.toPath().getFileName());
        }
    }

    public void loadPlugin(Path pluginPath) {
        if (!pluginPath.isAbsolute()) {
            pluginPath = Path.of(PLUGIN_DIR).resolve(pluginPath);
        }

        PluginLoader pluginLoader = new FatJarPluginLoader();

        Plugin plugin = pluginLoader.loadPlugin(pluginPath);

        // 判断这个插件是否继承了 JavaPlugin
        if (!isJavaPlugin(plugin)) {
            throw new PluginIllegalException(String.format("Plugin [%s] Not a JavaPlugin", pluginPath.getFileName()));
        }

        PluginDescription description = ((JavaPlugin) plugin).description;

        // 插件 id 重复
        if (plugins.containsKey(description.id())) {
            throw new PluginAlreadyLoadedException(String.format("Plugin [%s] already loaded with id [%s]", pluginPath.getFileName(), description.id()));
        }

        // 包装成一个 PluginWrapper
        PluginWrapper pluginWrapper = new PluginWrapper(plugin, pluginLoader.getClassLoader(), pluginPath, description);
        // 注册到内部的容器中
        registerPlugin(pluginWrapper, pluginLoader.getClassLoader());
    }

    private void registerPlugin(PluginWrapper plugin, ClassLoader classLoader) {
        String id = plugin.description.id();
        plugins.put(id, plugin);
        // 保存加载这个插件使用的类加载器
        pluginClassLoaders.put(id, classLoader);
        plugin.onLoad();
    }

    private boolean isJavaPlugin(Plugin plugin) {
        return (plugin instanceof JavaPlugin);
    }

    @Override
    public void enablePlugin(String pluginId) {
        PluginWrapper plugin = plugins.get(pluginId);
        if (plugin == null) {
            return;
        }
        if (plugin.isEnable()) {
            return;
        }
        plugin.enable();
        plugin.onEnable();
    }

    @Override
    public void enablePlugins() {
        for (String pluginId : plugins.keySet()) {
            enablePlugin(pluginId);
        }
    }

    @Override
    public void disablePlugin(String pluginId) {
        PluginWrapper plugin = plugins.get(pluginId);
        if (plugin == null) {
            throw new PluginNotFoundException(String.format("Plugin [%s] Not found", pluginId));
        }
        if (!plugin.isEnable()) {
            return;
        }
        plugin.disable();
        plugin.onDisable();
    }

    @Override
    public void disablePlugins() {
        for (String pluginId : plugins.keySet()) {
            disablePlugin(pluginId);
        }
    }

    @Override
    public void unloadPlugins() {
        List<String> keys = new ArrayList<>(plugins.keySet());
        for (String pluginId : keys) {
            unloadPlugin(pluginId);
        }
    }

    @Override
    public void unloadPlugin(String pluginId) {
        PluginWrapper plugin = plugins.remove(pluginId);
        if (plugin == null) {
            throw new PluginNotFoundException(String.format("Plugin [%s] Not found", pluginId));
        }
        plugin.disable();
        plugin.onUnLoad();
        // 移除类加载器
        destroyPlugin(plugin);
        // 帮助 GC 回收这个 plugin, 如果不置空会在未来更远的时间才能被卸载
        plugin = null;
        System.gc();
    }

    private void destroyPlugin(PluginWrapper plugin) {
        try {
            if (pluginClassLoaders.get(plugin.description.id()) instanceof Closeable closeable) {
                closeable.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        pluginClassLoaders.remove(plugin.description.id());
    }
}

使用示例

首先我们需要打包一个插件, 新建一个项目或者模块 demo.plugin.test, 添加对 demo.plugin.api 模块的 provided 依赖 (这是对于构建工具来说的, 对于模块系统来说就是 requires static)

TestPlugin.java

package com.test;

import com.demo.plugin.JavaPlugin;
import com.demo.plugin.PluginDescription;

/**
 * @author Erzbir
 * @since 1.0.0
 */
public class TestPlugin extends JavaPlugin {
    public final static TestPlugin INSTANCE = new TestPlugin();

    public TestPlugin() {
        super(new PluginDescription("test", "TestPlugin", "Just a test", "Erzbir", "0.0.1"));
    }

    @Override
    public void onEnable() {
        System.out.printf("Plugin %s enabled\n", description.id());
    }

    @Override
    public void onDisable() {
        System.out.printf("Plugin %s disable\n", description.id());
    }

    @Override
    public void onLoad() {
        System.out.printf("Plugin %s loaded\n", description.id());
    }

    @Override
    public void onUnLoad() {
        System.out.printf("Plugin %s unloaded\n", description.id());
    }
}

然后我们添加 SPI 配置文件

文件位置: META-INF/services/com.demo.plugin.Plugin

内容:

com.test.TestPlugin

然后我们可以使用 IDEA 构建 Jar 包的功能, 将 demo.plugin.test 构建打包成一个 Jar

或者使用命令:

# 编译 demo.plugin.api 模块
javac -d out/module --module-source-path api/src -m demo.plugin.api

# 编译 demo.plugin.test 模块
javac -d out/module --module-source-path plugin-test/src --module-path out/module/demo.plugin.api -m demo.plugin.test

# 打包插件, 将输出的 test-plugin.jar 放到 plugins 文件夹
cp -r plugin-test/resources/* out/module/demo.plugin.test && jar --create --file plugins/test-plugin.jar -C out/module/demo.plugin.test .

打包好插件了之后, 我们新建一个 demo.plugin.usage 模块, 来尝试测试框架. 这个模块添加 demo.plugin.core 依赖 (runtime scope), 以及 demo.plugin.api 依赖 (compile scope)

这里不要在 JUnit 中运行, 会卸载不掉类

PluginManagerTest.java

package com.demo.usage;

import com.demo.plugin.PluginManager;
import com.demo.plugin.PluginManagerProvider;

/**
 * @author Erzbir
 * @since 1.0.0
 */
public class PluginManagerTest {
    public static void main(String[] args) {
        PluginManager pluginManager = PluginManagerProvider.INSTANCE.getInstance();
        pluginManager.loadPlugins();
        pluginManager.enablePlugins();
        pluginManager.unloadPlugins();
    }
}

使用 IDEA 可以直接运行, 如果没有 IDEA, 可以敲下面命令:

# 编译 demo.plugin.api 模块
javac -d out/module --module-source-path api/src -m demo.plugin.api

# 编译 demo.plugin.core 模块
javac -d out/module --module-source-path core/src -m demo.plugin.core --module-path out/module/demo.plugin.api

# 编译 demo.plugin.usage 模块
javac -d out/module --module-source-path usage/src -m demo.plugin.usage --module-path out/module/demo.plugin.core:out/module/demo.plugin.api

# 运行 PluginManagerTest:main
java -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -p out/module/demo.plugin.usage:out/module/demo.plugin.api:out/module/demo.plugin.core -m demo.plugin.usage/com.demo.usage.PluginManagerTest

可以看到成功运行并正确执行了插件中的打印逻辑

Screenshot2025-04-26at02.44.23

如果要观察 JVM 加载以及卸载类的行为, 加上 -verbose:class 即可

java -verbose:class -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -p out/module/demo.plugin.usage:out/module/demo.plugin.api:out/module/demo.plugin.core -m demo.plugin.usage/com.demo.usage.PluginManagerTest

这里的输出会非常长, 可以看到我们在卸载插件后, JVM 也将类卸载了

Screenshot2025-04-26at02.52.16