目录
HotSwap机制(JPDA Enhancements)分析
干货分享,感谢您的阅读!
一、热部署现状和必要性分析
(一)热部署定义和现状分析
Java 热部署(Hot Deployment)是指在运行时更新和重新加载Java应用程序的部分代码或资源,而无需停止整个应用程序。这种能力对于开发和调试过程中的快速迭代和故障排除非常有用。
在过去,Java的热部署能力相对较弱,通常需要使用特定的框架或工具来实现。然而,随着时间的推移,Java开发人员对热部署的需求不断增加,许多新的解决方案和工具已经涌现出来,使得Java热部署变得更加容易和普遍。
以下是一些当前可用的Java热部署技术和工具:
- JRebel:JRebel是一个商业工具,可以在不重启应用程序的情况下实现Java代码和资源的热部署。它通过使用类加载器技术和字节码转换来实现热部署功能。JRebel广泛用于开发环境和调试过程中,但需要购买许可证才能使用。
- Spring Boot DevTools:Spring Boot DevTools是一个开发工具包,提供了许多开发时的便利功能,包括热部署。它可以在开发过程中监视代码和资源的变化,并自动重新加载相应的部分,以加快开发周期。
- DCEVM:Dynamic Code Evolution VM(DCEVM)是一个Java虚拟机补丁,它扩展了JVM的功能,使得可以在运行时修改类定义而无需重新启动。DCEVM与HotSwap技术结合使用,可以实现类级别的热部署。
- JRebel和DCEVM的集成:可以结合使用JRebel和DCEVM来实现更强大的热部署功能。这种集成方式可以提供更广泛的热部署覆盖范围,并提供更高级的调试和开发功能。
尽管Java热部署的能力在过去几年里有所改进,但仍然存在一些限制和挑战。例如,某些类型的代码更改,如对类的继承结构进行更改,可能需要重启应用程序才能生效。此外,某些热部署解决方案可能对特定的应用程序架构和环境要求较高。
总体而言,Java热部署的现状已经有了显著的改进,使得开发人员能够更快速、高效地进行开发和调试。然而,具体的选择取决于应用程序的需求和开发团队的偏好。
(二)技术实现难度分析
Java热部署的技术难点主要涉及以下几个方面:
- 类加载器和类定义的管理:Java的热部署依赖于类加载器的机制来加载和重新加载类定义。在热部署过程中,需要正确地管理类加载器和类的关系,确保新的类定义能够被正确加载并替换旧的定义,同时避免内存泄漏和类加载冲突等问题。
- 字节码转换和动态修改:在热部署过程中,需要对字节码进行转换和修改,以将新的代码和资源应用到运行中的应用程序。这涉及到对字节码的解析、修改和重新加载,同时需要保证修改后的字节码的正确性和一致性,以避免应用程序的异常行为或崩溃。
- 资源管理和更新:除了类定义外,热部署还需要考虑应用程序中的其他资源,如配置文件、模板文件等的更新和管理。确保这些资源的正确加载和应用,以及与代码的同步更新,是一个挑战。
- 状态和数据的一致性:热部署可能会中断正在进行的业务逻辑和数据处理过程。在重新加载代码和资源时,需要考虑如何保持应用程序的状态和数据的一致性,以避免数据丢失、业务错误或不一致的情况发生。
- 对特定框架和环境的适配:不同的Java框架和应用服务器对热部署的支持程度和方式可能有所不同。在实现热部署时,需要针对具体的框架和环境进行适配和调整,以确保热部署功能的稳定性和兼容性。
这些技术难点需要综合考虑,并在热部署工具和框架中进行解决和实现。不同的工具和框架可能采用不同的方法和策略来应对这些挑战,以提供可靠和高效的热部署功能。
目前美团官方介绍的Java系列 | 远程热部署在美团的落地实践 - 美团技术团队热部署插件Sonic应运而生。
(三)其必要性分析
一般大多数企业在开发过程中工程师在对应需求测试调试中,每天都需要发布服务多次,多数应用程序单次大概都在3~10分钟,再加之构建部署镜像处理等单次时长集中在20~45分钟,部署频繁频次高、耗时长,严重影响了系统上线的效率。热部署在Java开发中具有重要的必要性,以下是一些分析:
- 提高开发效率:热部署允许开发人员在不重新启动整个应用程序的情况下进行代码和资源的更新。这意味着开发人员可以更快地进行代码修改和实验,快速迭代和调试应用程序。热部署可以减少开发周期,提高开发效率。
- 支持快速反馈和调试:热部署使开发人员能够立即看到他们对代码所做的更改的结果,无需重新构建和重新启动应用程序。这种快速反馈机制可以帮助开发人员快速调试和修复问题,提高开发效率和质量。
- 减少系统停机时间:传统的部署方式通常需要停止应用程序、重新部署和启动,这会导致系统停机时间。而热部署可以避免这种停机时间,允许在运行时更新和重新加载部分代码和资源,实现无缝的应用程序更新和升级。
- 提升用户体验:热部署可以在不中断正在进行的业务流程的情况下进行应用程序更新。这意味着用户可以在使用应用程序的同时获得最新的功能和修复,提升用户体验和满意度。
- 支持动态配置和灵活性:热部署使得可以动态修改应用程序的配置和行为,而无需重新启动应用程序。这提供了更大的灵活性和可配置性,允许根据需要进行实时调整和配置。
尽管热部署在Java开发中具有许多优势和必要性,但也需要注意一些潜在的挑战和限制。例如,某些类型的代码更改可能需要重启应用程序才能生效,热部署可能会引入一些运行时的复杂性,并需要适当的工具和框架支持。因此,在选择和使用热部署技术时,需要综合考虑项目需求、开发团队的技术水平和工具成本等因素。
如果具备热部署条件,那么他将帮助程序员在开发自测联调中极大发挥作用,以下是美团在上线使用Sonic后的对比图。
开发自测场景
联调场景
二、走进美团Java远程热部署实践
(一)Sonic分析
Sonic是美团内部研发设计的一款IDEA插件,旨在通过低代码开发辅助远程/本地热部署,解决Coding、单测编写执行、自测联调等阶段的效率问题,提高开发者的编码产出效率。数据统计表明,开发者日常大概有35%时间用于编码的产出。如果想提高研发效率,要么扩大编码产出的时间占比,要么提高编码阶段的产出效率,而Sonic则聚焦提高编码阶段的产出效率。
目前,使用Sonic热部署可以解决大部分代码重复构建的问题。Sonic可以使用户在本地编写代码一键部署到远程环境,修改代码、部署、联调请求、查看日志,循环反复。如果不考虑代码修改时间,通常一个循环需要20~35分钟,而使用Sonic可以把整个时长缩短至5~10秒,而且能够给开发者带来高效沉浸式的开发体验。在实际编码工作中,多文件修改是家常便饭,Sonic对多文件的热部署能力尤为突出,它可以通过依赖分析等手段来对多文件批量进行远程热部署,并且支持Spring Bean Class、普通Class、Spring XML、MyBatis XML等多类型文件混合热部署。
(二)业界现有产品对比
JRebel支持三方插件较多,生态庞大,但是对于国产的插件不支持,例如FastJson等,同时它还存在远程热部署配置局限,对于公司内部的中间件需要个性化开发,并且是商业软件,整体的使用成本较高。
(二)Sonic远程热部署落地推广的实践经验
对于技术产品的推广,尤其是开发、测试阶段使用的产品,由于远离线上环境,推动力、执行力、产品功能闭环能否做好,是决定着该产品是否能在企业内部落地并得到大多数人认可的重要的一环。此外,因为很多开发者在开发、测试阶段已逐渐形成了“固化动作”,如何改变这些用户的行为,让他们拥抱新产品,也是Sonic面临的艰巨挑战之一。
Sonic团队从主动沟通、零成本(或极低成本)快速接入、自动化脚本,以及产品自动诊断、收集反馈等方向出发,践行出了如上四条原则。
三、整体设计方案
(一)Sonic结构
Sonic插件由4大部分组成,包括脚本端、插件端、Agent端,以及Sonic服务端。脚本端负责自动化构建Sonic启动参数、服务启动等集成工作;IDEA插件端集成环境为开发者提供更便捷的热部署服务;Agent端随项目启动负责热部署的功能实现;服务端则负责收集热部署信息、失败上报等统计工作。如下图所示:
(二)Agent分析
Agent技术分析
Agent(代理)在软件开发中是指一种能够在应用程序运行时对其进行监控、修改和扩展的工具或库。Agent可以通过字节码注入、动态代理、代码转换等技术来实现对应用程序的植入和操作。
Agent技术在Java开发中具有重要的作用和广泛的应用,以下是对Agent技术的分析:
- 动态监控和诊断:Agent可以在应用程序运行时监控和收集各种指标和信息,如性能指标、内存使用情况、方法调用等。这对于应用程序的性能分析、问题诊断和优化非常有用。Agent可以通过自定义的逻辑和规则来捕捉关键事件并生成相应的日志或报告。
- 代码增强和修正:Agent可以通过在运行时修改字节码来实现对应用程序的代码增强和修正。例如,可以在方法调用前后插入自定义的逻辑,对参数进行修改或验证。这种能力使得可以在不修改源代码的情况下对应用程序进行定制和增强。
- 动态加载和卸载:Agent可以通过自定义的类加载器机制实现动态加载和卸载类定义。这对于实现插件式架构、模块化开发和动态扩展非常有用。Agent可以根据需要动态加载新的类和资源,并在不需要时进行卸载,从而实现应用程序的动态性和灵活性。
- 安全检查和权限控制:Agent可以在应用程序运行时对代码进行安全检查和权限控制。例如,可以拦截敏感方法的调用并进行身份验证,或者对访问受限资源的操作进行权限检查。这有助于增强应用程序的安全性和可控性。
- AOP(面向切面编程)支持:Agent可以与AOP框架结合使用,实现对应用程序的横切关注点的管理和操作。通过Agent,可以在运行时对应用程序进行切面编程,实现日志记录、性能监控、事务管理等通用功能。
需要注意的是,Agent技术在使用时需要谨慎,因为对应用程序的运行时植入和操作可能会引入性能开销和复杂性。同时,对于一些安全敏感的环境,需要审慎评估Agent的使用和权限控制,以确保系统的安全性和稳定性。
一些知名的Java Agent工具包括Byte Buddy、AspectJ、Java Agent API等。这些工具提供了丰富的功能和灵活的扩展性,使得开发人员可以根据需要定制和使用Agent技术。
核心原理概述
Agent的核心原理涉及字节码操作和类加载器机制。以下是对Agent核心原理的分析:
- 字节码操作:Agent通过对应用程序的字节码进行操作来实现对应用程序的增强和修正。字节码操作可以包括插入新的指令、修改现有指令、删除指令等。这种操作可以在运行时动态地改变应用程序的行为。
- 类加载器机制:Agent利用Java的类加载器机制来加载和定义代理类。它通过自定义的类加载器来加载Agent生成的代理类,并将其定义为新的类定义。这允许Agent在运行时动态地创建和加载类,而不需要修改原始类的定义。
- 动态代理:Agent经常使用动态代理技术来实现对应用程序的植入和操作。动态代理可以通过创建代理类和代理对象来拦截对原始类的方法调用,并在调用前后插入自定义的逻辑。这使得可以在不修改原始类的情况下对应用程序进行增强和修正。
- JVM Attach机制:Agent通常使用JVM Attach机制来将自身附加到目标Java进程中。JVM Attach机制允许Agent访问目标进程的内部,并与Java虚拟机进行通信。这使得Agent能够在运行时向目标进程注入自己的代码,并与应用程序交互。
- Java Agent API:Java提供了Java Agent API,它定义了一组用于编写和管理Java Agent的接口和类。通过Java Agent API,Agent可以注册回调方法,在应用程序加载、初始化、转换等事件发生时进行相应的处理。
Agent的核心原理是通过在运行时操作字节码和类加载器来实现对应用程序的动态增强和修正。它允许开发人员通过自定义的逻辑和规则来拦截和修改应用程序的行为,从而实现对应用程序的定制和扩展。这种能力对于监控、诊断、安全控制、AOP等场景都具有重要的作用。
Instrument工具类分析
Instrument是Java语言中的一个工具类,位于java.lang.instrument包中。它提供了一组用于在运行时转换Java应用程序的字节码的工具和方法。Instrumentation API允许开发人员在类加载过程中修改字节码、定义新的类以及重新定义已加载的类。
Instrument主要用于开发Java Agent,它为Agent提供了对Java应用程序的底层访问和操作能力。通过Instrumentation API,开发人员可以在Java应用程序的生命周期中进行自定义的字节码转换和操作。Instrument的主要功能包括:
- 注册转换器(Transformer):Instrument提供了addTransformer方法,用于注册自定义的类转换器(ClassFileTransformer)。类转换器是实现ClassFileTransformer接口的类,用于在类加载过程中转换类的字节码。
- 转换类的字节码:ClassFileTransformer接口的transform方法允许开发人员修改类的字节码。通过实现该方法,可以在类加载时对类的字节码进行自定义的操作,例如插入新的指令、修改现有指令等。
- 重新定义类:Instrument提供了redefineClasses方法,允许开发人员重新定义已加载的类。通过提供新的类字节码,可以替换已加载类的定义,从而实现对类的重新定义和替换。
- 获取类的定义:Instrument的getAllLoadedClasses方法可以获取当前已加载的所有类的定义。通过遍历已加载的类,开发人员可以获取类的名称、字节码等信息。
- 动态添加代理类:Instrument的appendToBootstrapClassLoaderSearch方法可以将代理类添加到引导类加载器的搜索路径中。这样,代理类就可以被引导类加载器加载,从而在整个应用程序中起作用。
Instrument的使用通常结合Java Agent来实现。Java Agent利用Instrumentation API来实现对Java应用程序的动态植入和操作。通过Instrumentation的实例,Agent可以注册类转换器并进行类的转换和重新定义,从而实现对应用程序的定制和增强。
Instrumentation分析
Instrumentation是Java提供的一个API,位于java.lang.instrument包中。它提供了一组用于在Java应用程序运行时转换字节码的工具和方法。Instrumentation API允许开发人员在类加载过程中修改字节码、定义新的类以及重新定义已加载的类。
Agent利用Instrumentation API来实现对应用程序的动态植入和操作。通过Agent与Instrumentation API的结合,可以在应用程序加载阶段或运行时对类进行转换、修改和增强。具体来说,Agent可以通过Instrumentation API注册一个ClassFileTransformer,并实现其transform方法来对类的字节码进行转换。
当Java应用程序启动时,Agent通过JVM Attach机制将自身附加到目标进程,并在premain方法或agentmain方法中获取到Instrumentation实例。然后,Agent使用Instrumentation实例注册自定义的ClassFileTransformer,并在类加载过程中对类进行转换。
Instrumentation API提供了以下功能来支持Agent的实现:
- 注册ClassFileTransformer:Agent使用Instrumentation实例的addTransformer方法来注册自定义的ClassFileTransformer,以便在类加载过程中对类进行转换。
- 转换类的字节码:ClassFileTransformer接口的transform方法允许Agent修改和转换类的字节码。Agent可以在该方法中获取类的字节码,并对其进行自定义的操作,例如插入新的指令、修改现有指令等。
- 重新定义类:Instrumentation API的redefineClasses方法允许Agent重新定义已加载的类。Agent可以通过提供新的类字节码来替换已加载类的定义,从而实现对类的重新定义和替换。
Instrumentation和Agent的结合使得开发人员可以在运行时对Java应用程序进行动态植入和操作,实现对应用程序的定制、增强和修正。通过Instrumentation API,Agent可以以非侵入的方式修改应用程序的行为,而无需修改源代码。
Instrumentation类常用API如下:
public interface Instrumentation { //增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。 void addTransformer(ClassFileTransformer transformer, boolean canRetransform); //在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义, //如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。 //对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。 void addTransformer(ClassFileTransformer transformer); //删除一个类转换器 boolean removeTransformer(ClassFileTransformer transformer); //是否允许对class retransform boolean isRetransformClassesSupported(); //在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。 void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; //是否允许对class重新定义 boolean isRedefineClassesSupported(); //此方法用于替换类的定义,而不引用现有的类文件字节,就像从源代码重新编译以进行修复和继续调试时所做的那样。 //在要转换现有类文件字节的地方(例如在字节码插装中),应该使用retransformClasses。 //该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名 void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException; //获取已经被JVM加载的class,有className可能重复(可能存在多个classloader) @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); }
启动时和运行时加载Instrument Agent过程分析
(三)JVM和HotSwap之间的“相爱相杀”
围绕着Method Body的HotSwap JVM一直在进行改进。
从1.4版本开始,JPDA引入HotSwap机制(JPDA Enhancements),实现Debug时的Method Body的动态性。大家可参考文档:enhancements1.4 。
1.5版本开始通过JVMTI实现的java.lang.instrument(Java Platform SE 8)的Premain方式,实现Agent方式的动态性(JVM启动时指定Agent)。大家可参考文档:package-summary。
1.6版本又增加Agentmain方式,实现运行时动态性(通过The Attach API 绑定到具体VM)。大家可参考文档:package-summary 。基本实现是通过JVMTI的retransformClass/redefineClass进行method、body级的字节码更新,ASM、CGLib基本都是围绕这些在做动态性。但是针对Class的HotSwap一直没有动作(比如Class添加method、添加field、修改继承关系等等),为什么会这样呢?因为复杂度过高,且没有很高的回报。
HotSwap机制(JPDA Enhancements)分析
HotSwap机制是由Java平台调试体系结构(Java Platform Debugging Architecture,JPDA)提供的一项功能增强。JPDA是Java平台的一部分,用于支持开发者在调试应用程序时进行交互式的调试操作。
HotSwap机制允许在应用程序运行时对已加载的类进行修改,而无需重新启动应用程序。这对于开发过程中的快速迭代和调试非常有用,因为开发者可以在运行时更改代码并立即查看修改的效果,而不需要停止和重新启动应用程序。
HotSwap机制的主要特点和原理包括以下几个方面:
- 动态更新字节码:HotSwap机制通过在运行时替换类的字节码来实现类的更新。开发者可以使用调试器或其他支持JPDA的工具来编辑类的字节码,并将修改后的字节码发送给正在运行的虚拟机。
- 代码热替换:HotSwap机制支持对方法体进行修改,从而实现代码的热替换。开发者可以在运行时更改方法的实现代码,然后重新加载该方法,使新的代码立即生效。
- 限制和约束:HotSwap机制有一些限制和约束。例如,只能替换方法体,而不能添加新的字段或方法。此外,一些场景下的代码更改可能需要应用程序的重新初始化或重新连接。
- JPDA支持:HotSwap机制是通过Java平台调试体系结构(JPDA)提供的。JPDA是一套用于调试和监控Java应用程序的API和协议。通过JPDA的增强功能,开发者可以利用HotSwap机制来实现在运行时更新代码的需求。
总结来说,HotSwap机制是JPDA的一项功能增强,允许开发者在运行时对已加载的类进行修改,从而实现代码的热替换。它为开发者提供了一种快速迭代和调试代码的方式,节省了重新启动应用程序的时间,提高了开发效率。
JVMTI分析
JVMTI(Java Virtual Machine Tool Interface)是Java虚拟机工具接口,它允许开发人员开发和连接到Java虚拟机的工具,以便进行监控、调试和分析Java应用程序的执行。
以下是对JVMTI的分析:
- 功能概述:JVMTI提供了一组API,允许开发人员编写工具来监视和控制Java虚拟机的行为。它允许开发人员在应用程序执行过程中获取关于类、对象、线程、堆栈、内存等方面的信息,并进行动态修改和控制。
- 监控和调试功能:JVMTI允许开发人员通过事件通知机制监控Java虚拟机中发生的事件,例如类加载、方法调用、异常抛出等。开发人员可以注册事件监听器,以便在事件发生时执行相应的操作。此外,JVMTI还提供了对线程、堆栈、内存等进行监控和分析的能力。
- 动态修改功能:JVMTI允许开发人员在应用程序运行时对Java类进行修改。开发人员可以使用JVMTI提供的功能来重新定义类、修改方法体、添加字段等。这种动态修改的能力对于一些特定的应用场景(如代码热替换)非常有用。
- 性能分析功能:JVMTI还提供了一些性能分析工具,如内存分析、线程分析和CPU分析。开发人员可以使用这些工具来识别性能瓶颈、内存泄漏等问题,并优化Java应用程序的性能。
- 嵌入式和远程连接:JVMTI支持在Java虚拟机内嵌入工具代理,也支持通过远程连接与运行在远程Java虚拟机上的应用程序进行交互。这使得开发人员可以远程监控和调试分布式的Java应用程序。
总结来说,JVMTI是Java虚拟机工具接口,提供了一组API和功能,允许开发人员开发和连接到Java虚拟机的工具。它提供了监控、调试、分析和动态修改Java应用程序的能力,为开发人员提供了丰富的工具和接口来深入了解和控制Java应用程序的执行。
Agentmain实现运行时动态性分析
Agentmain是Java Agent机制中的一种方式,用于在应用程序运行时动态地加载和初始化Java Agent。通过Agentmain,开发人员可以在应用程序启动后将Java Agent加载到正在运行的Java虚拟机中,并在运行时对应用程序进行动态性分析。
Agentmain的实现原理和步骤如下:
- 编写Java Agent:首先,开发人员需要编写一个Java Agent,其中包含了实现所需的功能和逻辑。Java Agent可以使用Instrumentation API来监视和修改正在运行的应用程序。
- 启动应用程序:然后,开发人员需要启动目标应用程序,可以通过命令行或其他方式启动。在启动应用程序时,需要在JVM启动参数中指定-javaagent选项,并将Java Agent的JAR文件路径作为参数传递给-agentmain参数。
- 加载Agentmain:在应用程序启动后,Java虚拟机会自动加载并初始化Agentmain。Agentmain中的premain方法会被调用,用于初始化Java Agent。在premain方法中,开发人员可以获取Instrumentation实例,并注册需要的监视器和转换器。
- 运行时动态性分析:一旦Agentmain被成功加载和初始化,开发人员就可以使用Instrumentation API对正在运行的应用程序进行动态性分析。例如,可以添加方法拦截器、收集性能指标、记录日志等。通过Instrumentation API,可以修改类的定义、替换方法体或添加额外的字节码逻辑。
总结来说,Agentmain是Java Agent机制中的一种实现方式,用于在应用程序运行时动态加载和初始化Java Agent。通过Agentmain,开发人员可以使用Instrumentation API对正在运行的应用程序进行动态性分析和修改。这为开发人员提供了一种灵活且强大的工具,用于在应用程序的运行时环境中实施各种监控、分析和优化的功能。
(四)Sonic解决Instrumentation局限性
由于JVM限制,JDK 7和JDK 8都不允许改类结构,比如新增字段,新增方法和修改类的父类等,这对于Spring项目来说是致命的。比如开发同学想修改一个Spring Bean,新增一个@Autowired字段,此类场景在实际应用时很多,所以Sonic对此类场景的支持必不可少。
那么,具体是如何做到的呢?这里要提一下“大名鼎鼎”的Dcevm。Dcevm(DynamicCode Evolution Virtual Machine)是Java Hostspot的补丁(严格上来说是修改),允许(并非无限制)在运行环境下修改加载的类文件。当前虚拟机只允许修改方法体(Method,Body),而Decvm可以增加、删除类属性、方法,甚至改变一个类的父类,Dcevm是一个开源项目,遵从GPL 2.0协议。更多关于Dcevm的介绍,大家可以参考:Wuerthinger10a以及GitHub Decvm。
值得一提的是,在美团内部,针对Dcevm的安装,Sonic已经打通HULK,集成发布镜像即可完成(本地热部署可结合插件功能实现一键安装热部署环境)。
Dcevm分析
DCEVM(Dynamic Code Evolution VM)是一个用于Java虚拟机(JVM)的增强工具,它允许在运行时修改和重新加载Java类文件,从而实现热部署和快速开发的能力。
以下是对DCEVM的分析:
- 热部署和快速开发:DCEVM的主要目标是提供一种方便的方式来进行热部署和快速开发。传统的Java开发过程中,修改Java类文件通常需要重新编译和重启应用程序。而DCEVM允许在不重新启动应用程序的情况下,动态地加载和替换已修改的Java类文件,实现即时生效的效果。
- 增强的类加载机制:DCEVM通过增强JVM的类加载机制,使其能够在运行时动态加载和卸载类文件。它提供了比标准JVM更灵活的类加载器实现,能够在应用程序运行时加载新的类定义,并替换已加载类的定义。
- 快速代码热替换:DCEVM还提供了快速代码热替换(HotSwap)的功能。它允许开发人员在应用程序运行时修改已加载类的方法体,并立即生效。这种快速的代码热替换能够极大地提升开发效率,减少开发和调试的时间。
- 兼容性:DCEVM与标准的JVM兼容,并且可以与多个JVM实现配合使用。它可以与OpenJDK、Oracle JDK和其他一些JVM实现集成,提供对应用程序的增强功能。
- 部署和配置:使用DCEVM需要将其配置为JVM的替代版本,并将其与目标应用程序集成。DCEVM提供了相应的二进制发行版和安装工具,使得部署和配置相对简单。
总结来说,DCEVM是一个增强JVM的工具,它通过提供热部署和快速开发的能力,使开发人员能够在运行时动态加载和修改Java类文件。它可以实现即时生效的代码热替换,提高开发效率和灵活性。DCEVM与标准的JVM兼容,并且可以与多个JVM实现集成,为开发人员提供了一个方便和强大的工具来加速Java应用程序的开发和调试过程。
四、Sonic热部署技术解析
(一) Sonic整体架构模型
(二)Sonic功能流转
Sonic通过NIO监听本地文件变更,触发文件变更事件,例如Class新增、Class修改、Spring Bean重载等事件流程。下图展示了一次热部署单个文件的生命周期:
(三)文件监听
Sonic首先会在本地和远程预定义两个目录,/var/tmp/sonic/extraClasspath
和/var/tmp/sonic/classes
。extraClasspath为Sonic自定义的拓展Classpath URL,classes为Sonic监听的目录,当有文件变更时,通过IDEA插件来部署到远程/本地,触发Agent的监听目录,来继续下面的热加载逻辑:
为什么Sonic不直接替换用户ClassPath下面的资源文件呢?
因为考虑到业务方WAR包的API项目、Spring Boot、Tomcat项目、Jetty项目等,都是以JAR包来启动的,这样是无法直接修改用户的Class文件的。即使是用户项目可以修改,直接操作用户的Class,也会带来一系列的安全问题。
所以,Sonic采用拓展ClassPath URL路径来实现文件的修改和新增。并且存在这么一种场景,多个业务侧的项目引入相同的JAR包,在JAR里面配置MyBatis的XML和注解。在此类情况下,Sonic没有办法直接来修改JAR包中源文件,通过拓展路径的方式可以不需要关注JAR包,来修改JAR包中某一文件和XML。同理,采用此类方法可以进行整个JAR包的热替换。下面我们简单介绍一下Sonic的核心监听器,如下图所示:
(四)JVM Class Reload
JVM的字节码批量重载逻辑,通过新的字节码二进制流和旧的Class对象生成ClassDefinition定义,instrumentation.redefineClasses(definitions),来触发JVM重载,重载过后将触发初始化时Spring插件注册的Transfrom。接下来,我们简单讲解一下Spring是怎么重载的。
新增class Sonic如何保证可以加载到Classloader上下文中?由于项目在远程执行,所以运行环境复杂,有可能是JAR包方式启动(Spring Boot),也有可能是普通项目,也有可能是War Web项目,针对此类情况Sonic做了一层Classloader URL拓展。
User ClassLoader是框架自定义的ClassLoader统称,例如Jetty项目是WebAppclassLoader。其中Urlclasspath为当前项目的lib文件件下,例如Spring Boot项目也是从当前项目BOOT-INF/lib/路径中加载CLass等等,不同框架的自定义位置稍有不同。所以针对此类情况,Agent必须拿到用户的自定义Classloader,如果是常规方式启动的,比如普通Spring XML项目,借助Plus(美团内部服务发布平台)发布,此类没有自定义Classloader,是默认AppClassLoader,所以Agent在用户项目启动过程中,借助字节码增强的方式来获取到真正的用户Classloader。
找到用户使用的子Classloader之后,通过反射的方式来获取Classloader中的元素Classpath,其中ClassPath中的URL就是当前项目加载Class时需要的所有运行时Class环境,并且包括三方的JAR包依赖等。
Sonic获取到URL数组,把Sonic自定义的拓展Classpath目录加入到URL数组首位,这样当有新增Class时,Sonic只需要将Class文件复制到拓展Classpath对应的包目录下面即可,当有其他Bean依赖新增的Class时,会从当前目录下面查找类文件。
为什么不直接对Appclassloader进行加强?而是对框架的自定义Classloader进行加强?
考虑这样一个场景,框架自定义类加载器中有ClassA,此时用户新增ClassB需要热加载,B Class里面有A的引用关系,如果增强AppClassLoader,初始化B实例时ClassLoader。loadclass首先从UserClassLoader开始加载ClassB的字节码,依靠双亲委派原则,B被Appclassloader加载,因为B依赖类A,所以当前AppClassLoader加载B一定是加载不到的,此时会抛出ClassNotFoundException异常。所以对类加载器拓展,一定要拓展最上层的类加载器,这样才会达到使用者想要的效果。
(五)Spring Bean重载
Spring Bean Reload过程中,Bean的销毁和重启流程,主要内容如下图展示:
如何加载Spring Bean
首先当修改Java Class D时,通过Spring ClasspathScan扫描校验当前修改的Bean是否Sprin Bean(注解校验),然后触发销毁流程(BeanDefinitionRegistry.removeBeanDefinition),此方法会将当前Spring上下文中的Bean D和依赖Spring Bean D的Bean C一并销毁,但是作用范围仅仅在当前Spring上下文。如果C被子上下文中的Bean B依赖,就无法更新子上下文中的依赖关系,当有系统请求时,Bean B中关联的Bean C还是热部署之前的对象,所以热部署失败。
因此,在Spring初始化过程中,需要维护父子上下文的对应关系,当子上下文变时若变更范围涉及到Bean B时,需要重新更新子上下文中的依赖关系,当有多上下文关联时需要维护多上下文环境,且当前上下文环境入口需要Reload。这里的入口是指:Spring MVC Controller、Mthrift和Pigeon,对不同的流量入口,采用不同的Reload策略。RPC框架入口主要操作为解绑注册中心、重新注册、重新加载启动流程等等,对Spring MVC Controller,主要是解绑和注册URL Mappping来实现流量入口类的变化切换。
(六) Spring XML重载
当用户修改/新增Spring XML时,需要对XML中所有Bean进行重载。
重新Reload之后,将Spring销毁后重启。需要注意的是:XML修改方式改动较大,可能涉及到全局的AOP的配置以及前置和后置处理器相关的内容,影响范围为全局,所以目前只放开普通的XML Bean标签的新增/修改,其他能力酌情逐步放开。
(七)MyBatis 热部署
Spring MyBatis热部署的主要处理流程是在启动期间获取所有Configuration路径,并维护它和Spring Context的对应关系,在热部署Class、XML时去匹配Configuration,从而重新加载Configuration以达到热部署的目的。
五、Sonic应用
(一)热部署功能一览
除了Spring Bean、Spring MVC、MyBatis的重载流程,Sonic还支持其它常用的开发框架,丰富的框架支持和兼容能力是Sonic的基石,下面列举一些Sonic支持的常用的第三方框架:
截止目前,Sonic已经支持绝大部分常用第三方框架的热加载,常规业务开发几乎无需重启服务。并且在美团内部的成功率已经高达99.9%以上,真正地让热部署来代替常规部署构建成为一种可能。
(二)IDE插件集成
Sonic也提供了功能强大的IDEA插件,让用户进行沉浸式开发,远程热部署也变得更加便利。
参考资料链接
- Java系列 | 远程热部署在美团的落地实践 - 美团技术团队:主要的学习材料
- "Java Agent and Instrumentation Tutorial by Baeldung" - Baeldung知名Java开发者网站,提供了关于Java Agent和Instrumentation的教程和示例代码。可以在Baeldung网站上搜索相关教程,例如"Java Agent tutorial Baeldung"。
- "Oracle官方文档" - Oracle提供了Java SE的官方文档,可以在Oracle的官方网站上找到有关Instrumentation API和Java Agent的详细说明和示例。访问Oracle的官方网站并搜索"Java SE documentation"可进入官方文档的页面。
- "Byte Buddy官方文档" - Byte Buddy流行的Java字节码操作库,它广泛用于开发Java Agent。可以访问Byte Buddy的官方网站,并在网站上找到有关使用Byte Buddy和Instrumentation API的详细指南和示例。