Backend Development 12 min read

Implementing ClassLoader Isolation for Script Execution in a Business Monitoring Platform

The article explains how a Business Monitoring Platform isolates user‑defined Groovy scripts by assigning each script its own custom Java ClassLoader that loads uploaded JARs, breaking parent‑delegation, preventing class conflicts, reducing Metaspace growth, and dynamically creating Feign and Dubbo beans for safe, independent execution.

DeWu Technology
DeWu Technology
DeWu Technology
Implementing ClassLoader Isolation for Script Execution in a Business Monitoring Platform

The article introduces a self‑developed Business Monitoring Platform that validates data and system status, quickly detecting dirty data and logical errors to protect assets and ensure stability.

Validation is performed by executing user‑defined Groovy scripts. The focus of the article is a technical problem: achieving script execution isolation using Java ClassLoader isolation.

Script debugging workflow: developers write a DemoScript implementation with filter and check methods, upload the script and any dependent JARs, debug the script on the platform, and finally publish it.

@Service
public class DubboDemoScript implements DemoScript {
    @Resource
    private DemoService demoService;

    @Override
    public boolean filter(JSONObject jsonObject) {
        // 这里省略数据过滤逻辑 由业务使用方实现
        return true;
    }

    @Override
    public String check(JSONObject jsonObject) {
        Long id = jsonObject.getLong("id");
        // 数据校验,由业务使用方实现
        Response responseResult = demoService.queryById(id);
        log.info("[DubboClassloaderTestDemo]返回结果={}", JsonUtils.serialize(responseResult));
        return JsonUtils.serialize(responseResult);
    }
}

In Java, a ClassLoader loads bytecode into the JVM and follows a parent‑delegation model. To break this model, a custom ClassLoader is created.

public class CustomClassLoader extends URLClassLoader {
    /**
     * @param jarPath jar文件目录地址
     * @return
     */
    private CustomClassLoader createCustomClassloader(String jarPath) throws MalformedURLException {
        File file = new File(jarPath);
        URL url = file.toURI().toURL();
        List
urlList = Lists.newArrayList(url);
        URL[] urls = new URL[urlList.size()];
        urls = urlList.toArray(urls);
        return new CustomJarClassLoader(urls, classLoader.getParent());
    }
}

The custom ClassLoader overrides loadClass and findClass to load classes from specific JAR files, ensuring each script runs with its own isolated loader.

public class CustomClassLoader extends URLClassLoader {
    public JarFile jarFile;
    public ClassLoader parent;

    public CustomClassLoader(URL[] urls, JarFile jarFile, ClassLoader parent) {
        super(urls, parent);
        this.jarFile = jarFile;
        this.parent = parent;
    }

    public CustomClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    private static String classNameToJarEntry(String name) {
        String classPath = name.replaceAll("\\.", "\\/");
        return new StringBuilder(classPath).append(".class").toString();
    }

    @Override
    protected Class
loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 这里定义类加载规则,和findClass方法一起组合打破双亲
        if (name.startsWith("com.xx") || name.startsWith("com.yyy")) {
           return this.findClass(name);
        }
        return super.loadClass(name, resolve);
    }

    @Override
    protected Class
findClass(String name) throws ClassNotFoundException {
        Class clazz = null;
        try {
            String jarEntryName = classNameToJarEntry(name);
            if (jarFile == null) {
                return clazz;
            }
            JarEntry jarEntry = jarFile.getJarEntry(jarEntryName);
            if (jarEntry != null) {
                InputStream inputStream = jarFile.getInputStream(jarEntry);
                byte[] bytes = IOUtils.toByteArray(inputStream);
                clazz = defineClass(name, bytes, 0, bytes.length);
            }
        } catch (IOException e) {
            log.info("Custom classloader load calss {} failed", name)
        }
        return clazz;
    }
}

Each script receives its own ClassLoader and a set of JAR files uploaded to HDFS, preventing class conflicts when different scripts contain classes with the same fully‑qualified name.

The platform also dynamically creates FeignClient and Dubbo consumer beans for services referenced in scripts.

/**
 * @param serverName 服务名 (@FeignClient主键中的name值)
 *  eg:@FeignClient("demo-interfaces")
 * @param beanName feign对象名称 eg: DemoFeignClient
 * @param targetClass feign的Class对象
 * @param
FeignClient主键标记的Object
 * @return
 */
public static
T build(String serverName, String beanName, Class
targetClass) {
    return buildClient(serverName, beanName, targetClass);
}

private static
T buildClient(String serverName, String beanName, Class
targetClass) {
    T t = (T) BEAN_CACHE.get(serverName + "-" + beanName);
    if (Objects.isNull(t)) {
        FeignClientBuilder.Builder
builder = new FeignClientBuilder(applicationContext).forType(targetClass, serverName);
        t = builder.build();
        BEAN_CACHE.put(serverName + "-" + beanName, t);
    }
    return t;
}
public void registerDubboBean(Class clazz, String beanName) {
    // 当前应用配置
    ApplicationConfig application = new ApplicationConfig();
    application.setName("demo-service");
    // 连接注册中心配置
    RegistryConfig registry = new RegistryConfig();
    registry.setAddress(registryAddress);
    // ReferenceConfig为重对象,内部封装了与注册中心的连接,以及与服务提供方的连接
    ReferenceConfig reference = new ReferenceConfig<>(); // 此实例很重,封装了与注册中心的连接以及与提供者的连接,请自行缓存,否则可能造成内存和连接泄漏
    reference.setApplication(application);
    reference.setRegistry(registry); // 多个注册中心可以用setRegistries()
    reference.setInterface(clazz);
    reference.setVersion("1.0");
    // 注意:此代理对象内部封装了所有通讯细节,这里用dubbo2.4版本以后提供的缓存类ReferenceConfigCache
    ReferenceConfigCache cache = ReferenceConfigCache.getCache();
    Object dubboBean = cache.get(reference);
    dubboBeanMap.put(beanName, dubboBean);
    // 注册bean
    SpringContextUtils.registerBean(beanName, dubboBean);
    // 注入bean
    SpringContextUtils.autowireBean(dubboBean);
}

Two practical issues are discussed: (1) Metaspace growth when many scripts run, mitigated by per‑script ClassLoader isolation; (2) ClassCastException caused by the same class being loaded by different ClassLoaders, solved by reusing the appropriate loader.

In summary, the article demonstrates how custom ClassLoader isolation enables dynamic JAR loading, independent script execution, and reduced maintenance overhead for the Business Monitoring Platform, with potential applicability to other scenarios.

Javabackend developmentDubboClassLoaderdynamic loadingfeignScript Isolation
DeWu Technology
Written by

DeWu Technology

A platform for sharing and discussing tech knowledge, guiding you toward the cloud of technology.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.