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.
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.
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.
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.
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.
