springBoot中動態(tài)加載jar包的完整指南
一、概述
1、背景
目前數(shù)據(jù)治理服務中有眾多治理任務,當其中任一治理任務有改動需要升級或新增一個治理任務時,都需要將數(shù)據(jù)治理服務重啟,會影響其他治理任務的正常運行。
2、目標
- 能夠動態(tài)啟動、停止任一治理任務
- 能夠動態(tài)升級、添加治理任務
- 啟動、停止治理任務或升級、添加治理任務不能影響其他任務
3、方案
為了支持業(yè)務代碼盡量的解耦,把部分業(yè)務功能通過動態(tài)加載的方式加載到主程序中,以滿足可插拔式的加載、組合式的部署。
配合xxl-job任務調(diào)度框架,將數(shù)據(jù)治理任務做成xxl-job任務的方式注冊到xxl-job中,方便統(tǒng)一管理。
二、動態(tài)加載
1、自定義類加載器
URLClassLoader 是一種特殊的類加載器,可以從指定的 URL 中加載類和資源。它的主要作用是動態(tài)加載外部的 JAR 包或者類文件,從而實現(xiàn)動態(tài)擴展應用程序的功。為了便于管理動態(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方法
}
}
// 從其父類加載器的加載器層次結構中移除該類加載器
close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
自定義類加載器中,為了方便類的卸載,定義一個map保存已加載的類信息。key為這個類的ClassName,value為這個類的類信息。
同時定義了類加載器的卸載方法,卸載方法中,將已加載的類的集合中移除該類。由于此類可能使用系統(tǒng)資源或調(diào)用線程,為了避免資源未回收引起的內(nèi)存溢出,通過反射調(diào)用這個類中的destroy方法,回收資源。
最后調(diào)用close方法。
2、動態(tài)加載
由于此項目使用spring框架,以及xxl-job任務的機制調(diào)用動態(tài)加載的代碼,因此要完成以下內(nèi)容:
- 將動態(tài)加載的jar包讀到內(nèi)存中
- 將有spring注解的類,通過注解掃描的方式,掃描并手動添加到spring容器中。
- 將@XxlJob注解的方法,通過注解掃描的方式,手動添加到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;
/**
* 動態(tài)加載指定路徑下指定jar包
* @param path
* @param fileName
* @param isRegistXxlJob 是否需要注冊xxljob執(zhí)行器,項目首次啟動不需要注冊執(zhí)行器
* @return map<jobHander, Cron> 創(chuàng)建xxljob任務時需要的參數(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();
// 獲取當前項目的執(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. 加載類到jvm中
// 獲取類的全路徑名
String className = jarEntry.getName().replace('/', '.').substring(0, jarEntry.getName().length() - 6);
// 1.1進行反射獲取
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重復
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注冊到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注解的方法注冊任務
// 3.1 過濾方法
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 生成并注冊方法的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注解配置定時策略");
}
if (!CronExpression.isValidExpression(xxlJobCron.value())) {
thrownew CustomException("500", executeMethod.getName() + "(),@XxlJobCron參數(shù)內(nèi)容錯誤");
}
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實際注冊
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);
/**
* 判斷一個類是否有 Spring 核心注解
*
* @param clazz 要檢查的類
* @return true 如果該類上添加了相應的 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;
}
}
注冊xxljob執(zhí)行器的操作是仿照的xxljob中的XxlJobSpringExecutor的注冊方法。
3、動態(tài)卸載
動態(tài)卸載的過程,就是將動態(tài)加載的代碼,從內(nèi)存,spring以及xxljob中移除。
代碼如下:
/**
* 動態(tài)卸載指定路徑下指定jar包
* @param fileName
* @return map<jobHander, Cron> 創(chuàng)建xxljob任務時需要的參數(shù)配置
*/
publicvoidunloadJar(String fileName)throws IllegalAccessException, NoSuchFieldException {
// 獲取加載當前jar的類加載器
MyClassLoader myClassLoader = myClassLoaderCenter.get(fileName);
// 獲取jobHandlerRepository私有屬性,為了卸載xxljob任務
Field privateField = XxlJobExecutor.class.getDeclaredField("jobHandlerRepository");
// 設置私有屬性可訪問
privateField.setAccessible(true);
// 獲取私有屬性的值jobHandlerRepository
XxlJobExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
Map<String, IJobHandler> jobHandlerRepository = (ConcurrentHashMap<String, IJobHandler>) privateField.get(xxlJobSpringExecutor);
// 獲取beanFactory,準備從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任務從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,如果獲取失敗,表名這個類沒有加到spring容器中,則跳出本次循環(huán)
Object bean = null;
try{
bean = applicationContext.getBean(beanName);
}catch (Exception e){
// 異常說明spring中沒有這個bean
continue;
}
// 1.2 過濾方法
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);
}
// 卸載父任務,子任務已經(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("動態(tài)卸載的類,從類加載器中卸載失敗");
e.printStackTrace();
} catch (IllegalAccessException e) {
logger.error("動態(tài)卸載的類,從類加載器中卸載失敗");
e.printStackTrace();
}
logger.error("{} 動態(tài)卸載成功", fileName);
}
4、動態(tài)配置
使用動態(tài)加載時,為了避免服務重新啟動后丟失已加載的任務包,使用動態(tài)配置的方式,加載后動態(tài)更新初始化加載配置。
以下提供了兩種自己實際操作過的配置方式。
1.動態(tài)修改本地yml
動態(tài)修改本地yml配置文件,需要添加snakeyaml的依賴
依賴引入
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.29</version>
</dependency>
工具類
讀取指定路徑下的配置文件,并進行修改。
/**
* 用于動態(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.動態(tài)修改nacos配置
Spring Cloud Alibaba Nacos組件完全支持在運行時通過代碼動態(tài)修改配置,還提供了一些API供開發(fā)者在代碼里面實現(xiàn)動態(tài)修改配置。在每次動態(tài)加載或卸載數(shù)據(jù)治理任務jar包時,執(zhí)行成功后都會進行動態(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配置更新失敗");
}
}
}
三、分離打包
分離打包時,根據(jù)實際情況在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>
到此這篇關于springBoot中動態(tài)加載jar包的完整指南的文章就介紹到這了,更多相關springBoot動態(tài)加載jar包內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
nacos2.3.0實現(xiàn)接入pgsql或其他數(shù)據(jù)庫
這篇文章主要介紹了nacos2.3.0實現(xiàn)接入pgsql或其他數(shù)據(jù)庫,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2026-03-03
flyway實現(xiàn)java 自動升級SQL腳本的問題及解決方法
大家在平時開發(fā)自己寫SQL語句忘記在所有環(huán)境執(zhí)行,需要新增環(huán)境做數(shù)據(jù)遷移,那么遇到這樣的問題該如何解決呢?本文通過場景分析給大家介紹java 自動升級SQL腳本的策略,感興趣的朋友一起看看吧2021-07-07
使用Java將Excel中的數(shù)字轉換為文本或?qū)⑽谋拘蛿?shù)字轉換為數(shù)字
在 Excel 中,數(shù)字有時會以文本的形式存儲,這可能會影響數(shù)據(jù)的計算、排序和處理,無論是將數(shù)字轉換為文本,還是將文本型數(shù)字轉換為數(shù)字,這兩種操作在處理 Excel 數(shù)據(jù)時都非常常見且重要,本文將介紹如何使用 Java 完成這兩個任務,需要的朋友可以參考下2025-12-12
IDEA多Module設置language level默認級別方式
文章介紹了在Java項目中遇到編譯錯誤,源發(fā)行版8需要目標發(fā)行版1.8的問題,并提供了兩種解決方案:通過在出現(xiàn)問題的module的pom文件中加入特定代碼,或者修改maven的setting.xml文件來全局設置languagelevel,作者為解決這個問題提供了個人經(jīng)驗,并鼓勵讀者參考和支持2025-12-12
Java畢業(yè)設計實戰(zhàn)之校園一卡通系統(tǒng)的實現(xiàn)
這是一個使用了java+Springboot+Maven+mybatis+Vue+mysql+wd開發(fā)的校園一卡通系統(tǒng),是一個畢業(yè)設計的實戰(zhàn)練習,具有校園一卡通系統(tǒng)該有的所有功能,感興趣的朋友快來看看吧2022-01-01

