👉 歡迎 ,你將獲得: 專屬的計畫實戰 / Java 學習路線 / 一對一提問 / 學習打卡 / 贈書福利
新計畫: 仿小紅書 (微服務架構)正在更新中... , 全棧前後端分離部落格計畫 2.0 版本完結啦, 演示連結 : http://116.62.199.48/ 。全程手摸手,後端 + 前端全棧開發,從 0 到 1 講解每個功能點開發步驟,1v1 答疑,直到計畫上線。 目前已更新了255小節,累計40w+字,講解圖:1716張,還在持續爆肝中.. 後續還會上新更多計畫,目標是將Java領域典型的計畫都整一波,如秒殺系統, 線上商城, IM即時通訊,Spring Cloud Alibaba 等等,
一、概述
1、背景
2、目標
3、方案
二、動態載入
1、自訂類載入器
2、動態載入
3、動態解除安裝
4、動態配置
三、分離打包
一、概述
1、背景
目前數據治理服務中有眾多治理任務,當其中任一治理任務有改動需要升級或新增一個治理任務時,都需要將數據治理服務重新開機,會影響其他治理任務的正常執行。
2、目標
能夠動態啟動、停止任一治理任務
能夠動態升級、添加治理任務
啟動、停止治理任務或升級、添加治理任務不能影響其他任務
3、方案
為了支持業務程式碼盡量的解耦,把部份業務功能透過動態載入的方式載入到主程式中,以滿足可插拔式的載入、組合式的部署。
配合xxl-job任務排程框架,將數據治理任務做成xxl-job任務的方式註冊到xxl-job中,方便統一管理。
二、動態載入
1、自訂類載入器
URL classLoader 是一種特殊的類載入器,可以從指定的 URL 中載入類和資源。它的主要作用是動態載入外部的 JAR 包或者類檔,從而實作動態擴充套件應用程式的功。為了便於管理動態載入的jar包,自訂類載入器繼承URL classloader。
package cn.jy.sjzl.util;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URL classLoader;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 自訂類載入器
*
* @author lijianyu
* @date 2023/04/03 17:54
**/
public class My classLoader extends URL classLoader {
private Map<String, class<?>> loaded classes = new ConcurrentHashMap<>();
public Map<String, class<?>> getLoaded classes() {
return loaded classes;
}
public My classLoader(URL[] urls, classLoader parent) {
super(urls, parent);
}
@Override
protected class<?> find class(String name) throws classNotFoundException {
// 從已載入的類集合中獲取指定名稱的類
class<?> clazz = loaded classes.get(name);
if (clazz != null) {
return clazz;
}
try {
// 呼叫父類的find class方法載入指定名稱的類
clazz = super.find class(name);
// 將載入的類添加到已載入的類集合中
loaded classes.put(name, clazz);
return clazz;
} catch ( classNotFoundException e) {
e.printStackTrace();
return null;
}
}
public void unload() {
try {
for (Map.Entry<String, class<?>> entry : loaded classes.entrySet()) {
// 從已載入的類集合中移除該類
String className = entry.getKey();
loaded classes.remove( className);
try{
// 呼叫該類的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為這個類的類資訊。
同時定義了類載入器的解除安裝方法,解除安裝方法中,將已載入的類的集合中移除該類。由於此類可能使用系統資源或呼叫執行緒,為了避免資源未回收引起的記憶體溢位,透過反射呼叫這個類中的destroy方法,回收資源。
最後呼叫close方法。
2、動態載入
由於此計畫使用spring框架,以及xxl-job任務的機制呼叫動態載入的程式碼,因此要完成以下內容
將動態載入的jar包讀到記憶體中
將有spring註解的類,透過註解掃描的方式,掃描並手動添加到spring容器中。
將@XxlJob註解的方法,透過註解掃描的方式,手動添加到xxljob執行器中。
package com.jy.dynamicLoad;
import com.jy.annotation.XxlJobCron;
import com.jy. classLoader.My classLoader;
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;
/**
* @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, My classLoader> my classLoaderCenter = new ConcurrentHashMap<>();
@Value("${dynamicLoad.path}")
private String path;
/**
* 動態載入指定路徑下指定jar包
* @param path
* @param fileName
* @param isRegistXxlJob 是否需要註冊xxljob執行器,計畫首次啟動不需要註冊執行器
* @return map<jobHander, Cron> 建立xxljob任務時需要的參數配置
*/
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<>();
// 獲取beanFactory
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
// 獲取當前計畫的執行器
try {
// URL classloader載入jar包規範必須這麽寫
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();
// 建立自訂類載入器,並加到map中方便管理
My classLoader my classloader = new My classLoader(new URL[] { url }, classLoader.getSystem classLoader());
my classLoaderCenter.put(fileName, my classloader);
Set< class> initBean class = 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進行反射獲取
my classloader.load class( className);
}
}
Map<String, class<?>> loaded classes = my classloader.getLoaded classes();
XxlJobSpringExecutor xxlJobExecutor = new XxlJobSpringExecutor();
for(Map.Entry<String, class<?>> entry : loaded classes.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)){
initBean class.add(clazz);
}*/
initBean class.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){
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);
// regist
Method initMethod = null;
Method destroyMethod = null;
xxlJobExecutor.registJobHandler(xxlJob.value(), new CustomerMethodJobHandler(clazz, executeMethod, initMethod, destroyMethod));
}
}
}
// spring bean實際註冊
initBean class.forEach(beanFactory::getBean);
} catch (IOException e) {
logger.error("讀取{} 檔異常", fileName);
e.printStackTrace();
throw new RuntimeException("讀取jar檔異常: " + fileName);
}
}
}
以下是判斷該類是否有spring註解的工具類
apublic class SpringAnnotationUtils {
private static Logger logger = LoggerFactory.getLogger(SpringAnnotationUtils. class);
/**
* 判斷一個類是否有 Spring 核心註解
*
* @param clazz 要檢查的類
* @returntrue 如果該類上添加了相應的 Spring 註解;否則返回 false
*/
public static boolean hasSpringAnnotation( 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("出現異常:{}",e.getMessage());
}
returnfalse;
}
}
註冊xxljob執行器的操作是仿照的xxljob中的XxlJobSpringExecutor的註冊方法。
3、動態解除安裝
動態解除安裝的過程,就是將動態載入的程式碼,從記憶體,spring以及xxljob中移除。
程式碼如下:
/**
* 動態解除安裝指定路徑下指定jar包
* @param fileName
* @return map<jobHander, Cron> 建立xxljob任務時需要的參數配置
*/
public void unloadJar(String fileName) throws IllegalAccessException, NoSuchFieldException {
// 獲取載入當前jar的類載入器
My classLoader my classLoader = my classLoaderCenter.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<?>> loaded classes = my classLoader.getLoaded classes();
Set<String> beanNames = new HashSet<>();
for (Map.Entry<String, class<?>> entry: loaded classes.entrySet()) {
// 1. 將xxljob任務從xxljob執行器中移除
// 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容器中,則跳出本次迴圈
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.get class(),
new MethodIntrospector.MetadataLookup<XxlJob>() {
@Override
public XxlJob inspect(Method method) {
return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob. class);
}
});
} catch (Throwable ex) {
}
// 1.3 將job從執行器中移除
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.get class()
.getSuper class()
.getSuper class().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);
}
// 解除安裝父任務,子任務已經在迴圈中解除安裝
jobHandlerRepository.remove(fileName);
// 3.2 從類載入中移除
try {
// 從類載入器底層的 classes中移除連線
Field field = classLoader. class.getDeclaredField(" classes");
field.setAccessible(true);
Vector< class<?>> classes = (Vector< class<?>>) field.get(my classLoader);
classes.removeAllElements();
// 移除類載入器的參照
my classLoaderCenter.remove(fileName);
// 解除安裝類載入器
my classLoader.unload();
} catch (NoSuchFieldException e) {
logger.error("動態解除安裝的類,從類載入器中解除安裝失敗");
e.printStackTrace();
} catch (IllegalAccessException e) {
logger.error("動態解除安裝的類,從類載入器中解除安裝失敗");
e.printStackTrace();
}
logger.error("{} 動態解除安裝成功", fileName);
}
4、動態配置
使用動態載入時,為了避免服務重新啟動後遺失已載入的任務包,使用動態配置的方式,載入後動態更新初始化載入配置。
以下提供了兩種自己實際操作過的配置方式。
4.1 動態修改本地yml
動態修改本地yml配置檔,需要添加snakeyaml的依賴
4.1.1 依賴引入
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.29</version>
</dependency>
4.1.2 工具類
讀取指定路徑下的配置檔,並進行修改。
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.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 用於動態修改bootstrap.yml配置檔
* @author lijianyu
* @date 2023/04/18 17:57
**/
@Component
public class ConfigUpdater {
public void updateLoadJars(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.setDefaultFlow style(DumperOptions.Flow style.BLOCK);
options.setPrettyFlow(true);
Yaml yamlWriter = new Yaml(options);
yamlWriter.dump(obj, writer);
}
}
4.2 動態修改nacos配置
Spring Cloud Alibaba Nacos元件完全支持在執行時透過程式碼動態修改配置,還提供了一些API供開發者在程式碼裏面實作動態修改配置。在每次動態載入或解除安裝數據治理任務jar包時,執行成功後都會進行動態更新nacos配置。
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 cn.jy.sjzl.config.NacosConfig;
import com.alibaba.fastjson.JSONObject;
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.stream.Collectors;
/**
* nacos配置中,修改sjzl-loadjars.yml
*
* @author lijianyu
* @date 2023/04/19 17:59
**/
@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;
/**
* 從nacos配置檔中,添加初始化jar包配置
* @param jarName 要移除的jar包名
* @throws Exception
*/
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);
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配置更新失敗");
}
}
}
三、分離打包
分離打包時,根據實際情況在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>
👉 歡迎 ,你將獲得: 專屬的計畫實戰 / Java 學習路線 / 一對一提問 / 學習打卡 / 贈書福利
新計畫: 仿小紅書 (微服務架構)正在更新中... , 全棧前後端分離部落格計畫 2.0 版本完結啦, 演示連結 : http://116.62.199.48/ 。全程手摸手,後端 + 前端全棧開發,從 0 到 1 講解每個功能點開發步驟,1v1 答疑,直到計畫上線。 目前已更新了255小節,累計40w+字,講解圖:1716張,還在持續爆肝中.. 後續還會上新更多計畫,目標是將Java領域典型的計畫都整一波,如秒殺系統, 線上商城, IM即時通訊,Spring Cloud Alibaba 等等,
1.
2.
3.
4.
最近面試BAT,整理一份面試資料【Java面試BATJ通關手冊】,覆蓋了Java核心技術、JVM、Java並行、SSM、微服務、資料庫、數據結構等等。
獲取方式:點「在看」,關註公眾號並回復 Java 領取,更多內容陸續奉上。
PS:因公眾號平台更改了推播規則,如果不想錯過內容,記得讀完點一下「在看」,加個「星標」,這樣每次新文章推播才會第一時間出現在你的訂閱列表裏。
點「在看」支持小哈呀,謝謝啦