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

This article explains how to implement dynamic loading and unloading of Java modules using a custom URLClassLoader, integrate them with Spring beans and XXL‑Job tasks, and update configurations at runtime via YAML or Nacos, while also covering packaging with Maven shade plugin.

Java Backend Technology
Java Backend Technology
Java Backend Technology
How to Dynamically Load and Unload Java Modules with Custom ClassLoaders and XXL‑Job

Dynamic Loading and Unloading in Java Projects

Goal

Enable dynamic start/stop of any governance task.

Support dynamic upgrade or addition of governance tasks.

Ensure that starting, stopping, upgrading or adding tasks does not affect other tasks.

Solution

Load business functions dynamically to achieve plug‑in style loading and composable deployment.

Register governance tasks as XXL‑Job jobs via the xxl‑job framework for unified management.

Dynamic Loading

URLClassLoader is used to load external JAR files or class files at runtime. A custom class loader extends URLClassLoader to manage loaded classes and support unloading.

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
 * @author lijianyu
 * @date 2023/04/03 17:54
 */
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 {
                    Class<?> clazz = entry.getValue();
                    Method destroy = clazz.getDeclaredMethod("destory");
                    destroy.invoke(clazz);
                } catch (Exception e) {
                    // class may not have destroy method
                }
            }
            close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

The loading process for a JAR file includes:

Read the JAR into memory using a URL with the jar:file: protocol.

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

Iterate over all .class entries, load each class via the custom loader, and collect them.

For each loaded class, check whether it carries a Spring core annotation (e.g., @Component, @Service) using SpringAnnotationUtils.hasSpringAnnotation. If so, register the class as a Spring bean via BeanDefinitionBuilder and autowire it.

Detect methods annotated with @XxlJob. Ensure each method also has a custom @XxlJobCron annotation; otherwise throw an exception. Register the method as an XXL‑Job handler using XxlJobSpringExecutor.registJobHandler.

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.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 component
 * @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<String, MyClassLoader> myClassLoaderCenter = new ConcurrentHashMap<>();

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

    /**
     * Load a JAR from the specified path.
     */
    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<>();
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
        try {
            URL url = new URL("jar:file:" + file.getAbsolutePath() + "!/");
            URLConnection urlConnection = url.openConnection();
            JarURLConnection jarURLConnection = (JarURLConnection) urlConnection;
            JarFile jarFile = jarURLConnection.getJarFile();
            MyClassLoader myClassloader = new MyClassLoader(new URL[]{url}, ClassLoader.getSystemClassLoader());
            myClassLoaderCenter.put(fileName, myClassloader);
            Set<Class> initBeanClass = new HashSet<>(jarFile.size());
            Enumeration<JarEntry> entries = jarFile.entries();
            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
                if (SpringAnnotationUtils.hasSpringAnnotation(clazz)) {
                    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, method -> 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 RuntimeException(executeMethod.getName() + "() missing @XxlJobCron annotation");
                    }
                    if (!CronExpression.isValidExpression(xxlJobCron.value())) {
                        throw new RuntimeException(executeMethod.getName() + "() has invalid cron expression");
                    }
                    XxlJob xxlJob = methodXxlJobEntry.getValue();
                    jobPar.put(xxlJob.value(), xxlJobCron.value());
                    if (isRegistXxlJob) {
                        executeMethod.setAccessible(true);
                        Method initMethod = null;
                        Method destroyMethod = null;
                        xxlJobExecutor.registJobHandler(xxlJob.value(), new CustomerMethodJobHandler(clazz, executeMethod, initMethod, destroyMethod));
                    }
                }
            }
            // Initialize beans
            initBeanClass.forEach(beanFactory::getBean);
        } catch (IOException e) {
            logger.error("Failed to read {}", fileName, e);
            throw new RuntimeException("Failed to read jar file: " + fileName, e);
        }
    }
}

Dynamic Unloading

The unloading process removes the loaded classes, Spring beans, and XXL‑Job handlers, then clears references from the custom class loader.

/**
 * Dynamically unload a JAR.
 */
public void unloadJar(String fileName) throws IllegalAccessException, NoSuchFieldException {
    MyClassLoader myClassLoader = myClassLoaderCenter.get(fileName);
    // Remove job handlers from 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);
    // Remove beans from Spring
    DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
    Map<String, Class<?>> loadedClasses = myClassLoader.getLoadedClasses();
    Set<String> beanNames = new HashSet<>();
    for (Map.Entry<String, Class<?>> entry : loadedClasses.entrySet()) {
        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);
        Object bean;
        try {
            bean = applicationContext.getBean(beanName);
        } catch (Exception e) {
            continue;
        }
        // Remove XXL‑Job handlers defined in this class
        Map<Method, XxlJob> annotatedMethods = null;
        try {
            annotatedMethods = MethodIntrospector.selectMethods(bean.getClass(), method -> 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());
        }
        beanNames.add(beanName);
        beanFactory.destroyBean(beanName, bean);
    }
    // Remove bean definitions
    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 parent job handler
    jobHandlerRepository.remove(fileName);
    // Remove classes from the class loader
    try {
        Field field = ClassLoader.class.getDeclaredField("classes");
        field.setAccessible(true);
        @SuppressWarnings("unchecked")
        java.util.Vector<Class<?>> classes = (java.util.Vector<Class<?>>) field.get(myClassLoader);
        classes.removeAllElements();
        myClassLoaderCenter.remove(fileName);
        myClassLoader.unload();
    } catch (NoSuchFieldException | IllegalAccessException e) {
        logger.error("Failed to unload class loader for {}", fileName, e);
    }
    logger.info("{} dynamic unload successful", fileName);
}

Dynamic Configuration

To keep loaded tasks after a service restart, configuration is updated at runtime. Two approaches are provided: modifying a local YAML file and updating Nacos configuration.

Modify Local YAML

Add snakeyaml dependency and use ConfigUpdater to rewrite bootstrap.yml with the new list of JAR names.

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;

import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Map;

/**
 * Update bootstrap.yml dynamically.
 */
@Component
public class ConfigUpdater {
    public void updateLoadJars(List<String> jarNames) throws IOException {
        Yaml yaml = new Yaml();
        FileInputStream inputStream = new FileInputStream("src/main/resources/bootstrap.yml");
        Map<String, Object> obj = yaml.load(inputStream);
        inputStream.close();
        obj.put("loadjars", jarNames);
        FileWriter writer = new FileWriter("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);
    }
}

Update Nacos Configuration

Using Spring Cloud Alibaba Nacos, the NacosConfigUtil class reads the existing sjzl-loadjars.yml from Nacos, adds the new JAR name, and republishes the configuration.

package cn.jy.sjzl.util;

import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.exception.NacosException;
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.Properties;

/**
 * Add a JAR name to Nacos configuration.
 */
@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();
        Object yamlObject = yamlMapper.readValue(content, Object.class);
        ObjectMapper jsonMapper = new ObjectMapper();
        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 success = configService.publishConfig(dataId, group, newYamlString);
        if (success) {
            logger.info("Nacos configuration updated successfully");
        } else {
            logger.info("Nacos configuration update failed");
        }
    }
}

Packaging

When creating a deployable JAR, the Maven Shade plugin can be configured to include only the necessary packages.

<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>
Dynamic loading architecture diagram
Dynamic loading architecture diagram
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.

Java Backend Technology
Written by

Java Backend Technology

Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!

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.