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

在工作中,由于我是主要负责直播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的类图

posted @ 2025-06-27 16:33  CharyGao  阅读(39)  评论(0)    收藏  举报