1. 前言
为什么会接触javaAgent呢?
这起源于笔者最近在读Dubbo的源码。Dubbo有一个很有意思的功能——SPI。它可以根据运行时的URI参数。自适应的调用特定的实现类。大致的原理其实也能猜到。无非就是生成一个代理类。反射解析URI参数里的值。然后再调用对应的实现类。虽然大概可以猜到实现原理。但毕竟只是猜想。抱着科学严谨的精神。还是想看看Dubbo的实现源码。此时就有了一个想法。能不能把Dubbo生成的代理对象的Class类Dump下来。然后反编译看看它的源码呢?
理论上是完全可行的。阿里有一个很好用的开源工具Arthas。它的jad命令就支持对JVM已经加载的类进行反编译查看源码。笔者把Arthas项目源码down下来了。查看以后发现。需要用到JavaAgent技术。
2. JavaAgent规范
在JDK1.5以后。我们可以使用JavaAgent技术。以「零侵入」的方式对Java程序做增强。例如阿里云的Arms应用监控服务。就可以通过JavaAgent的方式接入一个探针。它会把应用的运行数据上报到阿里云。开发者可以在后台查看到应用的运行数据。这种方式。不需要我们对应用做任何改动。就可以轻松实现应用监控。
JavaAgent是一种规范。它分为两类:主程序运行前Agent、主程序运行后Agent。它可以在JVM加载Class文件前。对字节码做修改。甚至允许修改已经加载过的Class。这样我们就可以对应用做增强、以及实现代码热部署。
主程序运行前Agent的步骤:
1、编写Agent类。该类必须有静态方法premain()。
publicclassMyAgentClass{ //JVM优先执行该方法 publicstaticvoidpremain(StringagentArgs,Instrumentationinst){ System.err.println("mainbefore..."); } publicstaticvoidpremain(StringagentArgs){ System.err.println("mainbefore..."); } }
2、在resources/META-INF目录下编写MANIFEST.MF文件。指定Premain-Class。然后将程序打成Jar包。
Manifest-Version:1.0 Can-Redefine-Classes:true Can-Retransform-Classes:true Premain-Class:top.javap.agent.MyAgentClass //注意。这里必须空一行
使用Maven构建程序时。也可使用如下配置。
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Premain-Class>top.javap.agent.MyAgentClass</Premain-Class> <Can-Retransform-Classes>true</Can-Retransform-Classes> <Can-Redefine-Classes>true</Can-Redefine-Classes> </manifestEntries> </archive> </configuration> </plugin>
3、启动目标程序时。指定JVM参数。如下:
java-javaagent:agent-1.0-SNApsHOT.jarJavaapp
主程序运行后Agent的步骤:
这种是针对已经运行的JVM进程。我们可以通过attach机制。启动一个新的JVM进程发送指令给它执行。
1、编写Agent类。该类必须有静态方法agentmain()。
publicclassMyAgentClass{ publicstaticvoidagentmain(StringagentArgs,Instrumentationinst){ System.err.println("mainafter..."); } }
2、在resources/META-INF目录下编写MANIFEST.MF文件。指定Premain-Class。然后将程序打成Jar包。
Manifest-Version:1.0 Can-Redefine-Classes:true Can-Retransform-Classes:true Agent-Class:top.javap.agent.MyAgentClass //注意。这里必须空一行
3、编写attach程序。启动并attach到目标JVM进程。
publicstaticvoidmain(String[]args)throwsException{ Virtualmachinevm=VirtualMachine.attach("8080"); vm.loadAgent("/dev/agent.jar"); }
3. 相关组件
3.1 Instrumentation
编写的AgentClass类必须有premain()方法。其中一个比较重要的参数就是Instrumentation。它是JavaAgent技术用到的主要API。接口定义如下:
publicinterfaceInstrumentation{ /** *添加Class文件转换器。底层采用数组存储 *JVM加载Class文件前。需要依次经过转换 *@paramtransformer *@paramcanRetransform是否允许转换 */ voidaddTransformer(ClassFileTransformertransformer,booleancanRetransform); voidaddTransformer(ClassFileTransformertransformer); //删除Class文件转换器 booleanremoveTransformer(ClassFileTransformertransformer); booleanisRetransformClassesSupported(); //重新转换Class voidretransformClasses(Class<?>...classes)throwsUnmodifiableClassException; booleanisRedefineClassesSupported(); //重新定义Class。热更新 voidredefineClasses(ClassDefinition...definitions) throwsClassNotFoundException,UnmodifiableClassException; booleanisModifiableClass(Class<?>theClass); @SuppressWarnings("rawtypes") Class[]getAllLoadedClasses(); @SuppressWarnings("rawtypes") Class[]getInitiatedClasses(ClassLoaderloader); //获取对象大小 longgetObjectSize(ObjectobjectToSize); voidappendToBootstrapClassLoaderSearch(JarFilejarfile); voidappendToSystemClassLoaderSearch(JarFilejarfile); booleanisNativeMethodPrefixSupported(); voidsetNativeMethodPrefix(ClassFileTransformertransformer,Stringprefix); }
重要的方法笔者已经写上注释了。本文会用到的方法主要是addTransformer()。它可以用来添加Class转换器。JVM在加载Class前。会先经过这些转换器进行加工。
3.2 ClassFileTransformer
Class文件转换器。JVM加载某个Class前。会先经过它转换。我们可以在这里去修改字节码以达到功能增强的目的。它只有一个方法transform():
publicinterfaceClassFileTransformer{ /** *转换Class *@paramloader类加载器 *@paramclassName类名 *@paramclassBeingRedefined原始Class *@paramProtectionDomain *@paramclassfileBufferClass文件字节数组 */ byte[]transform(ClassLoaderloader, StringclassName, Class<?>classBeingRedefined, ProtectionDomainprotectionDomain, byte[]classfileBuffer) throwsIllegalClassFormatException; }
本文主要用到的就是classfileBuffer。有了Class的字节数组。只要把它导出到磁盘。通过IDEA反编译就能看到源码了。
4. 实战
【需求】
支持将任意Java对象的Class文件导出到磁盘。通过反编译查看源码。包括动态生成的类。
【实现】
1、编写InstrumentationHolder。持有Instrumentation实例。后续操作全靠它。
publicclassInstrumentationHolder{ privatestaticInstrumentationINSTANCE; publicstaticvoidinit(Instrumentationins){ INSTANCE=ins; } publicstaticInstrumentationget(){ if(INSTANCE==null){ thrownewRuntimeException("检查-javaagent配置"); } returnINSTANCE; } }
2、编写MyAgentClass。保存Instrumentation实例。
publicclassMyAgentClass{ publicstaticvoidpremain(StringagentArgs,Instrumentationinst){ System.err.println("mainbefore..."); InstrumentationHolder.init(inst); } }
3、编写ClassDumpTransformer。获取Class文件字节数组。导出到磁盘。
publicclassClassDumpTransformerimplementsClassFileTransformer{ privatefinalFilefile; privatefinalSet<Class<?>>classes=newHashSet<>(); publicClassDumpTransformer(Stringpath,Class<?>...classes){ this.file=newFile(path); this.classes.addAll(Arrays.asList(classes)); } @Override publicbyte[]transform(ClassLoaderloader,StringclassName,Class<?>classBeingRedefined,ProtectionDomainprotectionDomain,byte[]classfileBuffer)throwsIllegalClassFormatException{ if(classes.contains(classBeingRedefined)){ FileUtil.writeBytes(classfileBuffer,file); } returnnull; } }
4、编写ClassUtil工具类。支持导出Class文件。
publicclassClassUtil{ publicstaticvoidclassDump(Class<?>c,Stringpath){ ClassDumpTransformertransformer=newClassDumpTransformer(path,c); Instrumentationinst=InstrumentationHolder.get(); inst.addTransformer(transformer,true); try{ inst.retransformClasses(c); }catch(UnmodifiableClassExceptione){ e.printStackTrace(); }finally{ inst.removeTransformer(transformer); } } }
5、编写MANIFEST.MF文件。构建Jar包。
Manifest-Version:1.0 Can-Redefine-Classes:true Can-Retransform-Classes:true Premain-Class:top.javap.agent.MyAgentClass
6、编写测试类。利用JDK动态代理生成代理类。然后将代理类的Class文件导出。
publicclassAgentDemo{ publicstaticvoidmain(String[]args)throwsException{ Objectinstance=Proxy.newProxyInstance(A.class.getClassLoader(),newClass[]{A.class},newInvocationHandler(){ @Override publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{ returnnull; } }); ClassUtil.classDump(instance.getClass(), "/target/X.class"); } publicstaticinterfaceA{ voida(); } }
7、设置-javaagent参数并启动程序。
java-javaagent:agent.jarAgentDemo
此时。target目录下就会生成X.class文件。通过IDEA打开即可看到JDK生成的代理类源码。
5. 总结
JavaAgent十分强大。通过它可以在JVM加载Class文件前修改字节码。甚至修改JVM已经加载的Class。基于此。我们可以「零侵入」的对应用程序做增强。服务实现热部署等等。
本文通过一个小示例。编写ClassFileTransformer实现类导出对象的Class文件。反编译查看其源码。这对于ASM操作字节码、JDK动态代理等动态生成类的场景下。而我们又想看对象的具体实现时。提供了帮助。
以上就是由优质生活领域创作者 生活常识网 整理编辑的,如果觉得有帮助欢迎收藏转发~
本文地址:http://www.shenzhoubaby.com/5308.html,转载请说明来源于:生活常识网
声明:本站部分文章来自网络,如无特殊说明或标注,均为本站原创发布。如若本站内容侵犯了原著者的合法权益,可联系@qq.com进行处理。分享目的仅供大家学习与参考,不代表本站立场。