SpringBoot實(shí)現(xiàn)動態(tài)加載外部Jar流程詳解
背景及實(shí)現(xiàn)思路
想要設(shè)計一個stater,可以方便加載一個可以完整運(yùn)行的springboot單體jar包,為了在已執(zhí)行的服務(wù)上面快速的擴(kuò)展功能而不需要重啟整個服務(wù),又或者低代碼平臺生成代碼之后可以快速預(yù)覽。
加載jar的技術(shù)棧
- springboot 2.2.6.RELEASE
- mybatis-plus 3.4.1
實(shí)現(xiàn)加載
想要完成類加載要熟悉spring中類加載機(jī)制,以及java中classloader的雙親委派機(jī)制。
加載分為兩大步
第一步需要將對應(yīng)的jar中的class文件加載進(jìn)當(dāng)前運(yùn)行內(nèi)存中,第二步則是將對應(yīng)的bean注冊到spring,交由spring管理。
load class
load class主要使用jdk中URLClassLoader工具類,但是這里要注意一點(diǎn),構(gòu)建classloader時,構(gòu)造函數(shù)可以指定父類加載器,如果指定之后,java才會將兩個classloader加載的同一個class視作類型一致,如果不指定會出現(xiàn) com.demo.A can not cast to com.demo.A這樣的情況。
但是我這里依舊沒有指定父類加載器,原因如下:
- 我要加載的jar都是可以獨(dú)立運(yùn)行的,沒有必須要依賴別的工程的文件
- 我需要可以卸載掉,如果制定了父類加載器,那么會到這這個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注冊過程
想要實(shí)現(xiàn)熱加載,一定得了解在spring中類的加載機(jī)制,大體上spring在掃描到@Component注解的類時,會根據(jù)其class生成對應(yīng)的BeanDefinition,然后在將其注冊在BeanDefinitionRegistry(這是個接口,最終由DefaultListableBeanFactory實(shí)現(xiàn))。當(dāng)其備引用注入實(shí)例時即getBean時被實(shí)例化并被注冊到DefaultSingletonBeanRegistry中。后續(xù)單例都將由DefaultSingletonBeanRegistry所管理。
controller加載
controller的加載機(jī)制
controller所特殊的是,spring會將其注冊到RequestMappingHandlerMapping中。所以想要熱加載controller 就需要三步。
- 生成并注冊BeanDefinition
- 生成并注冊實(shí)例注冊
- RequestMappingHandlerMapping
代碼如下
// 獲取bean工廠并轉(zhuǎn)換為DefaultListableBeanFactory
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) ((ConfigurableApplicationContext)
applicationContext).getBeanFactory();
// 定義BeanDefinition
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanDefinitionBuilder.getRawBeanDefinition();
//設(shè)置當(dāng)前bean定義對象是單利的
beanDefinition.setScope("singleton");
// 將變量首字母置小寫
beanName = StringUtils.uncapitalize(beanName);
// 將構(gòu)建的BeanDefinition交由Spring管理
beanFactory.registerBeanDefinition(beanName, beanDefinition);
// 手動構(gòu)建實(shí)例,并注入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
其實(shí)只要注冊BeanDefinition之后,你getBean的時候spring會自動幫你完成@Autowired @Resouce 以及構(gòu)造方法的注入,這里我自己完成實(shí)例化是想完成一些業(yè)務(wù)上的處理,如自定義注入一些代理類。
關(guān)于AOP
這樣寫有一個弊端就是無法使用AOP,因?yàn)锳OP是在getBean的時候三層緩存中完成代理的生成的,這里如果你要用這種方式注入可以參考spring源碼,構(gòu)建出來代理類再注入
service加載
service加載我這里直接將service對應(yīng)的實(shí)現(xiàn)類實(shí)例化再加載進(jìn)去就可以了,不需要什么特殊的處理,所以這里就不貼代碼了,加載同controller的第一步
mapper加載
mapper的加載時最復(fù)雜的一部分,首先針mapper有兩種,一種是純Mapper接口文件的加載,一種是xml文件的加載。并且你需要分析本身Mybatis是如何加載的,這樣才能完整的降mapper加載到內(nèi)存中。這里我將步驟分解為以下幾步
- 注冊別名(主要是為了XML使用)
- 解析XML文件
- 解析Mapper接口,注冊mapper并注冊
注冊別名
mybatis對于別名的管理是存在SqlSessionFactory的Configuration(這個對象很重要,mybatis加載的資源之類的都在這個對象中管理)對象的TypeAliasRegistry中。TypeAliasRegistry是使用HashMap來維護(hù)別名的,這里我們直接調(diào)用registerAliases方法就好
SqlSessionFactory sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class);
sqlSessionFactory.getConfiguration().getTypeAliasRegistry().registerAlias(beanName.replace("/", "."), clazz);解析XML文件
解析XML文件其實(shí)比較簡單只要調(diào)用XMLMapperBuilder來解析就好了,XMLMapperBuilder.parse方法會解析XML文件并注冊resultMaps、sqlFragments、mappedStatements。但是這里需要注意一點(diǎn),那就是你解析的時候需要判斷一下把之前加載的數(shù)據(jù)需要刪除掉,同理resultMaps、sqlFragments、mappedStatements這些數(shù)據(jù)都是在SqlSessionFactory的Configuration中維護(hù)的,我們只要通過反射取得這些對象然后修改就可以了,代碼如下
/**
* 解析加載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進(jìn)去就好,如果是單例需要被spring管理的則registerBeanDefinition就可以了
到此這篇關(guān)于SpringBoot實(shí)現(xiàn)動態(tài)加載外部Jar流程詳解的文章就介紹到這了,更多相關(guān)SpringBoot動態(tài)加載外部Jar內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java IO學(xué)習(xí)之緩沖輸入流(BufferedInputStream)
這篇文章主要介紹了Java IO學(xué)習(xí)之緩沖輸入流(BufferedInputStream)的相關(guān)資料,需要的朋友可以參考下2017-02-02
selenium + ChromeDriver安裝及使用方法
這篇文章主要介紹了selenium + ChromeDriver安裝及使用方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-06-06
Spring Boot 實(shí)現(xiàn)圖片上傳并回顯功能
本篇文章給大家分享Spring Boot 實(shí)現(xiàn)圖片上傳并回顯功能,文中通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2021-07-07

