IDEA的debug怎么实现?有人知道吗
初学Java时,我对IDEA的Debug非常好奇,不止是它能查看断点的上下文环境,更神奇的是我可以在断点处使用它的Evaluate功能直接执行某些命令,进行一些计算或改变当前变量。
刚开始语法不熟经常写错代码,重新打包部署一次代码耗时很长,我就直接面向Debug开发。在要编写的方法开始处打一个断点,在Evaluate框内一次次地执行方法函数不停地调整代码,没问题后再将代码复制出来放到IDEA里,再进行下一个方法的编写,这样就跟写PHP类似的解释性语言一样,写完即执行,非常方便。
但Java是静态语言,运行之前是要先进行编译的,难道我写的这些代码是被实时编译又注入到我正在Debug的服务里了吗?
随着对Java的愈加熟悉,我也了解了反射、字节码等技术,直到前些天的周会分享,有位同事分享了Btrace的使用和实现,提到了Java的ASM框架和JVMTI接口。Btrace修改代码能力的实现与Debug的Evaluate有很多相似之处,这大大吸引了我。
分享就像一个引子,从中学到的东西只是皮毛,要了解它还是要自己研究。于是自己查看资料并写代码学习了下其具体实现。ASM
实现Evaluate要解决的第一个问题就是怎么改变原有代码的行为,它的实现在Java里被称为动态字节码技术。动态生成字节码
我们知道,我们编写的Java代码都是要被编译成字节码后才能放到JVM里执行的,而字节码一旦被加载到虚拟机中,就可以被解释执行。
字节码文件(。class)就是普通的二进制文件,它是通过Java编译器生成的。而只要是文件就可以被改变,如果我们用特定的规则解析了原有的字节码文件,对它进行修改或者干脆重新定义,这不就可以改变代码行为了么。
Java生态里有很多可以动态生成字节码的技术,像BCEL、Javassist、ASM、CGLib等,它们各有自己的优势。有的使用复杂却功能强大、有的简单确也性能些差。ASM框架
ASM是它们中最强大的一个,使用它可以动态修改类、方法,甚至可以重新定义类,连CGLib底层都是用ASM实现的。
当然,它的使用门槛也很高,使用它需要对Java的字节码文件有所了解,熟悉JVM的编译指令。虽然我对JVM的字节码语法不熟,但有大神开发了可以在IDEA里查看字节码的插件:ASMBytecodeOutline,在要查看的类文件里右键选择ShowbytecodeOutline即可以右侧的工具栏查看我们要生成的字节码。对照着示例,我们就可以很轻松地写出操作字节码的Java代码了。
而切到ASMified标签栏,我们甚至可以直接获取到ASM的使用代码。
常用方法
在ASM的代码实现里,最明显的就是访问者模式,ASM将对代码的读取和操作都包装成一个访问者,在解析JVM加载到的字节码时调用。
ClassReader是ASM代码的入口,通过它解析二进制字节码,实例化时它时,我们需要传入一个ClassVisitor,在这个Visitor里,我们可以实现visitMethod()visitAnnotation()等方法,用以定义对类结构(如方法、字段、注解)的访问方法。
而ClassWriter接口继承了ClassVisitor接口,我们在实例化类访问器时,将ClassWriter注入到里面,以实现对类写入的声明。Instrument
介绍
字节码是修改完了,可是JVM在执行时会使用自己的类加载器加载字节码文件,加载后并不会理会我们做出的修改,要想实现对现有类的修改,我们还需要搭配Java的另一个库instrument。
instrument是JVM提供的一个可以修改已加载类文件的类库。1。6以前,instrument只能在JVM刚启动开始加载类时生效,之后,instrument更是支持了在运行时对类定义的修改。使用
要使用instrument的类修改功能,我们需要实现它的ClassFileTransformer接口定义一个类文件转换器。它唯一的一个transform()方法会在类文件被加载时调用,在transform方法里,我们可以对传入的二进制字节码进行改写或替换,生成新的字节码数组后返回,JVM会使用transform方法返回的字节码数据进行类的加载。JVMTI
定义完了字节码的修改和重定义方法,但我们怎么才能让JVM能够调用我们提供的类转换器呢?这里又要介绍到JVMTI了。介绍
JVMTI(JVMToolInterface)JVM工具接口是JVM提供的一个非常强大的对JVM操作的工具接口,通过这个接口,我们可以实现对JVM多种组件的操作,从JVMTMToolInterface这里我们认识到JVMTI的强大,它包括了对虚拟机堆内存、类、线程等各个方面的管理接口。
JVMTI通过事件机制,通过接口注册各种事件勾子,在JVM事件触发时同时触发预定义的勾子,以实现对各个JVM事件的感知和反应。Agent
Agent是JVMTI实现的一种方式。我们在编译C项目里链接静态库,将静态库的功能注入到项目里,从而才可以在项目里引用库里的函数。我们可以将agent类比为C里的静态库,我们也可以用C或C来实现,将其编译为dll或so文件,在启动JVM时启动。
这时再来思考Debug的实现,我们在启动被Debug的JVM时,必须添加参数agentlib:jdwptransportdtsocket,suspendy,addresslocalhost:3333,而agentlib选项就指定了我们要加载的JavaAgent,jdwp是agent的名字,在linux系统中,我们可以在jre目录下找到jdwp。so库文件。
Java的调试体系jdpa组成,从高到低分别为jdijdwpjvmti,我们通过JDI接口发送调试指令,而jdwp就相当于一个通道,帮我们翻译JDI指令到JVMTI,最底层的JVMTI最终实现对JVM的操作。使用
JVMTI的agent使用很简单,在启动agent时添加agent参数指定我们要加载的agentjar包即可。
而要实现代码的修改,我们需要实现一个instrumentagent,它可以通过在一个类里添加premain()或agentmain()方法来实现。而要实现1。6以上的动态instrument功能,实现agentmain方法即可。
在agentmain方法里,我们调用Instrumentation。retransformClasses()方法实现对目标类的重定义。
另外往一个正在运行的JVM里动态添加agent,还需要用到JVM的attach功能,Sun公司的tools。jar包里包含的VirtualMachine类提供了attach一个本地JVM的功能,它需要我们传入一个本地JVM的pid,tools。jar可以在jre目录下找到。agent生成
另外,我们还需要注意agent的打包,它需要指定一个AgentClass参数指定我们的包括agentmain方法的类,可以算是指定入口类吧。
此外,还需要配置MANIFEST。MF文件的一些参数,允许我们重新定义类。如果你的agent实现还需要引用一些其他类库时,还需要将这些类库都打包到此jar包中,下面是我的pom文件配置。buildpluginsplugingroupIdorg。apache。maven。pluginsgroupIdmavenassemblypluginartifactIdconfigurationmanifestEntriesasm。TestAgentAgentClassCanRedefineClassestrueCanRedefineClassesCanRetransformClassestrueCanRetransformClassesManifestVersion1。0ManifestVersionPermissionsallpermissionsPermissionsmanifestEntriesarchivedescriptorRefsdescriptorRefjarwithdependenciesdescriptorRefdescriptorRefsconfigurationpluginpluginsbuild
另外在打包时需要使用mvnassembly:assembl命令生成jarwithdependencies作为agent。代码实现
我在测试时写了一个用以上技术实现了一个简单的字节码动态修改的Demo。被修改的类
TransformTarget是要被修改的目标类,正常执行时,它会三秒输出一次hello。publicclassTransformTarget{publicstaticvoidmain(String〔〕args){while(true){try{Thread。sleep(3000L);}catch(Exceptione){break;}printSomething();}}publicstaticvoidprintSomething(){System。out。println(hello);}}
Agent
Agent是执行修改类的主体,它使用ASM修改TransformTarget类的方法,并使用instrument包将修改提交给JVM。
入口类,也是代理的AgentClass。publicclassTestAgent{publicstaticvoidagentmain(Stringargs,Instrumentationinst){inst。addTransformer(newTestTransformer(),true);try{inst。retransformClasses(TransformTarget。class);System。out。println(AgentLoadDone。);}catch(Exceptione){System。out。println(agentloadfailed!);}}}
执行字节码修改和转换的类。publicclassTestTransformerimplementsClassFileTransformer{publicbyte〔〕transform(ClassLoaderloader,StringclassName,Classlt;?classBeingRedefined,ProtectionDomainprotectionDomain,byte〔〕classfileBuffer)throwsIllegalClassFormatException{System。out。println(TransformingclassName);ClassReaderreadernewClassReader(classfileBuffer);ClassWriterclassWriternewClassWriter(ClassWriter。COMPUTEFRAMES);ClassVisitorclassVisitornewTestClassVisitor(Opcodes。ASM5,classWriter);reader。accept(classVisitor,ClassReader。SKIPDEBUG);returnclassWriter。toByteArray();}classTestClassVisitorextendsClassVisitorimplementsOpcodes{TestClassVisitor(intapi,ClassVisitorclassVisitor){super(api,classVisitor);}OverridepublicMethodVisitorvisitMethod(intaccess,Stringname,Stringdesc,Stringsignature,String〔〕exceptions){MethodVisitormvsuper。visitMethod(access,name,desc,signature,exceptions);if(name。equals(printSomething)){mv。visitCode();Labell0newLabel();mv。visitLabel(l0);mv。visitLineNumber(19,l0);mv。visitFieldInsn(Opcodes。GETSTATIC,javalangSystem,out,LjavaioPrintStream;);mv。visitLdcInsn(bytecodereplaced!);mv。visitMethodInsn(Opcodes。INVOKEVIRTUAL,javaioPrintStream,println,(LjavalangString;)V,false);Labell1newLabel();mv。visitLabel(l1);mv。visitLineNumber(20,l1);mv。visitInsn(Opcodes。RETURN);mv。visitMaxs(2,0);mv。visitEnd();TransformTarget。printSomething();}returnmv;}}}
Attacher
使用tools。jar里方法将agent动态加载到目标JVM的类。publicclassAttacher{publicstaticvoidmain(String〔〕args)throwsAttachNotSupportedException,IOException,AgentLoadException,AgentInitializationException{VirtualMachinevmVirtualMachine。attach(34242);目标JVMpidvm。loadAgent(pathtoagent。jar);}}
这样,先启动TransformTarget类,获取到pid后将其传入Attacher里,并指定agentjar,将agentattach到TransformTarget中,原来输出的hello就变成我们想要修改的bytecodereplaced!了。
小结
掌握了字节码的动态修改技术后,再回头看Btrace的原理就更清晰了,稍微摸索一下我们也可以实现一个简版的。另外很多大牛实现的各种Java性能分析工具的技术栈也不外如此,了解了这些,未来我们也可以写出适合自己的工具,至少能对别人的工具进行修改
不得不说Java的生态真的非常繁荣,当真是博大精深,查阅一个模块的资料时能总引出一大堆新的概念,永远有学不完的新东西。
本人花费2个月时间,整理了一套JAVA开发技术资料,内容涵盖java基础,分布式、微服务等主流技术资料,包含大厂面经,学习笔记、源码讲义、项目实战、讲解视频。
希望可以帮助一些想通过自学提升能力的朋友,领取资料,扫码关注一下
记得转发关注私信
私信回复【2022学习资料】
领取更多学习资料
辽篮为什么差点输给首钢?杨鸣掏心窝说心里话,说得很实在辽篮为什么差点输给首钢?杨鸣掏心窝说心里话,说得很实在比赛还剩最后8。3秒,郭艾伦站上罚球线2罚1中,到此辽宁男篮和北京首钢第一场对决的烽火硝烟散去,最终辽宁男篮以83比……
挥刀自宫!欧盟去工业化再上台阶作者:老范1hr最近,欧盟又整大活了。三大机构欧盟委员会、欧盟议会和成员国宣布达成一项历史性协议:将从2035年起禁止生产新的燃油车。注意是禁止生产,不是禁售……
旅居海南参团旅游。南山。牛王岛旅居生活旅游记录美好退休生活也能多彩第三天导游安排的,到南山景区。因为人多嘛,需要早点去排队,很早就拉我们到了景区门外,早已排成了长长的队伍等候进去。南山位于……
小雪吃8宝,不把医生找!11月22日小雪,8宝指啥?怎么做秋日生活打卡季没了烟火气,人生就是一段孤独的旅程。小雪吃8宝,不把医生找!11月22日小雪,8宝指啥?怎么做,快快收藏吧!第一宝羊肉:推荐食谱牛羊肉烧萝卜又到……
如何快速消除眼袋眼睛底下经常挂着暗沉浮肿的眼袋?你可能需要长期治疗,才能解决根本问题,从而永久消除眼袋。不过,也有一些救急的权宜之计可以暂时减轻、消除或遮盖眼袋,效果可以持续几小时甚至几天。虽……
电车不是新能源,氢动力才是!车企的营销手段还能骗多久?目前,新能源汽车取代传统燃油车的趋势已然十分明朗,基本已经成了行业内的共识。消费者对新能源汽车的接受度也越来越高,渗透率以远超预期的速度增长。而我国在新能源汽车领域,无论是技术……
年老的苦,从50岁开始年老的苦,从50岁开始!中年的人要比真老年的人要难过,上有老下有小,夹在中间最难受。要是两边都不知足不听话的就是催人死得快点!除了那些老人,偏执的或者没心没肺自私自利的不管不问……
疏肝解郁,肝病自然随之而去点击上方关注我们疏肝解郁,肝病自然随之而去肝郁,又称肝气郁结综合征,是指以气机郁滞、肝失疏泄为特征的综合征,主要表现为情绪低落抑郁、胸胁或少腹胀痛。简单地说,……
人民网评张翰新剧低至2。2分俗套脱离现实等永远不可能成为卖点《东八区的先生们》开播不久,就迎来豆瓣2。4分的差评潮,如今已经降到2。2分。14日,人民网评该剧热搜第一。张翰搂抱女主时触及其胸部等镜头,被网友认为是十分冒犯女性的细节,需要……
有声绘本丨图图姐姐喊你听故事飞翔的语言小朋友们,图图姐姐今天要讲的故事叫做《飞翔的语言》。语言会飞吗?会飞到哪里呢?奶奶说:我觉得我脑袋里的词语正一个一个地往外飞。那么,它们为什么会飞走呢?飞到了哪里?还会飞回来吗……
后来居上?三款主流烹饪机器人体验,米家烹饪机器人值得入手吗?一、年轻人无法规避的懒人经济懒人经济看似是一个很新很潮的名词,但实际上,几年前懒人经济就已经开始渗透到年轻人生活的许多方面了。如果说外卖、跑腿的盛行是一种高效的懒话,那么……
入秋后,别嫌这5菜贵,富含多种氨基酸,孩子常吃有助提高抵抗力入秋之后,天气开始明显变得早晚凉快许多,胃口也开了,孩子们也陆续开学了,孩子上学后,作息规律了。一日三餐不仅要吃饱,更要吃好,这样才能精力学习。入秋后,别嫌这5菜贵,富含多种氨……