SpringBoot实现动态加载外部Jar流程详解
作者:加班狂魔
背景及实现思路
想要设计一个stater,可以方便加载一个可以完整运行的springboot单体jar包,为了在已执行的服务上面快速的扩展功能而不需要重启整个服务,又或者低代码平台生成代码之后可以快速预览。
加载jar的技术栈
- springboot 2.2.6.RELEASE
- mybatis-plus 3.4.1
实现加载
想要完成类加载要熟悉spring中类加载机制,以及java中classloader的双亲委派机制。
加载分为两大步
第一步需要将对应的jar中的class文件加载进当前运行内存中,第二步则是将对应的bean注册到spring,交由spring管理。
load class
load class主要使用jdk中URLClassLoader工具类,但是这里要注意一点,构建classloader时,构造函数可以指定父类加载器,如果指定之后,java才会将两个classloader加载的同一个class视作类型一致,如果不指定会出现 com.demo.A can not cast to com.demo.A这样的情况。
但是我这里依旧没有指定父类加载器,原因如下:
- 我要加载的jar都是可以独立运行的,没有必须要依赖别的工程的文件
- 我需要可以卸载掉,如果制定了父类加载器,那么会到这这个classloader不能回收,那么该加载器就一直在内存中。
加载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注册过程
想要实现热加载,一定得了解在spring中类的加载机制,大体上spring在扫描到@Component注解的类时,会根据其class生成对应的BeanDefinition,然后在将其注册在BeanDefinitionRegistry(这是个接口,最终由DefaultListableBeanFactory实现)。当其备引用注入实例时即getBean时被实例化并被注册到DefaultSingletonBeanRegistry中。后续单例都将由DefaultSingletonBeanRegistry所管理。
controller加载
controller的加载机制
controller所特殊的是,spring会将其注册到RequestMappingHandlerMapping中。所以想要热加载controller 就需要三步。
- 生成并注册BeanDefinition
- 生成并注册实例注册
- RequestMappingHandlerMapping
代码如下
// 获取bean工厂并转换为DefaultListableBeanFactory DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) ((ConfigurableApplicationContext) applicationContext).getBeanFactory(); // 定义BeanDefinition BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz); GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanDefinitionBuilder.getRawBeanDefinition(); //设置当前bean定义对象是单利的 beanDefinition.setScope("singleton"); // 将变量首字母置小写 beanName = StringUtils.uncapitalize(beanName); // 将构建的BeanDefinition交由Spring管理 beanFactory.registerBeanDefinition(beanName, beanDefinition); // 手动构建实例,并注入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); }
关于IOC
其实只要注册BeanDefinition之后,你getBean的时候spring会自动帮你完成@Autowired @Resouce 以及构造方法的注入,这里我自己完成实例化是想完成一些业务上的处理,如自定义注入一些代理类。
关于AOP
这样写有一个弊端就是无法使用AOP,因为AOP是在getBean的时候三层缓存中完成代理的生成的,这里如果你要用这种方式注入可以参考spring源码,构建出来代理类再注入
service加载
service加载我这里直接将service对应的实现类实例化再加载进去就可以了,不需要什么特殊的处理,所以这里就不贴代码了,加载同controller的第一步
mapper加载
mapper的加载时最复杂的一部分,首先针mapper有两种,一种是纯Mapper接口文件的加载,一种是xml文件的加载。并且你需要分析本身Mybatis是如何加载的,这样才能完整的降mapper加载到内存中。这里我将步骤分解为以下几步
- 注册别名(主要是为了XML使用)
- 解析XML文件
- 解析Mapper接口,注册mapper并注册
注册别名
mybatis对于别名的管理是存在SqlSessionFactory的Configuration(这个对象很重要,mybatis加载的资源之类的都在这个对象中管理)对象的TypeAliasRegistry中。TypeAliasRegistry是使用HashMap来维护别名的,这里我们直接调用registerAliases方法就好
SqlSessionFactory sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class); sqlSessionFactory.getConfiguration().getTypeAliasRegistry().registerAlias(beanName.replace("/", "."), clazz);
解析XML文件
解析XML文件其实比较简单只要调用XMLMapperBuilder来解析就好了,XMLMapperBuilder.parse方法会解析XML文件并注册resultMaps、sqlFragments、mappedStatements。但是这里需要注意一点,那就是你解析的时候需要判断一下把之前加载的数据需要删除掉,同理resultMaps、sqlFragments、mappedStatements这些数据都是在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. 去掉之前加载的数据 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就可以了
到此这篇关于SpringBoot实现动态加载外部Jar流程详解的文章就介绍到这了,更多相关SpringBoot动态加载外部Jar内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!