SpringBoot實現(xiàn)動態(tài)加載外部Jar流程詳解
背景及實現(xiàn)思路
想要設(shè)計一個stater,可以方便加載一個可以完整運行的springboot單體jar包,為了在已執(zhí)行的服務(wù)上面快速的擴展功能而不需要重啟整個服務(wù),又或者低代碼平臺生成代碼之后可以快速預(yù)覽。
加載jar的技術(shù)棧
- springboot 2.2.6.RELEASE
- mybatis-plus 3.4.1
實現(xiàn)加載
想要完成類加載要熟悉spring中類加載機制,以及java中classloader的雙親委派機制。
加載分為兩大步
第一步需要將對應(yīng)的jar中的class文件加載進當前運行內(nèi)存中,第二步則是將對應(yīng)的bean注冊到spring,交由spring管理。
load class
load class主要使用jdk中URLClassLoader工具類,但是這里要注意一點,構(gòu)建classloader時,構(gòu)造函數(shù)可以指定父類加載器,如果指定之后,java才會將兩個classloader加載的同一個class視作類型一致,如果不指定會出現(xiàn) com.demo.A can not cast to com.demo.A這樣的情況。
但是我這里依舊沒有指定父類加載器,原因如下:
- 我要加載的jar都是可以獨立運行的,沒有必須要依賴別的工程的文件
- 我需要可以卸載掉,如果制定了父類加載器,那么會到這這個classloader不能回收,那么該加載器就一直在內(nèi)存中。
加載jar的代碼
/** * 加載jar包 * * @param jarPath jar路徑 * @param packageName 掃面代碼的路徑 * @return */ public boolean loadJar(String jarPath, String packageName) { try { File file = FileUtil.file(jarPath); URLClassLoader classloader = new URLClassLoader(new URL[]{file.toURI().toURL()}, this.applicationContext.getClassLoader()); JarFile jarFile = new JarFile(file); // 獲取jar包下所有的classes String pkgPath = packageName.replace(".", "/"); Enumeration<JarEntry> entries = jarFile.entries(); Class<?> clazz = null; List<JarEntry> xmlJarEntry = new ArrayList<>(); List<String> loadedAliasClasses = new ArrayList<>(); List<String> otherClasses = new ArrayList<>(); // 首先加載model while (entries.hasMoreElements()) { JarEntry jarEntry = entries.nextElement(); String entryName = jarEntry.getName(); if (entryName.charAt(0) == '/') { entryName = entryName.substring(1); } if (entryName.endsWith("Mapper.xml")) { xmlJarEntry.add(jarEntry); } else { if (jarEntry.isDirectory() || !entryName.contains(pkgPath) || !entryName.endsWith(".class")) { continue; } String className = entryName.substring(0, entryName.length() - 6); otherClasses.add(className.replace("/", ".")); log.info("load class : " + className.replace("/", ".")); // 將變量首字母置小寫 String beanName = StringUtils.uncapitalize(className); if (beanName.contains(LoaderConstant.MODEL)) { // 加載所有的class clazz = classloader.loadClass(className.replace("/", ".")); SqlSessionFactory sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class); sqlSessionFactory.getConfiguration().getTypeAliasRegistry().registerAlias(beanName.replace("/", "."), clazz); loadedAliasClasses.add(beanName.replace("/", ".").toLowerCase()); doMap.put(className.replace("/", "."), clazz); } } } // 再加載其他class for (String otherClass : otherClasses) { // 加載所有的class clazz = classloader.loadClass(otherClass.replace("/", ".")); log.info("load class : " + otherClass.replace("/", ".")); // 將變量首字母置小寫 String beanName = StringUtils.uncapitalize(otherClass); if (beanName.endsWith(LoaderConstant.MAPPER)) { mapperMap.put(beanName, clazz); } else if (beanName.endsWith(LoaderConstant.CONTROLLER)) { controllerMap.put(beanName, clazz); } else if (beanName.endsWith(LoaderConstant.SERVICE_IMPL)) { serviceImplMap.put(beanName, clazz); } else if (beanName.endsWith(LoaderConstant.SERVICE)) { serviceMap.put(beanName, clazz); } } // 加載所有XML for (JarEntry jarEntry : xmlJarEntry) { SqlSessionFactory sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class); mybatisXMLLoader.xmlReload(sqlSessionFactory, jarFile, jarEntry, jarEntry.getName()); } Jar jar = new Jar(); jar.setName(jarPath); jar.setJarFile(jarFile); jar.setLoader(classloader); jar.setLoadedAliasClasses(loadedAliasClasses); // 開始加載bean registerBean(jar); registry.registerJar(jarPath, jar); } catch (Exception e) { log.error(e.getLocalizedMessage()); return false; } return true; }
通常bean注冊過程
想要實現(xiàn)熱加載,一定得了解在spring中類的加載機制,大體上spring在掃描到@Component注解的類時,會根據(jù)其class生成對應(yīng)的BeanDefinition,然后在將其注冊在BeanDefinitionRegistry(這是個接口,最終由DefaultListableBeanFactory實現(xiàn))。當其備引用注入實例時即getBean時被實例化并被注冊到DefaultSingletonBeanRegistry中。后續(xù)單例都將由DefaultSingletonBeanRegistry所管理。
controller加載
controller的加載機制
controller所特殊的是,spring會將其注冊到RequestMappingHandlerMapping中。所以想要熱加載controller 就需要三步。
- 生成并注冊BeanDefinition
- 生成并注冊實例注冊
- RequestMappingHandlerMapping
代碼如下
// 獲取bean工廠并轉(zhuǎn)換為DefaultListableBeanFactory DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) ((ConfigurableApplicationContext) applicationContext).getBeanFactory(); // 定義BeanDefinition BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz); GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanDefinitionBuilder.getRawBeanDefinition(); //設(shè)置當前bean定義對象是單利的 beanDefinition.setScope("singleton"); // 將變量首字母置小寫 beanName = StringUtils.uncapitalize(beanName); // 將構(gòu)建的BeanDefinition交由Spring管理 beanFactory.registerBeanDefinition(beanName, beanDefinition); // 手動構(gòu)建實例,并注入base service 防止卸載之后不再生成 Object obj = clazz.newInstance(); beanFactory.registerSingleton(beanName, obj); log.info("register Singleton :" + beanName); final RequestMappingHandlerMapping requestMappingHandlerMapping = applicationContext.getBean(RequestMappingHandlerMapping.class); if (requestMappingHandlerMapping != null) { String handler = beanName; Object controller = null; try { controller = applicationContext.getBean(handler); } catch (Exception e) { e.printStackTrace(); } if (controller == null) { return beanName; } // 注冊Controller Method method = requestMappingHandlerMapping.getClass().getSuperclass().getSuperclass(). getDeclaredMethod("detectHandlerMethods", Object.class); // 將private改為可使用 method.setAccessible(true); method.invoke(requestMappingHandlerMapping, handler); }
關(guān)于IOC
其實只要注冊BeanDefinition之后,你getBean的時候spring會自動幫你完成@Autowired @Resouce 以及構(gòu)造方法的注入,這里我自己完成實例化是想完成一些業(yè)務(wù)上的處理,如自定義注入一些代理類。
關(guān)于AOP
這樣寫有一個弊端就是無法使用AOP,因為AOP是在getBean的時候三層緩存中完成代理的生成的,這里如果你要用這種方式注入可以參考spring源碼,構(gòu)建出來代理類再注入
service加載
service加載我這里直接將service對應(yīng)的實現(xiàn)類實例化再加載進去就可以了,不需要什么特殊的處理,所以這里就不貼代碼了,加載同controller的第一步
mapper加載
mapper的加載時最復(fù)雜的一部分,首先針mapper有兩種,一種是純Mapper接口文件的加載,一種是xml文件的加載。并且你需要分析本身Mybatis是如何加載的,這樣才能完整的降mapper加載到內(nèi)存中。這里我將步驟分解為以下幾步
- 注冊別名(主要是為了XML使用)
- 解析XML文件
- 解析Mapper接口,注冊mapper并注冊
注冊別名
mybatis對于別名的管理是存在SqlSessionFactory的Configuration(這個對象很重要,mybatis加載的資源之類的都在這個對象中管理)對象的TypeAliasRegistry中。TypeAliasRegistry是使用HashMap來維護別名的,這里我們直接調(diào)用registerAliases方法就好
SqlSessionFactory sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class); sqlSessionFactory.getConfiguration().getTypeAliasRegistry().registerAlias(beanName.replace("/", "."), clazz);
解析XML文件
解析XML文件其實比較簡單只要調(diào)用XMLMapperBuilder來解析就好了,XMLMapperBuilder.parse方法會解析XML文件并注冊resultMaps、sqlFragments、mappedStatements。但是這里需要注意一點,那就是你解析的時候需要判斷一下把之前加載的數(shù)據(jù)需要刪除掉,同理resultMaps、sqlFragments、mappedStatements這些數(shù)據(jù)都是在SqlSessionFactory的Configuration中維護的,我們只要通過反射取得這些對象然后修改就可以了,代碼如下
/** * 解析加載XML * * @param sqlSessionFactory * @param jarFile jar對象 * @param jarEntry jar包中的XML對象 * @param name XML名稱 * @throws IOException * @throws NoSuchFieldException * @throws IllegalAccessException */ public void xmlReload(SqlSessionFactory sqlSessionFactory, JarFile jarFile, JarEntry jarEntry, String name) throws IOException, NoSuchFieldException, IllegalAccessException { // 2. 取得Configuration Configuration targetConfiguration = sqlSessionFactory.getConfiguration(); Class<?> aClass = targetConfiguration.getClass(); if (targetConfiguration.getClass().getSimpleName().equals("MybatisConfiguration")) { aClass = Configuration.class; } Set<String> loadedResources = (Set<String>) ObjectUtil.getFieldValue(targetConfiguration, aClass, "loadedResources"); loadedResources.remove(name); // 3. 去掉之前加載的數(shù)據(jù) Map<String, ResultMap> resultMaps = (Map<String, ResultMap>) ObjectUtil.getFieldValue(targetConfiguration, aClass, "resultMaps"); Map<String, XNode> sqlFragmentsMaps = (Map<String, XNode>) ObjectUtil.getFieldValue(targetConfiguration, aClass, "sqlFragments"); Map<String, MappedStatement> mappedStatementMaps = (Map<String, MappedStatement>) ObjectUtil.getFieldValue(targetConfiguration, aClass, "mappedStatements"); XPathParser parser = new XPathParser(jarFile.getInputStream(jarEntry), true, targetConfiguration.getVariables(), new XMLMapperEntityResolver()); XNode mapperXNode = parser.evalNode("/mapper"); List<XNode> resultMapNodes = mapperXNode.evalNodes("/mapper/resultMap"); String namespace = mapperXNode.getStringAttribute("namespace"); for (XNode xNode : resultMapNodes) { String id = xNode.getStringAttribute("id", xNode.getValueBasedIdentifier()); resultMaps.remove(namespace + "." + id); } List<XNode> sqlNodes = mapperXNode.evalNodes("/mapper/sql"); for (XNode sqlNode : sqlNodes) { String id = sqlNode.getStringAttribute("id", sqlNode.getValueBasedIdentifier()); sqlFragmentsMaps.remove(namespace + "." + id); } List<XNode> msNodes = mapperXNode.evalNodes("select|insert|update|delete"); for (XNode msNode : msNodes) { String id = msNode.getStringAttribute("id", msNode.getValueBasedIdentifier()); mappedStatementMaps.remove(namespace + "." + id); } try { // 4. 重新加載和解析被修改的 xml 文件 XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(jarFile.getInputStream(jarEntry), targetConfiguration, name, targetConfiguration.getSqlFragments()); xmlMapperBuilder.parse(); } catch (Exception e) { log.error(e.getMessage(), e); } log.info("Parsed mapper file: '" + name + "'"); }
其他類記載
其他類加載就比較簡單了,直接使用classloader將這些類load進去就好,如果是單例需要被spring管理的則registerBeanDefinition就可以了
到此這篇關(guān)于SpringBoot實現(xiàn)動態(tài)加載外部Jar流程詳解的文章就介紹到這了,更多相關(guān)SpringBoot動態(tài)加載外部Jar內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java 發(fā)送http請求(get、post)的示例
這篇文章主要介紹了Java 發(fā)送http請求的示例,幫助大家更好的理解和使用Java,感興趣的朋友可以了解下2020-10-10IntelliJ?IDEA?2023.2最新版激活方法及驗證ja-netfilter配置是否成功
隨著2023.2版本的發(fā)布,用戶們渴望了解如何激活這個最新版的IDE,本文將介紹三種可行的激活方案,包括許可證服務(wù)器、許可證代碼和idea?vmoptions配置,幫助讀者成功激活并充分利用IDEA的功能,感興趣的朋友參考下吧2023-08-08Java 使用getClass().getResourceAsStream()方法獲取資源
這篇文章主要介紹了Java 使用getClass().getResourceAsStream()方法獲取資源的相關(guān)資料,這里主要講解哪種方式可以獲取到文件資源,需要的朋友可以參考下2017-07-07