SpringBoot应用项目插件开发☞Jar包热更新

模块化开发---实现模块的动态加载与卸载

在工作中,由于我是主要负责直播APP的运营活动开发,这些活动代码有几个特性

  1. 活动周期短,通常只是一个节日、一个星期、十天、一个月等,所以导致代码用于运行的时间短,活动下线代码就废弃了。
  2. 活动规则总是根据收益和效果频繁变化,所以导致代码频繁修改和部署上线。
  3. 活动小而多,导致开发快上线多。
  4. 活动最好支持新旧版本同时在线,以便新版开放之前旧版正常使用。
  5. 活动之间没有联系,但是会有一些共同服务依赖,比如获取用户信息、发放奖励、推送提醒等。

针对这些,会带来两个问题

  • 代码只用一段时间,但类还是会被JVM加载且Spring管理的bean无法回收,占用内存。
  • 频繁修改代码和新的活动上线导致部署次数过多,可能影响其它活动,耗时并且不够灵活。

有什么好的办法可以解决呢?最先想到的是使用配置+Conditional来控制Spring是否注册Bean,但是效果不是很好。

如果能够把每个活动的代码单独打成一个jar包,在主项目运行时直接加载到JVM进行使用,并且可以注入其它的SpringBean,等到活动下线时就把它从JVM卸载,岂不美哉。

1、动态模块实现原理

Java的自定义ClassLoader和Spring的父子ApplicationContext就提供了这样的功能。

流程图:

动态模块流程图

1、新建Spring Boot项目test-project,分成两个module,把共享给模块的代码放到project-api。

2、project-server模块引入dynamic-module依赖,启动类加上自动配置注解并指定jar包存放目录。

3、开发模块test-module,引入dynamic-module和project-api,重写ModuleConfig类进行配置,并将类名写入META-INF/services/cn.zhh.dynamic_module.ModuleConfig文件提供SPI加载。同时实现Handler接口声明Spring组件。

4、将test-module打成jar包,使用dynamic-module提供的HTTP接口上传。

5、上传成功后dynamic-module根据ModuleConfig子类和Handler实现完成Class加载和Bean注册,生成对应的Module对象和Handler对象并管理起来。

6、project-server可以根据moduleName和handlerName调用module的handler了。

7、使用dynamic-module提供的HTTP接口查询模块信息或者卸载模块。

8、project-server重启会触发jar包存放目录下的所有模块自动加载注册。

下面看下核心的自定义ClassLoader类和自定义ApplicationContext类以及动态加载卸载过程。

完整代码和使用案例都可以在github找到:https://github.com/zhouhuanghua/dynamic-module

2、自定义ClassLoader

这里扩展了一个功能,默认的ClassLoader.loadClass方法是双亲委派模式,我们对它进行覆盖,针对配置的指定类可以直接自己加载,这样每个模块就可以使用不同版本相同名字的Class了。

 
  1. @Slf4j
  2. class ModuleClassLoader extends URLClassLoader {
  3.  
  4. public static final String[] DEFAULT_EXCLUDED_PACKAGES = new String[]{"java.", "javax.", "sun.", "oracle."};
  5.  
  6. private final Set<String> excludedPackages;
  7.  
  8. private final Set<String> overridePackages;
  9.  
  10. public ModuleClassLoader(URL url, ClassLoader parent) {
  11. super(new URL[]{url}, parent);
  12. this.excludedPackages = Sets.newHashSet(Arrays.asList(DEFAULT_EXCLUDED_PACKAGES.clone()));
  13. this.overridePackages = Sets.newHashSet();
  14. }
  15.  
  16. public void addExcludedPackages(Set<String> excludedPackages) {
  17. this.excludedPackages.addAll(excludedPackages);
  18. }
  19.  
  20. public void addOverridePackages(Set<String> overridePackages) {
  21. this.overridePackages.addAll(overridePackages);
  22. }
  23.  
  24. @Override
  25. protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
  26. Class<?> result = null;
  27. synchronized (ModuleClassLoader.class) {
  28. if (isEligibleForOverriding(name)) {
  29. if (log.isInfoEnabled()) {
  30. log.info("Load class for overriding: {}", name);
  31. }
  32. result = loadClassForOverriding(name);
  33. }
  34. if (Objects.nonNull(result)) {
  35. // 链接类
  36. if (resolve) {
  37. resolveClass(result);
  38. }
  39. return result;
  40. }
  41. }
  42. // 使用默认类加载方式
  43. return super.loadClass(name, resolve);
  44. }
  45.  
  46. private Class<?> loadClassForOverriding(String name) throws ClassNotFoundException {
  47. // 查找已加载的类
  48. Class<?> result = findLoadedClass(name);
  49. if (Objects.isNull(result)) {
  50. // 加载类
  51. result = findClass(name);
  52. }
  53. return result;
  54. }
  55.  
  56. private boolean isEligibleForOverriding(final String name) {
  57. checkNotNull(name, "name is null");
  58. return !isExcluded(name) && any(overridePackages, name::startsWith);
  59. }
  60.  
  61. protected boolean isExcluded(String className) {
  62. checkNotNull(className, "className is null");
  63. for (String packageName : this.excludedPackages) {
  64. if (className.startsWith(packageName)) {
  65. return true;
  66. }
  67. }
  68. return false;
  69. }
  70.  
  71. }
 

3、自定义ApplicationContext

使用基于注解配置的方式。暂时不需要扩展其它功能。

 
  1. class ModuleApplicationContext extends AnnotationConfigApplicationContext {
  2.  
  3. }
 

4、动态加载

创建ModuleClassLoader,指定jar包路径和父加载器。

采用SPI的方式读取ModuleConfig数据。

创建ModuleApplicationContext,指定父上下文、类加载器、扫描包路径,执行refresh。

 
  1. @Slf4j
  2. class ModuleLoader implements ApplicationContextAware {
  3.  
  4. /**
  5. * 注入父applicationContext
  6. */
  7. @Setter
  8. private ApplicationContext applicationContext;
  9.  
  10. /**
  11. * 加载模块
  12. *
  13. * @param jarPath jar包路径
  14. * @return Module
  15. */
  16. public Module load(Path jarPath) {
  17. if (log.isInfoEnabled()) {
  18. log.info("Start to load module: {}", jarPath);
  19. }
  20. ModuleClassLoader moduleClassLoader;
  21. try {
  22. moduleClassLoader = new ModuleClassLoader(jarPath.toUri().toURL(), applicationContext.getClassLoader());
  23. } catch (MalformedURLException e) {
  24. throw new ModuleRuntimeException("create ModuleClassLoader exception", e);
  25. }
  26. List<ModuleConfig> moduleConfigList = new ArrayList<>();
  27. ServiceLoader.load(ModuleConfig.class, moduleClassLoader).forEach(moduleConfigList::add);
  28. if (moduleConfigList.size() != 1) {
  29. throw new ModuleRuntimeException("module config has and only has one");
  30. }
  31. ModuleConfig moduleConfig = moduleConfigList.get(0);
  32. moduleClassLoader.addOverridePackages(moduleConfig.overridePackages());
  33. ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
  34. try {
  35. // 把当前线程的ClassLoader切换成模块的
  36. Thread.currentThread().setContextClassLoader(moduleClassLoader);
  37. ModuleApplicationContext moduleApplicationContext = new ModuleApplicationContext();
  38. moduleApplicationContext.setParent(applicationContext);
  39. moduleApplicationContext.setClassLoader(moduleClassLoader);
  40. moduleApplicationContext.scan(moduleConfig.scanPackages().toArray(new String[0]));
  41. moduleApplicationContext.refresh();
  42. if (log.isInfoEnabled()) {
  43. log.info("Load module success: name={}, version={}, jarPath={}", moduleConfig.name(), moduleConfig.version(), jarPath);
  44. }
  45. return new Module(jarPath, moduleConfig, moduleApplicationContext);
  46. } catch (Throwable e) {
  47. log.error(String.format("Load module exception, jarPath=%s", jarPath), e);
  48. CachedIntrospectionResults.clearClassLoader(moduleClassLoader);
  49. throw new ModuleRuntimeException("create ModuleApplicationContext exception", e);
  50. } finally {
  51. // 还原当前线程的ClassLoader
  52. Thread.currentThread().setContextClassLoader(currentClassLoader);
  53. }
  54. }
  55.  
  56. }
 

生成Module对象后,还要收集它的Handler并管理起来

 
  1. private Map<String, Handler> scanHandlers() {
  2. Map<String, Handler> handlers = Maps.newHashMap();
  3. // find Handler in module
  4. for (Handler handler : moduleApplicationContext.getBeansOfType(Handler.class).values()) {
  5. String handlerName = handler.name();
  6. if (!StringUtils.hasText(handlerName)) {
  7. throw new ModuleRuntimeException("scanHandlers handlerName is null");
  8. }
  9. checkState(!handlers.containsKey(handlerName), "Duplicated handler %s found by: %s",
  10. Handler.class.getSimpleName(), handlerName);
  11. handlers.put(handlerName, handler);
  12. }
  13. if (log.isInfoEnabled()) {
  14. log.info("Scan handlers finish: {}", String.join(",", handlers.keySet()));
  15. }
  16. return ImmutableMap.copyOf(handlers);
  17. }
 

5、动态卸载

  • 关闭ApplicationContext。
  • 清除ClassLoader并关闭(Class卸载的条件很苛刻,这个需要多加监控)。
  • 删除jar文件。
 
  1. public void destroy() throws Exception {
  2. if (log.isInfoEnabled()) {
  3. log.info("Destroy module: name={}, version={}", moduleConfig.name(), moduleConfig.version());
  4. }
  5. // close spring context
  6. closeApplicationContext(moduleApplicationContext);
  7. // clean class loader
  8. clearClassLoader(moduleApplicationContext.getClassLoader());
  9. // delete jar file
  10. Files.deleteIfExists(jarPath);
  11. }
  12.  
  13. private void closeApplicationContext(ConfigurableApplicationContext applicationContext) {
  14. checkNotNull(applicationContext, "applicationContext is null");
  15. try {
  16. applicationContext.close();
  17. } catch (Exception e) {
  18. log.error("Failed to close application context", e);
  19. }
  20. }
  21.  
  22. private void clearClassLoader(ClassLoader classLoader) throws IOException {
  23. checkNotNull(classLoader, "classLoader is null");
  24. // Introspector缓存BeanInfo类来获得更好的性能。卸载时刷新所有Introspector的内部缓存。
  25. Introspector.flushCaches();
  26. // 从已经使用给定类加载器加载的缓存中移除所有资源包
  27. ResourceBundle.clearCache(classLoader);
  28. // clear the introspection cache for the given ClassLoader
  29. CachedIntrospectionResults.clearClassLoader(classLoader);
  30. // close
  31. if (classLoader instanceof URLClassLoader) {
  32. ((URLClassLoader) classLoader).close();
  33. }
  34. }
 

先写这么多吧。。。主要是思路。

完整代码和使用案例都可以在github找到:https://github.com/zhouhuanghua/dynamic-module

附上dynamic-module的类图

dynamic-module

 

SpringBoot应用项目插件开发☞Jar包热更新

 

一、应用场景

        你参与开发的项目已经部署到Tomcat中对外发布了,项目中有一个支付功能,你默认走的是微信支付,假如你有好好地进行系统软件设计的话,那这个支付功能必然不是面向某个具体应用而实现的,而应该是面向抽象(面向接口编程)。也就是支付功能被抽取到了统一的接口中,微信支付实现该接口的具体做法就是调用微信支付接口,支付宝同理,其他支付实现也一样;

       这样一来,系统在现有微信支付功能上就可以很容易的扩展出支付宝支付的功能了。话又说回来了,假如我们开发了一个jar包,这个jar包功能很简单,就是支付宝支付功能的实现,那我们要怎么快速升级系统功能呢?以下有两种可选方式:

1、主应用(模块)pom中引入这个jar包的依赖,且这个jar包做成starter(springboot自动装配启动包,springboot3.0之前都是在META-INF/spring.factories中进行配置,SPI的高级扩展),使用这种方式不好的地方就是你需要定制你的jar包,而且主应用中还要进行依赖,完事后你需要重新编译打包,打完包后还需要重新部署,总之就是很繁琐(这里不涉及DevOps,如果是CI/CD模式,这篇文章可以不用往下看了,因为大部分项目都是独立给用户部署的,不可能搞什么开发运维一体化)。当然另外一种基于SPI的方式跟这种比没什么区别就不在说了。

2、基于JVM类加器的原理,在主应用中实现自定义的ClassLoader,通过监听指定目录下的*.jar文件的修改,再配合我们自定义的类加载器去完成加载/卸载(热替换),顺带将jar包中符合Spring Bean条件的类的BeanDefinition注册到Spring IOC容器中,注册的同时也要动态的更新对应bean实例中@Autowired修饰的属性值,这样一来,我们开发好的jar包,只需要传到指定的目录下,整个系统无需重启即可动态的"扩展"出支付宝支付的功能出来。整个流程简化版的图如下:

 

原图地址: jar包热加载(热部署) 

 


二、核心代码☞HotClassLoader

 

  代码注释很全,一看就明白,目前功能还很单一,只是测试DEMO阶段,待大量测试和改良,秉着开源精神,代码全部放开(主要是实现思路很有意思),最后会附上Gitee代码仓库地址。

 

 
  1. import com.appleyk.util.SpringBeanUtils;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.core.annotation.AnnotationUtils;
  5. import org.springframework.stereotype.Component;
  6. import org.springframework.stereotype.Service;
  7. import org.springframework.util.ClassUtils;
  8. import org.springframework.util.CollectionUtils;
  9. import org.springframework.util.ResourceUtils;
  10. import org.springframework.web.bind.annotation.RestController;
  11. import java.io.File;
  12. import java.io.IOException;
  13. import java.lang.reflect.Field;
  14. import java.net.*;
  15. import java.util.*;
  16. import java.util.jar.JarEntry;
  17. import java.util.jar.JarFile;
  18. import java.util.stream.Collectors;
  19.  
  20. /**
  21. * <p>自定义类加载器,主要用来加载指定目录下的所有以.jar结尾的文件</p>
  22. *
  23. * @author appleyk
  24. * @version V.0.1.1
  25. * @blob https://blog.csdn.net/appleyk
  26. * @github https://github.com/kobeyk
  27. * @date created on 下午9:16 2022/11/23
  28. */
  29. @Slf4j
  30. public class HotClassLoader extends URLClassLoader {
  31.  
  32. /**设定插件默认放置的路径*/
  33. private static final String PLUGINS_DIR = "classpath:plugins";
  34. /**jar更新时间键值对,通过value来判断jar包是否被修改了*/
  35. private static final Map<String,Long> jarUpdateTime;
  36. /**jar包对应的类加载器,即1个jar包对应1个类加载器,但是1个类加载器可以对应N个jar包*/
  37. private static final Map<String,HotClassLoader> jarClassLoaders;
  38. /**jar包中类的完全限定名键值对,即1个jar包包含N个class*/
  39. private static final Map<String, List<String>> jarClassName;
  40.  
  41. static {
  42. jarUpdateTime = new HashMap<>(16);
  43. jarClassLoaders = new HashMap<>(16);
  44. jarClassName = new HashMap<>(16);
  45. }
  46.  
  47. public HotClassLoader(ClassLoader parent) {
  48. super(new URL[0],parent);
  49. }
  50.  
  51. /**
  52. * 一次性加载plugins目录下的所有jar(这个可以放在定时扫描中,n秒执行一次)
  53. * 但是前提必须是jar包有更新,也就是第一次是全量加载,后面扫描只会基于更新的jar做热替换
  54. */
  55. public static void loadAllJar() throws Exception{
  56. File file = null;
  57. /** 首先先判断classpath下plugins是否存在,如果不存在,帮用户创建 */
  58. try{
  59. file = ResourceUtils.getFile(PLUGINS_DIR);
  60. }catch (Exception e){
  61. String classesPath = ClassUtils.getDefaultClassLoader().getResource("").getPath();
  62. String pluginsDir = classesPath+"plugins";
  63. file = new File(pluginsDir);
  64. if (!file.exists()){
  65. /**不存在就创建*/
  66. file.mkdirs();
  67. }
  68. }
  69.  
  70. /** 如果存在,遍历目录下面的所有子文件对象*/
  71. File[] files = file.listFiles();
  72. if (files == null || files.length == 0){
  73. log.info("no plugins resource need loading...");
  74. return;
  75. }
  76. List<String> updatedJars = new ArrayList<>();
  77. for (File childFile : files) {
  78. String name = childFile.getName();
  79. /**如果子文件对象是文件夹,则不处理*/
  80. if (childFile.isDirectory()){
  81. log.warn("not support the folder of " + name);
  82. continue;
  83. }
  84. /**如果文件不以jar结尾,也不处理*/
  85. if (!name.endsWith(".jar")){
  86. log.warn("not support the plugin file of " + name);
  87. continue;
  88. }
  89. /**构建jar类路径*/
  90. String jarPath = String.format("%s/%s",PLUGINS_DIR,name);
  91. long lastModifyTime = childFile.getAbsoluteFile().lastModified();
  92. if (Objects.equals(lastModifyTime,jarUpdateTime.get(jarPath))){
  93. continue;
  94. }
  95. /**将修改过的jar路径保存起来s*/
  96. System.out.println(String.format("%s changed, need to reload",jarPath));
  97. updatedJars.add(jarPath);
  98. }
  99.  
  100. if (updatedJars.size() == 0){
  101. System.out.println("There are no Jars to update !");
  102. return;
  103. }
  104.  
  105. /**
  106. * 如果本次扫描发现有jar包更新,则从ioc容器里取出新的classLoader实例以加载这些jar包中的class
  107. * 这个地方很巧妙。即同一批次更新的jar包会使用用同一个类加载器去加载,这就避免了类加载器不会平白无故的多出很多!
  108. * 为什么这里要重新加载呢?我们知道,判断类对象在JVM中是否独有一份并不取决于它的完全限定名,即com.appleyk.xxx
  109. * 是唯一的,还取决于把它载入JVM内存中的类加载器是不是同一个,也就是同样是User.class,我们可以让它在JVM中存在多份,
  110. * 这个只需要用不同的用户自定义类加载器实例去loadClass即可实现,话又说回来,如果不是需要热更新正常情况下我们肯定不会这么做的!
  111. * 这里使用新的类加载对象去加载这一批更新的jar包的目的就是实现Class的热卸载和热替换。
  112. * 具体怎么做的,可以细看loadJar方法的实现,最好一边调试一边看,效果最佳!
  113. */
  114. HotClassLoader classLoader = SpringBeanUtils.getBean(HotClassLoader.class);
  115. for (String updatedJar : updatedJars) {
  116. loadJar(updatedJar,classLoader);
  117. }
  118. }
  119.  
  120. /**
  121. * 使用指定的类加载加载单个jar文件中的所有class文件到JVM中,同时向Spring IOC容器中注入BD
  122. * @param jarPath jar类路径,格式如:classpath:plugins/xxxx.jar
  123. * @param classLoader 类加载器
  124. */
  125. public static void loadJar(String jarPath,HotClassLoader classLoader) throws Exception{
  126. /**先尝试从jar更新时间map中取出jarPath的更新时间*/
  127. Long lastModifyTime = jarUpdateTime.get(jarPath);
  128. /**如果等于0L,说明这个jar包还处于加载中,直接退出*/
  129. if (Objects.equals(lastModifyTime,0L)){
  130. log.warn("HotClassLoader.loadJar loading ,please not repeat the operation, jarPath = {}", jarPath);
  131. return;
  132. }
  133.  
  134. /**拿到jar文件对象*/
  135. File file = jarPath.startsWith("classpath:") ? ResourceUtils.getFile(jarPath) : new File(jarPath);
  136. /**为了保险,还是判断下jarPath(有可能是外部传进来的非法jarPath)是否存在*/
  137. if (!file.exists()) {
  138. log.warn("HotClassLoader.loadJar fail file not exist, jarPath = {}", jarPath);
  139. return;
  140. }
  141.  
  142. /**获取真实物理jarPath文件的修改时间*/
  143. long currentJarModifyTime = file.getAbsoluteFile().lastModified();
  144. /**如果通过对比发现jar包没有做任何修改,则不予重新加载,退出*/
  145. if(Objects.equals(lastModifyTime,currentJarModifyTime)){
  146. log.warn("HotClassLoader.loadJar current version has bean loaded , jarPath = {}", jarPath);
  147. return;
  148. }
  149.  
  150. /**获取新的类加载器*/
  151. if (classLoader == null){
  152. classLoader = SpringBeanUtils.getBean(HotClassLoader.class);
  153. }
  154.  
  155. /**
  156. * 如果jar包做了修改,则进行卸载流程
  157. * 用户自定义类加载器加载出来的Class被JVM回收的三个苛刻条件分别是:
  158. * 1、Class对应的所有的实例在JVM中不存在,即需要手动设置clzInstance = null;
  159. * 2、加载该类的ClassLoader在JVM中不存在,即需要手动设置classLoader = null;
  160. * 3、Class对象没有在任何地方被引用,比如不能再使用反射API,即需要手动设置class = null;
  161. */
  162. if (jarUpdateTime.containsKey(jarPath)){
  163. unloadJar(jarPath);
  164. }
  165.  
  166. /**保存或更新当前jarPath的类加载器*/
  167. jarClassLoaders.put(jarPath,classLoader);
  168.  
  169. try {
  170. if (jarPath.startsWith("classpath:")) {
  171. classLoader.addURL(new URI(jarPath).toURL());
  172. } else {
  173. classLoader.addURL(file.toURI().toURL());
  174. }
  175.  
  176. } catch (MalformedURLException e) {
  177. throw new IllegalArgumentException("通过url 添加 jar 失败");
  178. }
  179.  
  180. /**开始(重新)加载前,初始化jarPth的更新时间为0*/
  181. jarUpdateTime.put(jarPath, 0L);
  182.  
  183. List<String> classNameList = new ArrayList<>();
  184. /** 遍历 jar 包中的类 */
  185. try (JarFile jarFile = new JarFile(file.getAbsolutePath())) {
  186. List<JarEntry> jarEntryList = jarFile.stream().sequential().collect(Collectors.toList());
  187. for (JarEntry jarEntry : jarEntryList) {
  188. String jarName = jarEntry.getName();
  189. if (!jarName.endsWith(".class")) {
  190. continue;
  191. }
  192. /**类的完全限定名处理*/
  193. String className = jarName.replace(".class", "").replace("/", ".");
  194. boolean beanExist = SpringBeanUtils.contains(className);
  195. /**如果存在,更新*/
  196. if(beanExist){
  197. SpringBeanUtils.removeBean(className);
  198. }
  199. /**使用指定的类加载器加载该类*/
  200. Class<?> clz = classLoader.loadClass(className, false);
  201.  
  202. /**
  203. * 这个地方要反射一下,判断下,clazz上是否有注解(@Service@Component等)
  204. * 并不是所有的类都要注入到spring ioc容器中
  205. */
  206. boolean withBean =
  207. AnnotationUtils.findAnnotation(clz, Service.class) != null
  208. || AnnotationUtils.findAnnotation(clz, Component.class) != null;
  209. if (withBean){
  210. /**将class包装成BeanDefinition注册到Spring容器中*/
  211. SpringBeanUtils.registerBean(className, clz);
  212. /**
  213. * 动态替换bean,这个地方从常用的角度来看,我们只需处理@Controller类,
  214. * 给@AutoWired修饰的类字段做替换即可
  215. */
  216. doAutowired(className, clz);
  217. }
  218. classNameList.add(className);
  219. }
  220. } catch (IOException | ClassNotFoundException e) {
  221. throw new IllegalArgumentException("jar包解析失败");
  222. }
  223.  
  224. /** 记录jarPath包含的所有的类 */
  225. jarClassName.put(jarPath, classNameList);
  226.  
  227. /** 记录jarPath的更新时间 */
  228. jarUpdateTime.put(jarPath, currentJarModifyTime);
  229. }
  230.  
  231. /**卸载指定jar*/
  232. private static void unloadJar(String jarPath) throws Exception{
  233. /** 校验文件是否存在*/
  234. File file = ResourceUtils.getFile(jarPath);
  235. if (!file.exists()) {
  236. log.warn("HotClassLoader.loadJar fail file not exist, jarPath = {}", jarPath);
  237. return;
  238. }
  239. List<String> classNameList = jarClassName.get(jarPath);
  240. if(CollectionUtils.isEmpty(classNameList)){
  241. log.warn("HotClassLoader.loadJar fail,the jar no class, jarPath = {}", jarPath);
  242. return;
  243. }
  244.  
  245. HotClassLoader oldClassLoader = jarClassLoaders.get(jarPath);
  246. /** 遍历移除spring中对应的bean,移除引用 */
  247. for (String className : classNameList) {
  248. boolean beanExist = SpringBeanUtils.contains(className);
  249. if(beanExist){
  250. /**把旧的类实例移除,切断对象引用*/
  251. SpringBeanUtils.removeBean(className);
  252. }
  253. /**把旧的类加载器加载的Class对象置为null*/
  254. Class<?> oldClz = oldClassLoader.loadClass(className, false);
  255. oldClz = null;
  256. }
  257. /** 移除jarPath */
  258. jarUpdateTime.remove(jarPath);
  259. /**关闭类加载,然后切断引用*/
  260. if (oldClassLoader!=null){
  261. oldClassLoader.close();
  262. oldClassLoader = null;
  263. }
  264. }
  265.  
  266. /**
  267. * 处理bean的自动注入(手动)
  268. * 这一块代码逻辑稍显复杂,但是还好,有spring源码基础的小伙伴一定不陌生!
  269. * 这块的逻辑思路主要是借鉴了nacos的源码:
  270. * nacos不仅是配置中心还是服务注册与发现中心,其作为配置中心的时候,我们知道,
  271. * 项目中的Bean类中只要使用了@NacosValue注解去修饰属性字段,那么,一旦我们在
  272. * nacos的web端修改了指定配置属性字段的值并保存后,那么项目端无需重启,
  273. * 就可以获取到最新的配置值,它是怎么做到的呢? 首先抛开tcp连接不说,就说更新这块,
  274. * 那必然是先通过网络请求拿到nacos数据库中最新的配置值(值改变了会触发回调),然后
  275. * 找到这个字段所在的bean,然后再定位到bean实例的属性字段,然后通过反射set新值,
  276. * 也就是内存中保存的是旧值,然后运维或开发人员在nacos端修改了某项配置值,
  277. * 然后会通知App端进行值更新,App端获取到新的值后,会找到该值所在的beans,
  278. * 然后通过反射修改这些beans中的这个字段的值,修改成功后,内存中的旧值就被“热替换”了!
  279. */
  280. private static void doAutowired(String className,Class clz){
  281. Map<String, Object> beanMap = SpringBeanUtils.getBeanMap(RestController.class);
  282. if (beanMap == null || beanMap.size() == 0){
  283. return;
  284. }
  285. /**拿到clz的接口*/
  286. Class[] clzInterfaces = clz.getInterfaces();
  287. beanMap.forEach((k,v)->{
  288. Class<?> cz = v.getClass();
  289. /**拿到class所有的字段(private,protected,public,但不包括父类的)*/
  290. Field[] declaredFields = cz.getDeclaredFields();
  291. if (declaredFields == null || declaredFields.length == 0){
  292. return;
  293. }
  294. /**遍历字段,只处理@Autowired注解的字段值的注入*/
  295. for (Field declaredField : declaredFields) {
  296. if (!declaredField.isAnnotationPresent(Autowired.class)){
  297. return;
  298. }
  299. /**推断下字段类型是否是接口(如果是接口的话,注入的条件稍显"复杂"些)*/
  300. boolean bInterface = declaredField.getType().isInterface();
  301. /**拿到字段的类型完全限定名*/
  302. String fieldTypeName = declaredField.getType().getName();
  303.  
  304. /**设置字段可以被修改,这一版本,先不考虑多态bean的情况,下一个版本完善时再考虑*/
  305. declaredField.setAccessible(true);
  306. try{
  307. /**如果字段的类型非接口并且字段的类的完全限定名就等于clz的名,那就直接setter设置*/
  308. if (!bInterface && fieldTypeName == clz.getName()){
  309. declaredField.set(v,SpringBeanUtils.getBean(className,clz));
  310. }
  311. /**如果字段类型是接口,还得判断下clz是不是实现了某些接口,如果是,得判断两边接口类型是否一致才能注入值*/
  312. if (bInterface){
  313. if (clzInterfaces !=null || clzInterfaces.length > 0){
  314. for (Class inter : clzInterfaces) {
  315. if (fieldTypeName == inter.getName()){
  316. declaredField.set(v,SpringBeanUtils.getBean(className,clz));
  317. break;
  318. }
  319. }
  320. }
  321. }
  322. }catch (IllegalAccessException e){
  323. throw new IllegalArgumentException(e);
  324. }
  325. }
  326. });
  327. }
  328.  
  329. @Override
  330. protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
  331. if(name.startsWith("java.")){
  332. return ClassLoader.getSystemClassLoader().loadClass(name);
  333. }
  334. Class<?> clazz = findLoadedClass(name);
  335. if (clazz != null) {
  336. if (resolve) {
  337. return loadClass(name);
  338. }
  339. return clazz;
  340. }
  341. return super.loadClass(name, resolve);
  342. }
  343.  
  344. }
 

 

三、效果演示

 

视频地址:这边建议直接去B站看高清的:jar包热更新_哔哩哔哩_bilibili

jar包热更新

 

 


 

四、代码仓库

 

Gitee:springboot-plugins: 动态加载jar和卸载jar

 

 

目前只有一个master分支,其功能较为单一,后续有精力的话会继续开分支进行探究和完善。

posted @ 2024-07-01 15:18  CharyGao  阅读(126)  评论(1编辑  收藏  举报