You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何隔离通过ServiceLoader加载的Java插件?

基于ServiceLoader的插件隔离加载方案

要实现你提出的两个隔离要求,仅给ServiceLoader.load()传入自定义类加载器是不够的。你需要配合自定义类加载器的加载逻辑调整(打破双亲委派的默认顺序),同时为每个插件分配独立的类加载器实例,才能真正实现插件间、插件与应用间的类/资源隔离,以及依赖版本隔离。

核心思路

  • 每个插件使用独立的自定义类加载器,确保插件间的类加载器互相不可见
  • 自定义类加载器优先加载自身插件JAR内的类和资源,避免应用类加载器加载插件的私有依赖(解决同依赖不同版本的问题)
  • 公共服务接口由应用类加载器加载,保证应用和插件能基于同一接口交互
  • 通过ServiceLoader.load(服务接口类, 插件类加载器)指定用对应插件的类加载器加载服务实现

具体代码实现

1. 定义公共服务接口(应用侧)

这个接口必须放在应用的类路径下,是应用和插件交互的唯一契约:

// 示例包路径,可根据实际调整
public interface PluginService {
    void doBusinessLogic();
    String getPluginId();
}

2. 自定义插件类加载器

每个插件对应一个该类的实例,负责加载单个插件JAR,实现类和资源的隔离:

import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Enumeration;

public class PluginClassLoader extends URLClassLoader {

    public PluginClassLoader(URL pluginJarUrl, ClassLoader parent) {
        // 父类加载器设为应用类加载器,确保能加载公共服务接口
        super(new URL[]{pluginJarUrl}, parent);
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 打破双亲委派:先尝试从当前插件JAR加载类,找不到再委托父类加载器
        synchronized (getClassLoadingLock(name)) {
            Class<?> loadedClass = findLoadedClass(name);
            if (loadedClass == null) {
                try {
                    // 优先加载自身JAR内的类
                    loadedClass = findClass(name);
                } catch (ClassNotFoundException e) {
                    // 自身找不到,再委托应用类加载器加载(比如公共接口、应用共享的依赖)
                    loadedClass = super.loadClass(name, resolve);
                }
            }
            if (resolve) {
                resolveClass(loadedClass);
            }
            return loadedClass;
        }
    }

    @Override
    public URL getResource(String name) {
        // 优先从当前插件JAR获取资源
        URL resource = findResource(name);
        if (resource == null) {
            resource = super.getResource(name);
        }
        return resource;
    }

    @Override
    public Enumeration<URL> getResources(String name) throws IOException {
        // 合并自身和父类加载器的资源,优先返回自身的
        Enumeration<URL> ownResources = findResources(name);
        Enumeration<URL> parentResources = super.getResources(name);
        return new CompositeEnumeration<>(ownResources, parentResources);
    }

    // 辅助类:合并两个枚举对象
    private static class CompositeEnumeration<T> implements Enumeration<T> {
        private final Enumeration<T> first;
        private final Enumeration<T> second;

        public CompositeEnumeration(Enumeration<T> first, Enumeration<T> second) {
            this.first = first;
            this.second = second;
        }

        @Override
        public boolean hasMoreElements() {
            return first.hasMoreElements() || second.hasMoreElements();
        }

        @Override
        public T nextElement() {
            return first.hasMoreElements() ? first.nextElement() : second.nextElement();
        }
    }
}

3. 应用侧的插件加载逻辑

负责扫描插件目录,为每个插件创建独立的类加载器,并用ServiceLoader加载插件服务:

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Iterator;
import java.util.ServiceLoader;

public class PluginLoader {

    public static void loadPlugins(String pluginDirPath) {
        File pluginDir = new File(pluginDirPath);
        if (!pluginDir.isDirectory()) {
            System.err.println("插件目录无效: " + pluginDirPath);
            return;
        }

        File[] pluginJars = pluginDir.listFiles((dir, name) -> name.toLowerCase().endsWith(".jar"));
        if (pluginJars == null || pluginJars.length == 0) {
            System.err.println("未找到任何插件JAR包");
            return;
        }

        // 应用类加载器作为父加载器,确保能加载公共服务接口
        ClassLoader appClassLoader = PluginLoader.class.getClassLoader();

        for (File jarFile : pluginJars) {
            try {
                URL jarUrl = jarFile.toURI().toURL();
                // 每个插件分配独立的类加载器
                PluginClassLoader pluginCL = new PluginClassLoader(jarUrl, appClassLoader);

                // 使用插件专属类加载器加载服务
                ServiceLoader<PluginService> serviceLoader = ServiceLoader.load(PluginService.class, pluginCL);
                Iterator<PluginService> iterator = serviceLoader.iterator();

                while (iterator.hasNext()) {
                    PluginService plugin = iterator.next();
                    System.out.println("成功加载插件: " + plugin.getPluginId());
                    plugin.doBusinessLogic();
                }

            } catch (MalformedURLException e) {
                System.err.println("加载插件JAR失败: " + jarFile.getName());
                e.printStackTrace();
            } catch (Exception e) {
                System.err.println("初始化插件出错: " + jarFile.getName());
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        // 示例插件目录:当前项目下的plugins文件夹
        loadPlugins("./plugins");
    }
}

插件侧的配置要求

  • 插件JAR中必须包含META-INF/services/[公共接口全限定名]文件,内容是插件实现类的全限定名,比如:
    com.example.pluginA.PluginAImpl
    
  • 插件的私有依赖(比如不同版本的第三方库)要打包进自身JAR,或放在插件专属目录,确保能被自定义类加载器加载
  • 插件只能通过PluginService接口与应用交互,不能直接引用应用侧的其他类

关键说明

如果仅给ServiceLoader.load()传自定义类加载器,但没有调整类加载顺序(还是遵循双亲委派),会导致:

  1. 插件的私有依赖类会被应用类加载器加载,引发版本冲突
  2. 多个插件共用同一个类加载器的话,同名类/资源会出现冲突

只有为每个插件分配独立的类加载器,同时让自定义类加载器优先加载自身JAR内的类,才能真正满足你提出的两个隔离要求。

内容的提问来源于stack exchange,提问作者sschuberth

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 13:30:40