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,和热部署代码无关
- 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
- 监听重定义事件LoadEvent.REDEFINE flushBeanIntrospectorCaches
- 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以后,静态变量在代理类重定义以后再执行
- 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中只会实例化一次)
- WatchResourcesClassLoader将自定义的路径放在最前面,优先从自定义的包下面加载类
- WatchResourcesClassLoader优先使用UrlOnlyClassLoader查找资源,只从URL[]查找资源而不会通过父类去加载
public static class UrlOnlyClassLoader extends URLClassLoader { public UrlOnlyClassLoader(URL[] urls) { super(urls); } // do not use parent resource (may introduce infinite loop) @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
上面的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(); //清理metadataReaderFactory缓存 MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource); // 只有Spring bean类才去拿到beanDefinition 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时是直接调用?
- ResetSpringStaticCaches.reset() 清理所有静态缓存
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) {
// 反射执行classPathBeanDefinitionScanner.postProcessBeanDefinition方法
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
// 反射执行AnnotationConfigUtils.processCommonDefinitionAnnotations方法
processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
removeIfExists(beanName); // 先清理存在的bean
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
// 反射执行AnnotationConfigUtils.applyScopedProxyMode方法
definitionHolder = applyScopedProxyMode(scopeMetadata, definitionHolder, registry);
// 发射执行ClassPathBeanDefinitionScanner.registerBeanDefinition方法
registerBeanDefinition(definitionHolder, registry);
DefaultListableBeanFactory bf = maybeRegistryToBeanFactory();
if (bf != null)
// 清理Spring HandlerMapping, 包括handlerMethods、urlMap、nameMap等字段
// 然后重新调用afterPropertiesSet方法重新初始化HandlerMapping
ResetRequestMappingCaches.reset(bf);
// 清理插件代理,在SpringPlugin实例化流程的2.2中给com.sun.proxy.$Proxy代理类创建了代理类
ProxyReplacer.clearAllProxies();
// 调用beanFactory.freezeConfiguration的方法,Allow for caching all bean definition metadata, not expecting further changes
freezeConfiguration();
}
}
}



