How to Dynamically Load and Unload Java Governance Tasks with Custom ClassLoaders and XXL‑Job

This article explains how to design a plug‑in architecture for a data‑governance service that can start, stop, add, or upgrade individual tasks at runtime without restarting the whole service, using a custom URLClassLoader, Spring bean registration, XXL‑Job integration, dynamic configuration updates, and a clean unload process.

Architect
Architect
Architect
How to Dynamically Load and Unload Java Governance Tasks with Custom ClassLoaders and XXL‑Job

Overview

The data‑governance service contains many independent tasks. Previously, modifying or adding any task required restarting the whole service, which disrupted other tasks. The goal is to enable dynamic start/stop, add, and upgrade of any task without affecting the rest of the system.

Solution Architecture

Encapsulate each business function in a separate JAR and load it at runtime to achieve a plug‑in style deployment.

Register each task as an xxl‑job job so that the XXL‑Job scheduler can manage them uniformly.

1. Custom ClassLoader

A subclass of URLClassLoader is created to keep track of loaded classes and provide an explicit unload method.

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;

/**
 * Custom class loader for dynamic loading.
 */
public class MyClassLoader extends URLClassLoader {
    private Map<String, Class<?>> loadedClasses = new ConcurrentHashMap<>();

    public Map<String, Class<?>> 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<String, Class<?>> entry : loadedClasses.entrySet()) {
                String className = entry.getKey();
                loadedClasses.remove(className);
                try {
                    Method destroy = entry.getValue().getDeclaredMethod("destory");
                    destroy.invoke(entry.getValue());
                } catch (Exception e) {
                    // No destroy method
                }
            }
            close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

The loader stores each loaded class in a map so that it can be removed later. The unload method clears the map, optionally calls a destory (sic) method on the class, and then closes the loader.

2. Dynamic Loading Process

The DynamicLoad component reads a JAR from a configured directory, creates a MyClassLoader, loads all .class files, registers Spring beans for classes that carry Spring annotations, and registers methods annotated with @XxlJob as XXL‑Job handlers.

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 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.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * Dynamic loading implementation.
 */
@Component
public class DynamicLoad {
    private static Logger logger = LoggerFactory.getLogger(DynamicLoad.class);

    @Autowired
    private ApplicationContext applicationContext;

    private Map<String, MyClassLoader> myClassLoaderCenter = new ConcurrentHashMap<>();

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

    /**
     * Load a JAR, optionally register its XXL‑Job handlers.
     */
    public void loadJar(String path, String fileName, Boolean isRegistXxlJob) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        File file = new File(path + "/" + fileName);
        Map<String, String> jobPar = new HashMap<>();
        // Obtain Spring bean factory
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
        try {
            // Build URL for the JAR
            URL url = new URL("jar:file:" + file.getAbsolutePath() + "!/");
            URLConnection urlConnection = url.openConnection();
            JarURLConnection jarURLConnection = (JarURLConnection) urlConnection;
            JarFile jarFile = jarURLConnection.getJarFile();
            Enumeration<JarEntry> entries = jarFile.entries();

            // Create custom class loader
            MyClassLoader myClassloader = new MyClassLoader(new URL[]{url}, ClassLoader.getSystemClassLoader());
            myClassLoaderCenter.put(fileName, myClassloader);
            Set<Class> initBeanClass = new HashSet<>(jarFile.size());

            while (entries.hasMoreElements()) {
                JarEntry jarEntry = entries.nextElement();
                if (jarEntry.getName().endsWith(".class")) {
                    String className = jarEntry.getName().replace('/', '.').substring(0, jarEntry.getName().length() - 6);
                    myClassloader.loadClass(className);
                }
            }
            Map<String, Class<?>> loadedClasses = myClassloader.getLoadedClasses();
            XxlJobSpringExecutor xxlJobExecutor = new XxlJobSpringExecutor();
            for (Map.Entry<String, Class<?>> entry : loadedClasses.entrySet()) {
                String className = entry.getKey();
                Class<?> clazz = entry.getValue();
                // Register Spring beans
                Boolean flag = SpringAnnotationUtils.hasSpringAnnotation(clazz);
                if (flag) {
                    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
                    AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
                    String packageName = className.substring(0, className.lastIndexOf(".") + 1);
                    String beanName = className.substring(className.lastIndexOf(".") + 1);
                    beanName = packageName + beanName.substring(0, 1).toLowerCase() + beanName.substring(1);
                    beanFactory.registerBeanDefinition(beanName, beanDefinition);
                    beanFactory.autowireBean(clazz);
                    beanFactory.initializeBean(clazz, beanName);
                    initBeanClass.add(clazz);
                }
                // Register XXL‑Job handlers
                Map<Method, XxlJob> annotatedMethods = null;
                try {
                    annotatedMethods = MethodIntrospector.selectMethods(clazz, new MethodIntrospector.MetadataLookup<XxlJob>() {
                        @Override
                        public XxlJob inspect(Method method) {
                            return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class);
                        }
                    });
                } catch (Throwable ex) {
                    // ignore
                }
                for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
                    Method executeMethod = methodXxlJobEntry.getKey();
                    XxlJobCron xxlJobCron = executeMethod.getAnnotation(XxlJobCron.class);
                    if (xxlJobCron == null) {
                        throw new CustomException("500", executeMethod.getName() + "(),没有添加@XxlJobCron注解配置定时策略");
                    }
                    if (!CronExpression.isValidExpression(xxlJobCron.value())) {
                        throw new CustomException("500", executeMethod.getName() + "(),@XxlJobCron参数内容错误");
                    }
                    XxlJob xxlJob = methodXxlJobEntry.getValue();
                    jobPar.put(xxlJob.value(), xxlJobCron.value());
                    if (isRegistXxlJob) {
                        executeMethod.setAccessible(true);
                        xxlJobExecutor.registJobHandler(xxlJob.value(), new CustomerMethodJobHandler(clazz, executeMethod, null, null));
                    }
                }
            }
            // Initialize beans that were added to Spring
            initBeanClass.forEach(beanFactory::getBean);
        } catch (IOException e) {
            logger.error("读取{} 文件异常", fileName);
            e.printStackTrace();
            throw new RuntimeException("读取jar文件异常: " + fileName);
        }
    }
}

The loader performs the following steps:

Construct a URL pointing to the JAR and open it via JarURLConnection.

Create a MyClassLoader instance and store it in a map for later management.

Iterate over all entries in the JAR; for each .class file, compute the fully‑qualified class name and invoke loadClass on the custom loader.

Collect the loaded classes, then for each class:

Check whether it carries a Spring core annotation ( @Component, @Service, etc.) using SpringAnnotationUtils. If so, build a bean definition, generate a unique bean name, and register the bean with the Spring DefaultListableBeanFactory. Autowire and initialize the bean so that it participates in the application context.

Inspect methods for the @XxlJob annotation. For each such method, verify that an accompanying @XxlJobCron annotation exists and that its cron expression is valid. Record the job‑handler name and cron expression, and if registration is requested, register the method as an XXL‑Job handler via XxlJobSpringExecutor.registJobHandler.

After processing all classes, trigger Spring to instantiate the newly registered beans.

Utility: SpringAnnotationUtils

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

    /**
     * Determine whether a class has a core Spring annotation.
     */
    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;
    }
}

3. Dynamic Unloading Process

When a JAR needs to be removed, the system reverses the loading steps: it removes the job handler from XXL‑Job, destroys the Spring bean, deletes the bean definition, clears the class loader’s internal class cache, and finally discards the custom class loader.

public void unloadJar(String fileName) throws IllegalAccessException, NoSuchFieldException {
    // Retrieve the custom loader for the JAR
    MyClassLoader myClassLoader = myClassLoaderCenter.get(fileName);

    // Access private jobHandlerRepository field of XXL‑Job executor
    Field privateField = XxlJobExecutor.class.getDeclaredField("jobHandlerRepository");
    privateField.setAccessible(true);
    XxlJobExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
    Map<String, IJobHandler> jobHandlerRepository = (ConcurrentHashMap<String, IJobHandler>) privateField.get(xxlJobSpringExecutor);

    // Spring bean factory for bean removal
    DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
    Map<String, Class<?>> loadedClasses = myClassLoader.getLoadedClasses();

    Set<String> beanNames = new HashSet<>();
    for (Map.Entry<String, Class<?>> entry : loadedClasses.entrySet()) {
        // Derive bean name from class name
        String key = entry.getKey();
        String packageName = key.substring(0, key.lastIndexOf(".") + 1);
        String beanName = key.substring(key.lastIndexOf(".") + 1);
        beanName = packageName + beanName.substring(0, 1).toLowerCase() + beanName.substring(1);

        // Remove XXL‑Job handler
        Object bean = null;
        try {
            bean = applicationContext.getBean(beanName);
        } catch (Exception e) {
            continue; // Bean not in Spring, skip
        }
        Map<Method, XxlJob> annotatedMethods = null;
        try {
            annotatedMethods = MethodIntrospector.selectMethods(bean.getClass(), new MethodIntrospector.MetadataLookup<XxlJob>() {
                @Override
                public XxlJob inspect(Method method) {
                    return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class);
                }
            });
        } catch (Throwable ex) {
            // ignore
        }
        for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
            XxlJob xxlJob = methodXxlJobEntry.getValue();
            jobHandlerRepository.remove(xxlJob.value());
        }
        // Mark bean for removal from Spring
        beanNames.add(beanName);
        beanFactory.destroyBean(beanName, bean);
    }

    // Remove bean definitions from the bean factory's internal map
    Field mergedBeanDefinitions = beanFactory.getClass().getSuperclass().getSuperclass().getDeclaredField("mergedBeanDefinitions");
    mergedBeanDefinitions.setAccessible(true);
    Map<String, RootBeanDefinition> rootBeanDefinitionMap = (Map<String, RootBeanDefinition>) mergedBeanDefinitions.get(beanFactory);
    for (String beanName : beanNames) {
        beanFactory.removeBeanDefinition(beanName);
        rootBeanDefinitionMap.remove(beanName);
    }

    // Remove the parent job handler (the JAR itself)
    jobHandlerRepository.remove(fileName);

    // Clean up the class loader's internal class list
    try {
        Field field = ClassLoader.class.getDeclaredField("classes");
        field.setAccessible(true);
        Vector<Class<?>> classes = (Vector<Class<?>>) field.get(myClassLoader);
        classes.removeAllElements();
        myClassLoaderCenter.remove(fileName);
        myClassLoader.unload();
    } catch (NoSuchFieldException | IllegalAccessException e) {
        logger.error("动态卸载的类,从类加载器中卸载失败");
        e.printStackTrace();
    }
    logger.error("{} 动态卸载成功", fileName);
}

4. Dynamic Configuration Updates

To prevent loss of loaded tasks after a service restart, the system updates configuration sources at runtime.

4.1 Modify Local bootstrap.yml

Add the snakeyaml dependency and use a utility class to read, modify, and write the YAML file.

<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>1.29</version>
</dependency>
package com.jy.util;

import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.InputStream;
import java.util.Map;

/**
 * Update bootstrap.yml with the list of loaded JARs.
 */
public class ConfigUpdater {
    public void updateLoadJars(List<String> jarNames) throws IOException {
        Yaml yaml = new Yaml();
        InputStream inputStream = new FileInputStream(new File("src/main/resources/bootstrap.yml"));
        Map<String, Object> obj = yaml.load(inputStream);
        inputStream.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);
    }
}

4.2 Modify Nacos Configuration

Spring Cloud Alibaba Nacos allows programmatic updates of configuration files. The NacosConfigUtil class reads the existing YAML from Nacos, adds a new JAR name to the loadjars list, and publishes the updated content.

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);
    }
}
package cn.jy.sjzl.util;

import com.alibaba.nacos.api.config.ConfigService;
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;
import java.util.Map;

/**
 * Add a JAR name to the Nacos configuration file.
 */
@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);
        com.alibaba.fastjson.JSONObject jsonObject = com.alibaba.fastjson.JSONObject.parseObject(jsonString);
        List<String> loadjars;
        if (jsonObject.containsKey("loadjars")) {
            loadjars = (List<String>) jsonObject.get("loadjars");
        } else {
            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配置更新失败");
        }
    }
}

5. Packaging the Plug‑in JAR

The Maven Shade plugin is used to produce a shaded JAR that contains only the required classes (e.g., com/jy/job/demo/**) and gives the final artifact a custom name.

<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 adjusting the filters and finalName sections, developers can control which packages are bundled and the name of the resulting JAR.

Overall, the article walks through the complete lifecycle of a plug‑in task: defining a custom class loader, loading JARs, registering Spring beans and XXL‑Job handlers, updating runtime configuration, and finally unloading the task cleanly, all while preserving the stability of the running service.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaMicroservicesspringNacosclassloaderDynamic LoadingXXL-JOB
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

0 followers
Reader feedback

How this landed with the community

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.