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.0Can-Redefine-Classes:trueCan-Retransform-Classes:truePremain-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.0Can-Redefine-Classes:trueCan-Retransform-Classes:trueAgent-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();//重新转换ClassvoidretransformClasses(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 ClassFileTransformerClass文件转换器 。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.0Can-Redefine-Classes:trueCan-Retransform-Classes:truePremain-Class:top.javap.agent.MyAgentClass
6、编写测试类 。利用JDK动态代理生成代理类 。然后将代理类的Class文件导出 。
publicclassAgentDemo{ publicstaticvoidmain(String[]args)throwsException{Objectinstance=Proxy.newProxyInstance(A.class.getClassLoader(),newClass[]{A.class},newInvocationHandler(){@OverridepublicObjectinvoke(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动态代理等动态生成类的场景下 。而我们又想看对象的具体实现时 。提供了帮助 。
- 什么是p2p终结者 p2p终结者有用吗
- 4g与4g+区别 4g+和4g有什么区别
- 红豆薏米粉怎么吃正确啊
- 红豆薏米粉功效有哪些
- 红豆薏米粉的禁忌有哪些
- 通用设备管理在哪里 通用设备管理在哪里苹果8
- iphone x运营商在哪里 iphonex如何显示运营商
- 有哪些真心值得推荐的淘宝男装店 淘宝男装潮牌店铺排行
- 电力猫的性能和电路本身质量的没有关系 电力猫的原理和缺陷
- 电饭锅做蛋糕详细步骤有哪些