IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> HotswapAgent源码解析 -> 正文阅读

[Java知识库]HotswapAgent源码解析

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监听了DefaultListableBeanFactoryorg.springframework.cglib.reflect.FastClass.Generatornet.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代码,进而执行SpringPluginregisterComponentScanBasePackage方法,会和3.3一样,将包名进行注册registerBasePackage转换增强,同时将资源包下面的子包所有URL加入到watcher中, 子包中的类加载的时候后续会通过ClassPathBeanRefreshCommand命令重新再加载一次(子包应该是jar包,并且一个类在这个watcher中只会实例化一次)

.class文件变更以后如何判定是否需要重新加载bean

  1. WatchResourcesClassLoader将自定义的路径放在最前面,优先从自定义的包下面加载类
  2. 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);
        }
    };
    
  3. WatchResourcesClassLoader.initWatchResources时通过watcher.addEventListener注册了一个监听器listener,文件变更以后通过listener然后将类保存到changedUrls中, 用来判断类是否发生了变更
  4. WatchResourcesClassLoader在getResource方法会判断类是否发生了变更,如果有变更返回变更后的resource
  5. 在上面第3.3和5中生成并注册了HaClassFileTransformer,通过老的class定义和新的字节码判定类是否变更,变更后会重新加载bean

xml文件变更后如何更新

上面的3.2.b这一步监听了所有xml文件变更事件,变更以后会通过XmlBeanRefreshCommand命令重新加载xml中的bean

如何重新加载Spring bean

可以看代码细节来了解Spring bean从字节码到注册的整个流程

  1. ClassPathBeanRefreshCommand.executeCommand,通过反射代用下面的refreshClass方法
  2. ClassPathBeanDefinitionScannerAgent.refreshClass方法
    1. ResetSpringStaticCaches.reset() 清理所有静态缓存
      • 为什么所有类变更都要清掉缓存,如果不是Spring bean不清理可不可以?
    2. 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....
      }
      
    3. 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) {
                // 反射执行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();
            }
        }
    }
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-05-08 07:54:26  更:2022-05-08 07:55:17 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 0:18:14-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码