在工作中,由于我是主要负责直播APP的运营活动开发,这些活动代码有几个特性
- 活动周期短,通常只是一个节日、一个星期、十天、一个月等,所以导致代码用于运行的时间短,活动下线代码就废弃了。
- 活动规则总是根据收益和效果频繁变化,所以导致代码频繁修改和部署上线。
- 活动小而多,导致开发快上线多。
- 活动最好支持新旧版本同时在线,以便新版开放之前旧版正常使用。
- 活动之间没有联系,但是会有一些共同服务依赖,比如获取用户信息、发放奖励、推送提醒等。
针对这些,会带来两个问题
- 代码只用一段时间,但类还是会被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的类图