springboot動態(tài)加載jar包動態(tài)配置實(shí)例詳解
一、概述
1、背景
? 目前數(shù)據(jù)治理服務(wù)中有眾多治理任務(wù),當(dāng)其中任一治理任務(wù)有改動需要升級或新增一個(gè)治理任務(wù)時(shí),都需要將數(shù)據(jù)治理服務(wù)重啟,會影響其他治理任務(wù)的正常運(yùn)行。
2、目標(biāo)
- 能夠動態(tài)啟動、停止任一治理任務(wù)
- 能夠動態(tài)升級、添加治理任務(wù)
- 啟動、停止治理任務(wù)或升級、添加治理任務(wù)不能影響其他任務(wù)
3、方案
- 為了支持業(yè)務(wù)代碼盡量的解耦,把部分業(yè)務(wù)功能通過動態(tài)加載的方式加載到主程序中,以滿足可插拔式的加載、組合式的部署。
- 配合xxl-job任務(wù)調(diào)度框架,將數(shù)據(jù)治理任務(wù)做成xxl-job任務(wù)的方式注冊到xxl-job中,方便統(tǒng)一管理。
二、動態(tài)加載
1、自定義類加載器
URLClassLoader 是一種特殊的類加載器,可以從指定的 URL 中加載類和資源。它的主要作用是動態(tài)加載外部的 JAR 包或者類文件,從而實(shí)現(xiàn)動態(tài)擴(kuò)展應(yīng)用程序的功。為了便于管理動態(tài)加載的jar包,自定義類加載器繼承URLClassloader。
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; /** * 自定義類加載器 * * @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 { // 調(diào)用父類的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{ // 調(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)存溢出,通過反射調(diào)用這個(gè)類中的destroy方法,回收資源。
- 最后調(diào)用close方法。
2、動態(tài)加載
由于此項(xiàng)目使用spring框架,以及xxl-job任務(wù)的機(jī)制調(diào)用動態(tài)加載的代碼,因此要完成以下內(nèi)容
- 將動態(tài)加載的jar包讀到內(nèi)存中
- 將有spring注解的類,通過注解掃描的方式,掃描并手動添加到spring容器中。
- 將@XxlJob注解的方法,通過注解掃描的方式,手動添加到xxljob執(zhí)行器中。
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; /** * @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; /** * 動態(tài)加載指定路徑下指定jar包 * @param path * @param fileName * @param isRegistXxlJob 是否需要注冊xxljob執(zhí)行器,項(xiàng)目首次啟動不需要注冊執(zhí)行器 * @return map<jobHander, Cron> 創(chuàng)建xxljob任務(wù)時(shí)需要的參數(shù)配置 */ 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(); // 獲取當(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注冊到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注解的方法注冊任務(wù) // 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注解配置定時(shí)策略"); } if (!CronExpression.isValidExpression(xxlJobCron.value())) { throw new 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í)際注冊 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); /** * 判斷一個(gè)類是否有 Spring 核心注解 * * @param clazz 要檢查的類 * @return true 如果該類上添加了相應(yīng)的 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("出現(xiàn)異常:{}",e.getMessage()); } return false; } }
注冊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任務(wù)時(shí)需要的參數(shù)配置 */ public void unloadJar(String fileName) throws IllegalAccessException, NoSuchFieldException { // 獲取加載當(dāng)前jar的類加載器 MyClassLoader myClassLoader = myClassLoaderCenter.get(fileName); // 獲取jobHandlerRepository私有屬性,為了卸載xxljob任務(wù) Field privateField = XxlJobExecutor.class.getDeclaredField("jobHandlerRepository"); // 設(shè)置私有屬性可訪問 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){ // 異常說明spring中沒有這個(gè)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); } // 卸載父任務(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("動態(tài)卸載的類,從類加載器中卸載失敗"); e.printStackTrace(); } catch (IllegalAccessException e) { logger.error("動態(tài)卸載的類,從類加載器中卸載失敗"); e.printStackTrace(); } logger.error("{} 動態(tài)卸載成功", fileName); }
4、動態(tài)配置
使用動態(tài)加載時(shí),為了避免服務(wù)重新啟動后丟失已加載的任務(wù)包,使用動態(tài)配置的方式,加載后動態(tài)更新初始化加載配置。
以下提供了兩種自己實(shí)際操作過的配置方式。
4.1 動態(tài)修改本地yml
動態(tài)修改本地yml配置文件,需要添加snakeyaml的依賴
4.1.1 依賴引入
<dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> <version>1.29</version> </dependency>
4.1.2 工具類
讀取指定路徑下的配置文件,并進(jìn)行修改。
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; /** * 用于動態(tài)修改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 動態(tài)修改nacos配置
Spring Cloud Alibaba Nacos組件完全支持在運(yùn)行時(shí)通過代碼動態(tài)修改配置,還提供了一些API供開發(fā)者在代碼里面實(shí)現(xiàn)動態(tài)修改配置。在每次動態(tài)加載或卸載數(shù)據(jù)治理任務(wù)jar包時(shí),執(zhí)行成功后都會進(jìn)行動態(tài)更新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); // 修改配置文件內(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>
總結(jié)
到此這篇關(guān)于springboot動態(tài)加載jar包動態(tài)配置的文章就介紹到這了,更多相關(guān)springboot動態(tài)加載jar包內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springBoot動態(tài)加載jar及如何將類注冊到IOC
在SpringBoot項(xiàng)目中動態(tài)加載jar文件并將其類注冊到IOC容器是一種高級應(yīng)用方式,,這種方法為SpringBoot項(xiàng)目提供了更靈活的擴(kuò)展能力,使得項(xiàng)目可以在不修改原有代碼的基礎(chǔ)上增加新的功能模塊,感興趣的朋友一起看看吧2024-11-11springcloud gateway自定義斷言規(guī)則詳解,以后綴結(jié)尾進(jìn)行路由
這篇文章主要介紹了springcloud gateway自定義斷言規(guī)則詳解,以后綴結(jié)尾進(jìn)行路由,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10Java使用jacob將微軟office中word、excel、ppt轉(zhuǎn)成pdf
這篇文章主要為大家詳細(xì)介紹了Java使用jacob將微軟office中word、excel、ppt轉(zhuǎn)成pdf,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12IDEA如何將Java項(xiàng)目打包成可執(zhí)行的Jar包
在Java開發(fā)中,我們通常會將我們的項(xiàng)目打包成可執(zhí)行的Jar包,以便于在其他環(huán)境中部署和運(yùn)行,本文將介紹如何使用IDEA集成開發(fā)環(huán)境將Java項(xiàng)目打包成可執(zhí)行的Jar包,感興趣的朋友一起看看吧2023-07-07java的jdk基礎(chǔ)知識點(diǎn)總結(jié)
在本篇文章里小編給大家整理的是一篇關(guān)于java的jdk基礎(chǔ)知識點(diǎn)總結(jié)內(nèi)容,有興趣的朋友們可以學(xué)習(xí)參考下。2021-01-01基于hibernate實(shí)現(xiàn)的分頁技術(shù)實(shí)例分析
這篇文章主要介紹了基于hibernate實(shí)現(xiàn)的分頁技術(shù),結(jié)合實(shí)例形式分析了Hibernate分頁技術(shù)的原理,實(shí)現(xiàn)步驟與相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2016-03-03關(guān)于Java Guava ImmutableMap不可變集合源碼分析
這篇文章主要介紹Java Guava不可變集合ImmutableMap的源碼分析的相關(guān)資料,需要的朋友可以參考下面具體的文章內(nèi)容2021-09-09