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.
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>Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
