Dynamic Loading and Unloading of Java Governance Tasks with Custom ClassLoader and XXL‑Job Integration
The solution introduces a plug‑in architecture that uses a custom URLClassLoader to load governance task JARs at runtime, registers their Spring beans and @XxlJob handlers with XXL‑Job, and provides unload logic to deregister jobs, destroy beans, and update configuration via YAML or Nacos without restarting the service.
Overview
The data‑governance service contains many tasks. Updating or adding a single task currently requires restarting the whole service, which disrupts other tasks.
Goals
Start or stop any governance task dynamically.
Upgrade or add tasks without restarting the service.
Ensure that starting, stopping, upgrading or adding a task does not affect other running tasks.
Solution
Introduce a plug‑in style architecture: load business functions as separate JARs via a custom class loader, register each task as an xxl‑job job, and manage them through Spring.
1. Custom ClassLoader
A subclass of URLClassLoader keeps a map of loaded classes and provides an unload() method that calls a destory() (sic) method via reflection and then closes the 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;
/**
* Custom class loader
*/
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 {
Class
clazz = entry.getValue();
Method destory = clazz.getDeclaredMethod("destory");
destory.invoke(clazz);
} catch (Exception e) {
// class has no destory method
}
}
close();
} catch (Exception e) {
e.printStackTrace();
}
}
}2. Dynamic Loading Component
The DynamicLoad component reads a JAR file, creates a MyClassLoader , loads all classes, registers Spring‑annotated beans into the application context, and registers methods annotated with @XxlJob into an XxlJobSpringExecutor .
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;
/**
* Dynamic loading of JARs
*/
@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;
public void loadJar(String path, String fileName, Boolean isRegistXxlJob) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
File file = new File(path + "/" + fileName);
Map
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);
Enumeration
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
> loadedClasses = myClassloader.getLoadedClasses();
XxlJobSpringExecutor xxlJobExecutor = new XxlJobSpringExecutor();
for (Map.Entry
> entry : loadedClasses.entrySet()) {
Class
clazz = entry.getValue();
Boolean flag = SpringAnnotationUtils.hasSpringAnnotation(clazz);
if (flag) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
String packageName = entry.getKey().substring(0, entry.getKey().lastIndexOf(".") + 1);
String beanName = entry.getKey().substring(entry.getKey().lastIndexOf(".") + 1);
beanName = packageName + beanName.substring(0, 1).toLowerCase() + beanName.substring(1);
beanFactory.registerBeanDefinition(beanName, beanDefinition);
beanFactory.autowireBean(clazz);
beanFactory.initializeBean(clazz, beanName);
}
Map
annotatedMethods = null;
try {
annotatedMethods = MethodIntrospector.selectMethods(clazz,
method -> AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class));
} catch (Throwable ex) {
}
for (Map.Entry
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));
}
}
}
} catch (IOException e) {
logger.error("读取{} 文件异常", fileName);
e.printStackTrace();
throw new RuntimeException("读取jar文件异常: " + fileName);
}
}
}3. Dynamic Unloading
The unloadJar method removes the loaded classes from the custom class loader, deregisters the corresponding XXL‑Job handlers, destroys Spring beans, and clears the loader’s internal class cache.
public void unloadJar(String fileName) throws IllegalAccessException, NoSuchFieldException {
MyClassLoader myClassLoader = myClassLoaderCenter.get(fileName);
Field privateField = XxlJobExecutor.class.getDeclaredField("jobHandlerRepository");
privateField.setAccessible(true);
XxlJobExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
Map
jobHandlerRepository = (ConcurrentHashMap
) privateField.get(xxlJobSpringExecutor);
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
Map
> loadedClasses = myClassLoader.getLoadedClasses();
Set
beanNames = new HashSet<>();
for (Map.Entry
> 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 = null;
try {
bean = applicationContext.getBean(beanName);
} catch (Exception e) {
continue;
}
Map
annotatedMethods = null;
try {
annotatedMethods = MethodIntrospector.selectMethods(bean.getClass(),
method -> AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class));
} catch (Throwable ex) {
}
for (Map.Entry
methodXxlJobEntry : annotatedMethods.entrySet()) {
XxlJob xxlJob = methodXxlJobEntry.getValue();
jobHandlerRepository.remove(xxlJob.value());
}
beanNames.add(beanName);
beanFactory.destroyBean(beanName, bean);
}
Field mergedBeanDefinitions = beanFactory.getClass().getSuperclass().getSuperclass().getDeclaredField("mergedBeanDefinitions");
mergedBeanDefinitions.setAccessible(true);
Map
rootBeanDefinitionMap = (Map
) mergedBeanDefinitions.get(beanFactory);
for (String beanName : beanNames) {
beanFactory.removeBeanDefinition(beanName);
rootBeanDefinitionMap.remove(beanName);
}
jobHandlerRepository.remove(fileName);
try {
Field field = ClassLoader.class.getDeclaredField("classes");
field.setAccessible(true);
Vector
> classes = (Vector
>) field.get(myClassLoader);
classes.removeAllElements();
myClassLoaderCenter.remove(fileName);
myClassLoader.unload();
} catch (Exception e) {
logger.error("动态卸载的类,从类加载器中卸载失败");
e.printStackTrace();
}
logger.error("{} 动态卸载成功", fileName);
}4. Dynamic Configuration
Two approaches are provided to persist the list of loaded JARs:
Modify a local bootstrap.yml file using SnakeYAML.
Update a Nacos configuration entry ( sjzl-loadjars.yml ) via the Nacos SDK.
Example of the YAML updater:
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;
@Component
public class ConfigUpdater {
public void updateLoadJars(List
jarNames) throws IOException {
Yaml yaml = new Yaml();
InputStream inputStream = new FileInputStream(new File("src/main/resources/bootstrap.yml"));
Map
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);
}
}Example of the Nacos updater (adds a JAR name to the loadjars list):
package cn.jy.sjzl.util;
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.Map;
@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);
Map
jsonObject = com.alibaba.fastjson.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配置更新失败");
}
}
}5. Packaging
The Maven Shade plugin is used to create an executable JAR that contains only the required packages (e.g., com/jy/job/demo/** ) and sets the final artifact name to demoJob .
<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>Java Tech Enthusiast
Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!
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.