一、概述1、背景目前數據治理服務中有眾多治理任務,當其中任一治理任務有改動需要升級或新增一個治理任務時,都需要將數據治理服務重啟,會影響其他治理任務的正常運行。 2、目標- 啟動、停止治理任務或升級、添加治理任務不能影響其他任務
3、方案- 為了支持業務代碼盡量的解耦,把部分業務功能通過動態加載的方式加載到主程序中,以滿足可插拔式的加載、組合式的部署。
- 配合xxl-job任務調度框架,將數據治理任務做成xxl-job任務的方式注冊到xxl-job中,方便統一管理。
二、動態加載1、自定義類加載器URLClassLoader 是一種特殊的類加載器,可以從指定的 URL 中加載類和資源。它的主要作用是動態加載外部的 JAR 包或者類文件,從而實現動態擴展應用程序的功。為了便于管理動態加載的jar包,自定義類加載器繼承URLClassloader。 /** * 自定義類加載器 * * @author lijianyu * @date 2023/04/03 17:54 **/ 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 { // 調用父類的findClass方法加載指定名稱的類 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{ // 調用該類的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方法,回收資源。
2、動態加載由于此項目使用spring框架,以及xxl-job任務的機制調用動態加載的代碼,因此要完成以下內容 - 將有spring注解的類,通過注解掃描的方式,掃描并手動添加到spring容器中。
- 將@XxlJob注解的方法,通過注解掃描的方式,手動添加到xxljob執行器中。
/** * @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, MyClassLoader> myClassLoaderCenter = 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 { // URLClassloader加載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中方便管理 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){ 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實際注冊 initBeanClass.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 要檢查的類 * @return true 如果該類上添加了相應的 Spring 注解;否則返回 false */ 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; } }
注冊xxljob執行器的操作是仿照的xxljob中的XxlJobSpringExecutor的注冊方法。 3、動態卸載動態卸載的過程,就是將動態加載的代碼,從內存,spring以及xxljob中移除。 代碼如下: /** * 動態卸載指定路徑下指定jar包 * @param fileName * @return map<jobHander, Cron> 創建xxljob任務時需要的參數配置 */ public void unloadJar(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執行器中移除 // 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.getClass(), 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.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); }
// 卸載父任務,子任務已經在循環中卸載 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("動態卸載的類,從類加載器中卸載失敗"); 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 工具類 讀取指定路徑下的配置文件,并進行修改。 /** * 用于動態修改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.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); options.setPrettyFlow(true); Yaml yamlWriter = new Yaml(options); yamlWriter.dump(obj, writer); } }
4.2 動態修改nacos配置Spring Cloud Alibaba Nacos組件完全支持在運行時通過代碼動態修改配置,還提供了一些API供開發者在代碼里面實現動態修改配置。在每次動態加載或卸載數據治理任務jar包時,執行成功后都會進行動態更新nacos配置。 @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); } } /** * 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>
PS:防止找不到本篇文章,可以收藏點贊,方便翻閱查找哦。
|