Backend Development 22 min read

Dynamic Class Loading, Registration, and Unloading for Data Governance Tasks in a Spring‑XXL‑Job Service

This article explains how to design a plug‑in architecture for a data‑governance service by using a custom URLClassLoader, Spring bean registration, and XXL‑Job integration to dynamically load, start, stop, upgrade, and unload individual governance tasks without restarting the whole service, and also shows how to persist the configuration in local YAML or Nacos.

Top Architect
Top Architect
Top Architect
Dynamic Class Loading, Registration, and Unloading for Data Governance Tasks in a Spring‑XXL‑Job Service

Overview – In a data‑governance platform each governance task currently requires a full service restart when upgraded or added, which blocks other tasks. The goal is to enable dynamic start, stop, upgrade, and addition of tasks without affecting the rest of the system.

Solution – The approach is to decouple business functions into separate JARs that can be loaded at runtime, registered with Spring, and scheduled with XXL‑Job. A custom class loader is used to isolate the JARs, and a management component handles loading, registration, and unloading.

1. Custom Class Loader

package cn.jy.sjzl.util;

import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 自定义类加载器
 * @author lijianyu
 * @date 2023/04/03 17:54
 */
public class MyClassLoader extends URLClassLoader {
    private Map
> loadedClasses = new ConcurrentHashMap<>();

    public Map
> getLoadedClasses() {
        return loadedClasses;
    }

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

    @Override
    protected Class
findClass(String name) throws ClassNotFoundException {
        Class
clazz = loadedClasses.get(name);
        if (clazz != null) {
            return clazz;
        }
        try {
            clazz = super.findClass(name);
            loadedClasses.put(name, clazz);
            return clazz;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }

    public void unload() {
        try {
            for (Map.Entry
> entry : loadedClasses.entrySet()) {
                String className = entry.getKey();
                loadedClasses.remove(className);
                try {
                    Method destroy = entry.getValue().getDeclaredMethod("destory");
                    destroy.invoke(entry.getValue());
                } catch (Exception e) {
                    // ignore if no destroy method
                }
            }
            close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2. Dynamic Loading

package com.jy.dynamicLoad;

import com.jy.annotation.XxlJobCron;
import com.jy.classLoader.MyClassLoader;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import com.xxl.job.core.handler.annotation.XxlJob;
import com.xxl.job.core.handler.impl.MethodJobHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.Enumeration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * @author lijianyu
 * @date 2023/04/29 13:18
 */
@Component
public class DynamicLoad {
    private static Logger logger = LoggerFactory.getLogger(DynamicLoad.class);

    @Autowired
    private ApplicationContext applicationContext;

    private Map
myClassLoaderCenter = new ConcurrentHashMap<>();

    @Value("${dynamicLoad.path}")
    private String path;

    /**
     * 动态加载指定路径下指定jar包
     */
    public void loadJar(String path, String fileName, Boolean isRegistXxlJob) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        File file = new File(path + "/" + fileName);
        // ... (omitted for brevity) ...
        // Load classes, register Spring beans, register XXL‑Job handlers
    }
}

3. Spring Annotation Utility

package com.jy.sjzl.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.context.annotation.Configuration;

public class SpringAnnotationUtils {
    private static Logger logger = LoggerFactory.getLogger(SpringAnnotationUtils.class);

    /** 判断一个类是否有 Spring 核心注解 */
    public static boolean hasSpringAnnotation(Class
clazz) {
        if (clazz == null) return false;
        if (clazz.isInterface()) return false;
        if (Modifier.isAbstract(clazz.getModifiers())) return false;
        try {
            if (clazz.getAnnotation(Component.class) != null ||
                clazz.getAnnotation(Repository.class) != null ||
                clazz.getAnnotation(Service.class) != null ||
                clazz.getAnnotation(Controller.class) != null ||
                clazz.getAnnotation(Configuration.class) != null) {
                return true;
            }
        } catch (Exception e) {
            logger.error("出现异常:{}", e.getMessage());
        }
        return false;
    }
}

4. Dynamic Unloading

public void unloadJar(String fileName) throws IllegalAccessException, NoSuchFieldException {
    MyClassLoader myClassLoader = myClassLoaderCenter.get(fileName);
    // Remove job handlers from XXL‑Job executor
    // Remove beans from Spring BeanFactory
    // Clear classes from the custom class loader and close it
    logger.error("{} 动态卸载成功", fileName);
}

5. Dynamic Configuration – To keep the loaded tasks after a service restart, the article shows two ways to persist the list of JARs:

Modify a local bootstrap.yml file at runtime (requires snakeyaml dependency).

Update a Nacos configuration file ( sjzl‑loadjars.yml ) via the Nacos Java SDK.

Example of updating a local YAML file:

<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>1.29</version>
</dependency>

Utility class that rewrites bootstrap.yml :

package com.jy.util;

import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import java.io.*;
import java.util.List;
import java.util.Map;

/** 用于动态修改 bootstrap.yml 配置文件 */
@Component
public class ConfigUpdater {
    public void updateLoadJars(List
jarNames) throws IOException {
        Yaml yaml = new Yaml();
        InputStream is = new FileInputStream(new File("src/main/resources/bootstrap.yml"));
        Map
obj = yaml.load(is);
        is.close();
        obj.put("loadjars", jarNames);
        FileWriter writer = new FileWriter(new File("src/main/resources/bootstrap.yml"));
        DumperOptions options = new DumperOptions();
        options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
        options.setPrettyFlow(true);
        Yaml yamlWriter = new Yaml(options);
        yamlWriter.dump(obj, writer);
    }
}

Example of updating Nacos configuration:

package cn.jy.sjzl.config;

import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.exception.NacosException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;

@Configuration
public class NacosConfig {
    @Value("${spring.cloud.nacos.server-addr}")
    private String serverAddr;
    @Value("${spring.cloud.nacos.config.namespace}")
    private String namespace;

    public ConfigService configService() throws NacosException {
        Properties properties = new Properties();
        properties.put("serverAddr", serverAddr);
        properties.put("namespace", namespace);
        return NacosFactory.createConfigService(properties);
    }
}

Utility that adds a JAR name to the Nacos list:

package cn.jy.sjzl.util;

import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;

@Component
public class NacosConfigUtil {
    private static Logger logger = LoggerFactory.getLogger(NacosConfigUtil.class);
    @Autowired
    private NacosConfig nacosConfig;
    private String dataId = "sjzl-loadjars.yml";
    @Value("${spring.cloud.nacos.config.group}")
    private String group;

    public void addJarName(String jarName) throws Exception {
        ConfigService configService = nacosConfig.configService();
        String content = configService.getConfig(dataId, group, 5000);
        YAMLMapper yamlMapper = new YAMLMapper();
        ObjectMapper jsonMapper = new ObjectMapper();
        Object yamlObject = yamlMapper.readValue(content, Object.class);
        String jsonString = jsonMapper.writeValueAsString(yamlObject);
        JSONObject jsonObject = JSONObject.parseObject(jsonString);
        List
loadjars = jsonObject.containsKey("loadjars") ? (List
) jsonObject.get("loadjars") : new ArrayList<>();
        if (!loadjars.contains(jarName)) {
            loadjars.add(jarName);
        }
        jsonObject.put("loadjars", loadjars);
        Object yaml = yamlMapper.readValue(jsonMapper.writeValueAsString(jsonObject), Object.class);
        String newYamlString = yamlMapper.writeValueAsString(yaml);
        boolean b = configService.publishConfig(dataId, group, newYamlString);
        if (b) {
            logger.info("nacos配置更新成功");
        } else {
            logger.info("nacos配置更新失败");
        }
    }
}

6. Packaging – When the JARs are built they can be shaded with Maven Shade Plugin to produce a single executable JAR. Example configuration:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.2.4</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <filters>
                            <filter>
                                <artifact>*:*</artifact>
                                <includes>
                                    <include>com/jy/job/demo/**</include>
                                </includes>
                            </filter>
                        </filters>
                        <finalName>demoJob</finalName>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

By combining the custom class loader, Spring bean registration, XXL‑Job handler registration, and dynamic configuration updates, the system achieves true plug‑in capability: governance tasks can be added, upgraded, started, stopped, or removed at runtime without service downtime.

JavamicroservicesSpringClassLoaderxxl-jobDynamicLoading
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

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.