HotwapAgent Java unlimited redefinition of classes at runtime
使用方法
- 按照github和官网说明操作即可, 依赖于DCEVM
- 如果自定义监控外部文件,需要将hotswap-agent.properties添加到src/main/resources中,并且配置extraClasspath属性
源码解析
评价
- 代码非常优雅,没有一个过长的类,每一个类源码都值得学习
- 字节码操作使用的是javassit, 可以非常直观的在某个方法的前面或者后面插入代码,性能比asm直接操作字节码要差一点
- 热部署的原理和美团的sonic非常相似,如果要实现sonic,可以基于HotswapAgent来实现
- 用了很多事件观察者模式,解耦了各个模块
- 利用注解,非常巧妙的拆解了热部署需要的基础功能,可以很简单的实现一个自定义插件
核心代码
- org.hotswap.agent.HotswapAgent agent类
- org.hotswap.agent.config.PluginManager 插件管理类
- org.hotswap.agent.config.PluginRegistry 插件注册类
- org.hotswap.agent.util.HotswapTransformer 字节操作类
- org.hotswap.agent.annotation.handler.AnnotationProcessor 注解解析类
- org.hotswap.agent.util.classloader.URLClassLoaderHelper URLClassLoader操作辅助类
- ClassLoaderDefineClassPatcher.patch 将classLoaderFrom下的所有字节绑定到classLoaderTo
- org.hotswap.agent.util.ReflectionHelper 发射工具类
- org.hotswap.agent.util.classloader.WatchResourcesClassLoader Special URL classloader to get only changed resources from URL.
- org.hotswap.agent.plugin.spring.ResetSpringStaticCaches 清理所有Spring静态缓存
- org.hotswap.agent.distribution.PluginDocs 文档生成
- 骚操作,将所有插件模块的README.md转换为html,和热部署代码无关
核心插件plugin
org.hotswap.agent.plugin.watchResources.WatchResourcesPlugin 资源监控
- 初始化
watchResourcesClassLoader.initWatchResources - 修改
appClassloader 优先使用watchResourcesClassLoader ,这里有个骚操作,使用javaassist进行代理增强实现的 org.hotswap.agent.plugin.hotswapper.HotswapperPlugin 通过JDPA API远程热更新
- Watch for any class file change and reload (hotswap) it on the fly. 说明文档
org.hotswap.agent.plugin.jdk.JdkPlugin JDK插件
- 监听重定义事件
LoadEvent.REDEFINE flushBeanIntrospectorCaches
- Removing from threadGroupContext
- Removing class from declaredMethodCache.
- 监听重定义事件
LoadEvent.REDEFINE flushIntrospectClassInfoCache
- Flushing class from com.sun.beans.introspect.ClassInfo cache
- 监听重定义事件
LoadEvent.REDEFINE flushObjectStreamCaches
- Flushing class from ObjectStreamClass caches
org.hotswap.agent.plugin.jvm.AnonymousClassPatchPlugin 匿名类插件
- 只监听匿名类的变更
.*\$\d+ - 匿名内是按照MyClass$1, MyClass$2这种在代码中的顺序生成的,如果更换位置以后原来的类位置都会发生变更
- 为了热更新的时候保证匿名类不错乱,才疏学浅某天沉下心来再深入研究
org.hotswap.agent.plugin.jvm.ClassInitPlugin 静态类和静态变量插件,类重定义LoadEvent.REDEFINE 时处理
- 先删除过去增强注入的老的
$$ha$clinit 方法 - 如果类有静态变量,则将静态变量初始化的函数注入成
public static $$ha$clinit() 方法,如果静态变量有变更或者枚举有变更,将通过scheduleCommand在150ms以后重新调用这个类的$$ha$clinit() 方法,因为代理类重定义是在100ms以后,静态变量在代理类重定义以后再执行
SpringPlugin
实例化流程
- 1.
pluginRegistry.scanPlugins 时发现SpringPlugin类将其加入到registeredPlugins - 2.
annotationProcessor.processAnnotations 将SpringPlugin上面两个@OnClassLoadEvent注解的静态方法生成两个PluginClassFileTransformer注册到hotswapTransformer 中
- @Plugin注解上的3个supportClass的静态注解也同样会被
annotationProcessor 处理,supportClass的注解处理和SpringPlugin是一样的 - 2.1
ClassPathBeanDefinitionScannerTransformer 监听了org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider 类加载事件,在其findCandidateComponents 方法的最后插入了ClassPathBeanDefinitionScannerAgent.registerBasePackage 代码 - 2.2
ProxyReplacerTransformer 监听了DefaultListableBeanFactory 、org.springframework.cglib.reflect.FastClass.Generator 、net.sf.cglib.reflect.FastClass.Generator 这三个类加载事件,在这些类字节被加载的时候进行代码插桩增强 - 2.3
XmlBeanDefinitionScannerTransformer 监听了org.springframework.beans.factory.xml.XmlBeanDefinitionReader 类加载事件,在loadBeanDefinitions 方法的最后插入了org.hotswap.agent.plugin.spring.scanner.XmlBeanDefinitionScannerAgent.registerXmlBeanDefinitionScannerAgent 代码 - 3.当
org.springframework.beans.factory.support.DefaultListableBeanFactory 类的字节码被定义的时候,会反射调用上面注册的transformer的函数,将SpringPlugin初始化和init函数代码插入到DefaultListableBeanFactory 的构造函数前面
- 3.1 当
DefaultListableBeanFactory 被实例化的时候SpringPlugin才开始实例化 - 3.2 当SpringPlugin进行实例化的时候,执行
annotationProcessor.processAnnotations 方法
- a.会将实例化对象pluginInstance的非静态注解变量(hotswapTransformer, watcher, scheduler,appClassLoader)通过反射赋值
- b.会将
@OnResourceFileEvent(path="/", filter = ".*.xml", events = {FileEvent.MODIFY}) 注解的registerResourceListeners 方法加入到资源文件监听listener中,当xml文件变更以后会执行XmlBeanRefreshCommand 命令重新加载xml中的bean - 3.3 执行SpringPlugin.init方法
- 如果在配置文件中自定义了的包路径,会调用registerBasePackage,生成一个字节转换HaClassFileTransformer, 该转换器在加载类的时候判定字节是否和原来的类定义有变化然后会在后面通过ClassPathBeanRefreshCommand命令重新对该bean进行初始化
- 4.当
org.springframework.aop.framework.CglibAopProxy 类的字节码被定义的时候,会反射调用上面注册的transformer的函数,将CglibAopProxy 类的createEnhancer 方法替换,禁用缓存 - 5.当
ClassPathScanningCandidateComponentProvider 类执行findCandidateComponents 的时候,会调用ClassPathBeanDefinitionScannerAgent.registerBasePackage 代码,进而执行SpringPlugin 的registerComponentScanBasePackage 方法,会和3.3一样,将包名进行注册registerBasePackage转换增强,同时将资源包下面的子包所有URL加入到watcher中, 子包中的类加载的时候后续会通过ClassPathBeanRefreshCommand命令重新再加载一次(子包应该是jar包,并且一个类在这个watcher中只会实例化一次)
.class文件变更以后如何判定是否需要重新加载bean
- WatchResourcesClassLoader将自定义的路径放在最前面,优先从自定义的包下面加载类
- WatchResourcesClassLoader优先使用UrlOnlyClassLoader查找资源,只从URL[]查找资源而不会通过父类去加载
public static class UrlOnlyClassLoader extends URLClassLoader {
public UrlOnlyClassLoader(URL[] urls) {
super(urls);
}
@Override
public URL getResource(String name) {
return findResource(name);
}
};
- WatchResourcesClassLoader.initWatchResources时通过watcher.addEventListener注册了一个监听器listener,文件变更以后通过listener然后将类保存到changedUrls中, 用来判断类是否发生了变更
- WatchResourcesClassLoader在getResource方法会判断类是否发生了变更,如果有变更返回变更后的resource
- 在上面第3.3和5中生成并注册了HaClassFileTransformer,通过老的class定义和新的字节码判定类是否变更,变更后会重新加载bean
xml文件变更后如何更新
上面的3.2.b这一步监听了所有xml文件变更事件,变更以后会通过XmlBeanRefreshCommand 命令重新加载xml中的bean
如何重新加载Spring bean
可以看代码细节来了解Spring bean从字节码到注册的整个流程
- ClassPathBeanRefreshCommand.executeCommand,通过反射代用下面的refreshClass方法
- ClassPathBeanDefinitionScannerAgent.refreshClass方法
- ResetSpringStaticCaches.reset() 清理所有静态缓存
- 为什么所有类变更都要清掉缓存,如果不是Spring bean不清理可不可以?
- resolveBeanDefinition 解析bean定义
public BeanDefinition resolveBeanDefinition(byte[] bytes) throws IOException {
Resource resource = new ByteArrayResource(bytes);
resetCachingMetadataReaderFactoryCache();
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
sbd.setSource(resource);
if (isCandidateComponent(sbd)) {
return sbd;
} else {
return null;
}
}else....
}
- defineBean 重新定义类,这里加了同步锁,将整个ClassPathBeanDefinitionScannerAgent类锁住了
-. 为什么前面调用Spring方法的时候都通过反射调用,最后调用freezeConfiguration时是直接调用?
public void defineBean(BeanDefinition candidate) {
synchronized (getClass()) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
removeIfExists(beanName);
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder = applyScopedProxyMode(scopeMetadata, definitionHolder, registry);
registerBeanDefinition(definitionHolder, registry);
DefaultListableBeanFactory bf = maybeRegistryToBeanFactory();
if (bf != null)
ResetRequestMappingCaches.reset(bf);
ProxyReplacer.clearAllProxies();
freezeConfiguration();
}
}
}
|