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

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

@Slf4j
class ModuleClassLoader extends URLClassLoader {

    public static final String[] DEFAULT_EXCLUDED_PACKAGES = new String[]{"java.", "javax.", "sun.", "oracle."};

    private final Set<String> excludedPackages;

    private final Set<String> overridePackages;

    public ModuleClassLoader(URL url, ClassLoader parent) {
        super(new URL[]{url}, parent);
        this.excludedPackages = Sets.newHashSet(Arrays.asList(DEFAULT_EXCLUDED_PACKAGES.clone()));
        this.overridePackages = Sets.newHashSet();
    }

    public void addExcludedPackages(Set<String> excludedPackages) {
        this.excludedPackages.addAll(excludedPackages);
    }

    public void addOverridePackages(Set<String> overridePackages) {
        this.overridePackages.addAll(overridePackages);
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class<?> result = null;
        synchronized (ModuleClassLoader.class) {
            if (isEligibleForOverriding(name)) {
                if (log.isInfoEnabled()) {
                    log.info("Load class for overriding: {}", name);
                }
                result = loadClassForOverriding(name);
            }
            if (Objects.nonNull(result)) {
                // 链接类
                if (resolve) {
                    resolveClass(result);
                }
                return result;
            }
        }
        // 使用默认类加载方式
        return super.loadClass(name, resolve);
    }

    private Class<?> loadClassForOverriding(String name) throws ClassNotFoundException {
        // 查找已加载的类
        Class<?> result = findLoadedClass(name);
        if (Objects.isNull(result)) {
            // 加载类
            result = findClass(name);
        }
        return result;
    }

    private boolean isEligibleForOverriding(final String name) {
        checkNotNull(name, "name is null");
        return !isExcluded(name) && any(overridePackages, name::startsWith);
    }

    protected boolean isExcluded(String className) {
        checkNotNull(className, "className is null");
        for (String packageName : this.excludedPackages) {
            if (className.startsWith(packageName)) {
                return true;
            }
        }
        return false;
    }

}

3、自定义ApplicationContext

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

class ModuleApplicationContext extends AnnotationConfigApplicationContext {

}

4、动态加载

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

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

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

@Slf4j
class ModuleLoader implements ApplicationContextAware {

    /**
     * 注入父applicationContext
     */
    @Setter
    private ApplicationContext applicationContext;

    /**
     * 加载模块
     *
     * @param jarPath jar包路径
     * @return Module
     */
    public Module load(Path jarPath) {
        if (log.isInfoEnabled()) {
            log.info("Start to load module: {}", jarPath);
        }
        ModuleClassLoader moduleClassLoader;
        try {
            moduleClassLoader = new ModuleClassLoader(jarPath.toUri().toURL(), applicationContext.getClassLoader());
        } catch (MalformedURLException e) {
            throw new ModuleRuntimeException("create ModuleClassLoader exception", e);
        }
        List<ModuleConfig> moduleConfigList = new ArrayList<>();
        ServiceLoader.load(ModuleConfig.class, moduleClassLoader).forEach(moduleConfigList::add);
        if (moduleConfigList.size() != 1) {
            throw new ModuleRuntimeException("module config has and only has one");
        }
        ModuleConfig moduleConfig = moduleConfigList.get(0);
        moduleClassLoader.addOverridePackages(moduleConfig.overridePackages());
        ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
        try {
            // 把当前线程的ClassLoader切换成模块的
            Thread.currentThread().setContextClassLoader(moduleClassLoader);
            ModuleApplicationContext moduleApplicationContext = new ModuleApplicationContext();
            moduleApplicationContext.setParent(applicationContext);
            moduleApplicationContext.setClassLoader(moduleClassLoader);
            moduleApplicationContext.scan(moduleConfig.scanPackages().toArray(new String[0]));
            moduleApplicationContext.refresh();
            if (log.isInfoEnabled()) {
                log.info("Load module success: name={}, version={}, jarPath={}", moduleConfig.name(), moduleConfig.version(), jarPath);
            }
            return new Module(jarPath, moduleConfig, moduleApplicationContext);
        } catch (Throwable e) {
            log.error(String.format("Load module exception, jarPath=%s", jarPath), e);
            CachedIntrospectionResults.clearClassLoader(moduleClassLoader);
            throw new ModuleRuntimeException("create ModuleApplicationContext exception", e);
        } finally {
            // 还原当前线程的ClassLoader
            Thread.currentThread().setContextClassLoader(currentClassLoader);
        }
    }

}

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

    private Map<String, Handler> scanHandlers() {
        Map<String, Handler> handlers = Maps.newHashMap();
        // find Handler in module
        for (Handler handler : moduleApplicationContext.getBeansOfType(Handler.class).values()) {
            String handlerName = handler.name();
            if (!StringUtils.hasText(handlerName)) {
                throw new ModuleRuntimeException("scanHandlers handlerName is null");
            }
            checkState(!handlers.containsKey(handlerName), "Duplicated handler %s found by: %s",
                    Handler.class.getSimpleName(), handlerName);
            handlers.put(handlerName, handler);
        }
        if (log.isInfoEnabled()) {
            log.info("Scan handlers finish: {}", String.join(",", handlers.keySet()));
        }
        return ImmutableMap.copyOf(handlers);
    }

5、动态卸载

  • 关闭ApplicationContext。
  • 清除ClassLoader并关闭(Class卸载的条件很苛刻,这个需要多加监控)。
  • 删除jar文件。
public void destroy() throws Exception {
        if (log.isInfoEnabled()) {
            log.info("Destroy module: name={}, version={}", moduleConfig.name(), moduleConfig.version());
        }
        // close spring context
        closeApplicationContext(moduleApplicationContext);
        // clean class loader
        clearClassLoader(moduleApplicationContext.getClassLoader());
        // delete jar file
        Files.deleteIfExists(jarPath);
    }

    private void closeApplicationContext(ConfigurableApplicationContext applicationContext) {
        checkNotNull(applicationContext, "applicationContext is null");
        try {
            applicationContext.close();
        } catch (Exception e) {
            log.error("Failed to close application context", e);
        }
    }

    private void clearClassLoader(ClassLoader classLoader) throws IOException {
        checkNotNull(classLoader, "classLoader is null");
        // Introspector缓存BeanInfo类来获得更好的性能。卸载时刷新所有Introspector的内部缓存。
        Introspector.flushCaches();
        // 从已经使用给定类加载器加载的缓存中移除所有资源包
        ResourceBundle.clearCache(classLoader);
        // clear the introspection cache for the given ClassLoader
        CachedIntrospectionResults.clearClassLoader(classLoader);
        // close
        if (classLoader instanceof URLClassLoader) {
            ((URLClassLoader) classLoader).close();
        }
    }

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

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

附上dynamic-module的类图

dynamic-module

 

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值