springBoot中動(dòng)態(tài)加載jar包的完整指南
一、概述
1、背景
目前數(shù)據(jù)治理服務(wù)中有眾多治理任務(wù),當(dāng)其中任一治理任務(wù)有改動(dòng)需要升級(jí)或新增一個(gè)治理任務(wù)時(shí),都需要將數(shù)據(jù)治理服務(wù)重啟,會(huì)影響其他治理任務(wù)的正常運(yùn)行。
2、目標(biāo)
- 能夠動(dòng)態(tài)啟動(dòng)、停止任一治理任務(wù)
- 能夠動(dòng)態(tài)升級(jí)、添加治理任務(wù)
- 啟動(dòng)、停止治理任務(wù)或升級(jí)、添加治理任務(wù)不能影響其他任務(wù)
3、方案
為了支持業(yè)務(wù)代碼盡量的解耦,把部分業(yè)務(wù)功能通過(guò)動(dòng)態(tài)加載的方式加載到主程序中,以滿足可插拔式的加載、組合式的部署。
配合xxl-job任務(wù)調(diào)度框架,將數(shù)據(jù)治理任務(wù)做成xxl-job任務(wù)的方式注冊(cè)到xxl-job中,方便統(tǒng)一管理。
二、動(dòng)態(tài)加載
1、自定義類加載器
URLClassLoader 是一種特殊的類加載器,可以從指定的 URL 中加載類和資源。它的主要作用是動(dòng)態(tài)加載外部的 JAR 包或者類文件,從而實(shí)現(xiàn)動(dòng)態(tài)擴(kuò)展應(yīng)用程序的功。為了便于管理動(dòng)態(tài)加載的jar包,自定義類加載器繼承URLClassloader。
/**
* 自定義類加載器
*
* @author lijianyu
* @date 2023/04/03 17:54
**/
publicclassMyClassLoaderextendsURLClassLoader{
private Map<String, Class<?>> loadedClasses = new ConcurrentHashMap<>();
public Map<String, Class<?>> getLoadedClasses() {
return loadedClasses;
}
publicMyClassLoader(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 {
// 調(diào)用父類的findClass方法加載指定名稱的類
clazz = super.findClass(name);
// 將加載的類添加到已加載的類集合中
loadedClasses.put(name, clazz);
return clazz;
} catch (ClassNotFoundException e) {
e.printStackTrace();
returnnull;
}
}
publicvoidunload(){
try {
for (Map.Entry<String, Class<?>> entry : loadedClasses.entrySet()) {
// 從已加載的類集合中移除該類
String className = entry.getKey();
loadedClasses.remove(className);
try{
// 調(diào)用該類的destory方法,回收資源
Class<?> clazz = entry.getValue();
Method destory = clazz.getDeclaredMethod("destory");
destory.invoke(clazz);
} catch (Exception e ) {
// 表明該類沒有destory方法
}
}
// 從其父類加載器的加載器層次結(jié)構(gòu)中移除該類加載器
close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
自定義類加載器中,為了方便類的卸載,定義一個(gè)map保存已加載的類信息。key為這個(gè)類的ClassName,value為這個(gè)類的類信息。
同時(shí)定義了類加載器的卸載方法,卸載方法中,將已加載的類的集合中移除該類。由于此類可能使用系統(tǒng)資源或調(diào)用線程,為了避免資源未回收引起的內(nèi)存溢出,通過(guò)反射調(diào)用這個(gè)類中的destroy方法,回收資源。
最后調(diào)用close方法。
2、動(dòng)態(tài)加載
由于此項(xiàng)目使用spring框架,以及xxl-job任務(wù)的機(jī)制調(diào)用動(dòng)態(tài)加載的代碼,因此要完成以下內(nèi)容:
- 將動(dòng)態(tài)加載的jar包讀到內(nèi)存中
- 將有spring注解的類,通過(guò)注解掃描的方式,掃描并手動(dòng)添加到spring容器中。
- 將@XxlJob注解的方法,通過(guò)注解掃描的方式,手動(dòng)添加到xxljob執(zhí)行器中。
/**
* @author lijianyu
* @date 2023/04/29 13:18
**/
@Component
publicclassDynamicLoad{
privatestatic Logger logger = LoggerFactory.getLogger(DynamicLoad.class);
@Autowired
private ApplicationContext applicationContext;
private Map<String, MyClassLoader> myClassLoaderCenter = new ConcurrentHashMap<>();
@Value("${dynamicLoad.path}")
private String path;
/**
* 動(dòng)態(tài)加載指定路徑下指定jar包
* @param path
* @param fileName
* @param isRegistXxlJob 是否需要注冊(cè)xxljob執(zhí)行器,項(xiàng)目首次啟動(dòng)不需要注冊(cè)執(zhí)行器
* @return map<jobHander, Cron> 創(chuàng)建xxljob任務(wù)時(shí)需要的參數(shù)配置
*/
publicvoidloadJar(String path, String fileName, Boolean isRegistXxlJob)throws ClassNotFoundException, InstantiationException, IllegalAccessException {
File file = new File(path +"/" + fileName);
Map<String, String> jobPar = new HashMap<>();
// 獲取beanFactory
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
// 獲取當(dāng)前項(xiàng)目的執(zhí)行器
try {
// URLClassloader加載jar包規(guī)范必須這么寫
URL url = new URL("jar:file:" + file.getAbsolutePath() + "!/");
URLConnection urlConnection = url.openConnection();
JarURLConnection jarURLConnection = (JarURLConnection)urlConnection;
// 獲取jar文件
JarFile jarFile = jarURLConnection.getJarFile();
Enumeration<JarEntry> entries = jarFile.entries();
// 創(chuàng)建自定義類加載器,并加到map中方便管理
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")) {
// 1. 加載類到j(luò)vm中
// 獲取類的全路徑名
String className = jarEntry.getName().replace('/', '.').substring(0, jarEntry.getName().length() - 6);
// 1.1進(jìn)行反射獲取
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();
// 2. 將有@spring注解的類交給spring管理
// 2.1 判斷是否注入spring
Boolean flag = SpringAnnotationUtils.hasSpringAnnotation(clazz);
if(flag){
// 2.2交給spring管理
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
// 此處beanName使用全路徑名是為了防止beanName重復(fù)
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);
// 2.3注冊(cè)到spring的beanFactory中
beanFactory.registerBeanDefinition(beanName, beanDefinition);
// 2.4允許注入和反向注入
beanFactory.autowireBean(clazz);
beanFactory.initializeBean(clazz, beanName);
/*if(Arrays.stream(clazz.getInterfaces()).collect(Collectors.toSet()).contains(InitializingBean.class)){
initBeanClass.add(clazz);
}*/
initBeanClass.add(clazz);
}
// 3. 帶有XxlJob注解的方法注冊(cè)任務(wù)
// 3.1 過(guò)濾方法
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) {
}
// 3.2 生成并注冊(cè)方法的JobHander
for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
Method executeMethod = methodXxlJobEntry.getKey();
// 獲取jobHander和Cron
XxlJobCron xxlJobCron = executeMethod.getAnnotation(XxlJobCron.class);
if(xxlJobCron == null){
thrownew CustomException("500", executeMethod.getName() + "(),沒有添加@XxlJobCron注解配置定時(shí)策略");
}
if (!CronExpression.isValidExpression(xxlJobCron.value())) {
thrownew CustomException("500", executeMethod.getName() + "(),@XxlJobCron參數(shù)內(nèi)容錯(cuò)誤");
}
XxlJob xxlJob = methodXxlJobEntry.getValue();
jobPar.put(xxlJob.value(), xxlJobCron.value());
if (isRegistXxlJob) {
executeMethod.setAccessible(true);
// regist
Method initMethod = null;
Method destroyMethod = null;
xxlJobExecutor.registJobHandler(xxlJob.value(), new CustomerMethodJobHandler(clazz, executeMethod, initMethod, destroyMethod));
}
}
}
// spring bean實(shí)際注冊(cè)
initBeanClass.forEach(beanFactory::getBean);
} catch (IOException e) {
logger.error("讀取{} 文件異常", fileName);
e.printStackTrace();
thrownew RuntimeException("讀取jar文件異常: " + fileName);
}
}
}
以下是判斷該類是否有spring注解的工具類
apublic classSpringAnnotationUtils{
privatestatic Logger logger = LoggerFactory.getLogger(SpringAnnotationUtils.class);
/**
* 判斷一個(gè)類是否有 Spring 核心注解
*
* @param clazz 要檢查的類
* @return true 如果該類上添加了相應(yīng)的 Spring 注解;否則返回 false
*/
publicstaticbooleanhasSpringAnnotation(Class<?> clazz){
if (clazz == null) {
returnfalse;
}
//是否是接口
if (clazz.isInterface()) {
returnfalse;
}
//是否是抽象類
if (Modifier.isAbstract(clazz.getModifiers())) {
returnfalse;
}
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) {
returntrue;
}
}catch (Exception e){
logger.error("出現(xiàn)異常:{}",e.getMessage());
}
returnfalse;
}
}
注冊(cè)xxljob執(zhí)行器的操作是仿照的xxljob中的XxlJobSpringExecutor的注冊(cè)方法。
3、動(dòng)態(tài)卸載
動(dòng)態(tài)卸載的過(guò)程,就是將動(dòng)態(tài)加載的代碼,從內(nèi)存,spring以及xxljob中移除。
代碼如下:
/**
* 動(dòng)態(tài)卸載指定路徑下指定jar包
* @param fileName
* @return map<jobHander, Cron> 創(chuàng)建xxljob任務(wù)時(shí)需要的參數(shù)配置
*/
publicvoidunloadJar(String fileName)throws IllegalAccessException, NoSuchFieldException {
// 獲取加載當(dāng)前jar的類加載器
MyClassLoader myClassLoader = myClassLoaderCenter.get(fileName);
// 獲取jobHandlerRepository私有屬性,為了卸載xxljob任務(wù)
Field privateField = XxlJobExecutor.class.getDeclaredField("jobHandlerRepository");
// 設(shè)置私有屬性可訪問(wèn)
privateField.setAccessible(true);
// 獲取私有屬性的值jobHandlerRepository
XxlJobExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
Map<String, IJobHandler> jobHandlerRepository = (ConcurrentHashMap<String, IJobHandler>) privateField.get(xxlJobSpringExecutor);
// 獲取beanFactory,準(zhǔn)備從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()) {
// 1. 將xxljob任務(wù)從xxljob執(zhí)行器中移除
// 1.1 截取beanName
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);
// 獲取bean,如果獲取失敗,表名這個(gè)類沒有加到spring容器中,則跳出本次循環(huán)
Object bean = null;
try{
bean = applicationContext.getBean(beanName);
}catch (Exception e){
// 異常說(shuō)明spring中沒有這個(gè)bean
continue;
}
// 1.2 過(guò)濾方法
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) {
}
// 1.3 將job從執(zhí)行器中移除
for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
XxlJob xxlJob = methodXxlJobEntry.getValue();
jobHandlerRepository.remove(xxlJob.value());
}
// 2.0從spring中移除,這里的移除是僅僅移除的bean,并未移除bean定義
beanNames.add(beanName);
beanFactory.destroyBean(beanName, bean);
}
// 移除bean定義
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);
// 父類bean定義去除
rootBeanDefinitionMap.remove(beanName);
}
// 卸載父任務(wù),子任務(wù)已經(jīng)在循環(huán)中卸載
jobHandlerRepository.remove(fileName);
// 3.2 從類加載中移除
try {
// 從類加載器底層的classes中移除連接
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 e) {
logger.error("動(dòng)態(tài)卸載的類,從類加載器中卸載失敗");
e.printStackTrace();
} catch (IllegalAccessException e) {
logger.error("動(dòng)態(tài)卸載的類,從類加載器中卸載失敗");
e.printStackTrace();
}
logger.error("{} 動(dòng)態(tài)卸載成功", fileName);
}
4、動(dòng)態(tài)配置
使用動(dòng)態(tài)加載時(shí),為了避免服務(wù)重新啟動(dòng)后丟失已加載的任務(wù)包,使用動(dòng)態(tài)配置的方式,加載后動(dòng)態(tài)更新初始化加載配置。
以下提供了兩種自己實(shí)際操作過(guò)的配置方式。
1.動(dòng)態(tài)修改本地yml
動(dòng)態(tài)修改本地yml配置文件,需要添加snakeyaml的依賴
依賴引入
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.29</version>
</dependency>
工具類
讀取指定路徑下的配置文件,并進(jìn)行修改。
/**
* 用于動(dòng)態(tài)修改bootstrap.yml配置文件
* @author lijianyu
* @date 2023/04/18 17:57
**/
@Component
publicclassConfigUpdater{
publicvoidupdateLoadJars(List<String> jarNames)throws IOException {
// 讀取bootstrap.yml
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);
}
}
2.動(dòng)態(tài)修改nacos配置
Spring Cloud Alibaba Nacos組件完全支持在運(yùn)行時(shí)通過(guò)代碼動(dòng)態(tài)修改配置,還提供了一些API供開發(fā)者在代碼里面實(shí)現(xiàn)動(dòng)態(tài)修改配置。在每次動(dòng)態(tài)加載或卸載數(shù)據(jù)治理任務(wù)jar包時(shí),執(zhí)行成功后都會(huì)進(jìn)行動(dòng)態(tài)更新nacos配置。
@Configuration
publicclassNacosConfig{
@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);
}
}
/**
* nacos配置中,修改sjzl-loadjars.yml
*
* @author lijianyu
* @date 2023/04/19 17:59
**/
@Component
publicclassNacosConfigUtil{
privatestatic 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;
/**
* 從nacos配置文件中,添加初始化jar包配置
* @param jarName 要移除的jar包名
* @throws Exception
*/
publicvoidaddJarName(String jarName)throws Exception {
ConfigService configService = nacosConfig.configService();
String content = configService.getConfig(dataId, group, 5000);
// 修改配置文件內(nèi)容
YAMLMapper yamlMapper = new YAMLMapper();
ObjectMapper jsonMapper = new ObjectMapper();
Object yamlObject = yamlMapper.readValue(content, Object.class);
String jsonString = jsonMapper.writeValueAsString(yamlObject);
JSONObject jsonObject = 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配置更新失敗");
}
}
}
三、分離打包
分離打包時(shí),根據(jù)實(shí)際情況在pom.xml中修改以下配置
<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>
到此這篇關(guān)于springBoot中動(dòng)態(tài)加載jar包的完整指南的文章就介紹到這了,更多相關(guān)springBoot動(dòng)態(tài)加載jar包內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
nacos2.3.0實(shí)現(xiàn)接入pgsql或其他數(shù)據(jù)庫(kù)
這篇文章主要介紹了nacos2.3.0實(shí)現(xiàn)接入pgsql或其他數(shù)據(jù)庫(kù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2026-03-03
flyway實(shí)現(xiàn)java 自動(dòng)升級(jí)SQL腳本的問(wèn)題及解決方法
大家在平時(shí)開發(fā)自己寫SQL語(yǔ)句忘記在所有環(huán)境執(zhí)行,需要新增環(huán)境做數(shù)據(jù)遷移,那么遇到這樣的問(wèn)題該如何解決呢?本文通過(guò)場(chǎng)景分析給大家介紹java 自動(dòng)升級(jí)SQL腳本的策略,感興趣的朋友一起看看吧2021-07-07
使用Java將Excel中的數(shù)字轉(zhuǎn)換為文本或?qū)⑽谋拘蛿?shù)字轉(zhuǎn)換為數(shù)字
在 Excel 中,數(shù)字有時(shí)會(huì)以文本的形式存儲(chǔ),這可能會(huì)影響數(shù)據(jù)的計(jì)算、排序和處理,無(wú)論是將數(shù)字轉(zhuǎn)換為文本,還是將文本型數(shù)字轉(zhuǎn)換為數(shù)字,這兩種操作在處理 Excel 數(shù)據(jù)時(shí)都非常常見且重要,本文將介紹如何使用 Java 完成這兩個(gè)任務(wù),需要的朋友可以參考下2025-12-12
IDEA多Module設(shè)置language level默認(rèn)級(jí)別方式
文章介紹了在Java項(xiàng)目中遇到編譯錯(cuò)誤,源發(fā)行版8需要目標(biāo)發(fā)行版1.8的問(wèn)題,并提供了兩種解決方案:通過(guò)在出現(xiàn)問(wèn)題的module的pom文件中加入特定代碼,或者修改maven的setting.xml文件來(lái)全局設(shè)置languagelevel,作者為解決這個(gè)問(wèn)題提供了個(gè)人經(jīng)驗(yàn),并鼓勵(lì)讀者參考和支持2025-12-12
java如何根據(jù)用戶請(qǐng)求獲取ip地址并解析省市信息
根據(jù)當(dāng)前用戶本地IP地址,查詢IP地址庫(kù),獲取IP所在的省市信息,目前有多種方式實(shí)現(xiàn)該功能,這篇文章主要給大家介紹了關(guān)于java如何根據(jù)用戶請(qǐng)求獲取ip地址并解析省市信息的相關(guān)資料,需要的朋友可以參考下2023-12-12
Java畢業(yè)設(shè)計(jì)實(shí)戰(zhàn)之校園一卡通系統(tǒng)的實(shí)現(xiàn)
這是一個(gè)使用了java+Springboot+Maven+mybatis+Vue+mysql+wd開發(fā)的校園一卡通系統(tǒng),是一個(gè)畢業(yè)設(shè)計(jì)的實(shí)戰(zhàn)練習(xí),具有校園一卡通系統(tǒng)該有的所有功能,感興趣的朋友快來(lái)看看吧2022-01-01

