文章插图
文章插图
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() 。
public class MyAgentClass {// JVM优先执行该方法public static void premain(String agentArgs, Instrumentation inst) {System.err.println("main before...");}public static void premain(String agentArgs) {System.err.println("main before...");}}
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.jar JavaApp
主程序运行后Agent的步骤:这种是针对已经运行的JVM进程,我们可以通过attach机制,启动一个新的JVM进程发送指令给它执行 。
1、编写Agent类,该类必须有静态方法agentmain() 。
public class MyAgentClass {public static void agentmain(String agentArgs, Instrumentation inst) {System.err.println("main after...");}}
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进程 。public static void main(String[] args) throws Exception {VirtualMachine vm = VirtualMachine.attach("8080");vm.loadAgent("/dev/agent.jar");}
3. 相关组件3.1 Instrumentation【项目源码在哪里找 源代码在哪找】编写的AgentClass类必须有premain()方法,其中一个比较重要的参数就是Instrumentation 。它是JavaAgent技术用到的主要API,接口定义如下:
public interface Instrumentation {/** * 添加Class文件转换器,底层采用数组存储 * JVM加载Class文件前,需要依次经过转换 * @param transformer * @param canRetransform 是否允许转换 */void addTransformer(ClassFileTransformer transformer, boolean canRetransform);void addTransformer(ClassFileTransformer transformer);// 删除Class文件转换器boolean removeTransformer(ClassFileTransformer transformer);boolean isRetransformClassesSupported();// 重新转换Classvoid retransformClasses(Class<?>... classes) throws UnmodifiableClassException;boolean isRedefineClassesSupported();// 重新定义Class,热更新void redefineClasses(ClassDefinition... definitions)throwsClassNotFoundException, UnmodifiableClassException;boolean isModifiableClass(Class<?> theClass);@SuppressWarnings("rawtypes")Class[] getAllLoadedClasses();@SuppressWarnings("rawtypes")Class[] getInitiatedClasses(ClassLoader loader);// 获取对象大小long getObjectSize(Object objectToSize);void appendToBootstrapClassLoaderSearch(JarFile jarfile);void appendToSystemClassLoaderSearch(JarFile jarfile);boolean isNativeMethodPrefixSupported();void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);}
重要的方法笔者已经写上注释了,本文会用到的方法主要是addTransformer() 。它可以用来添加Class转换器,JVM在加载Class前,会先经过这些转换器进行加工 。3.2 ClassFileTransformer
Class文件转换器,JVM加载某个Class前,会先经过它转换,我们可以在这里去修改字节码以达到功能增强的目的 。它只有一个方法transform():
public interface ClassFileTransformer{/** * 转换Class * @param loader 类加载器 * @param className 类名 * @param classBeingRedefined 原始Class * @param ProtectionDomain* @param classfileBuffer Class文件字节数组 */byte[] transform(ClassLoader loader,String className,Class<?>classBeingRedefined,ProtectionDomain protectionDomain,byte[]classfileBuffer)throws IllegalClassFormatException;}
本文主要用到的就是classfileBuffer,有了Class的字节数组,只要把它导出到磁盘,通过IDEA反编译就能看到源码了 。4. 实战
【需求】
支持将任意Java对象的Class文件导出到磁盘,通过反编译查看源码,包括动态生成的类 。
【实现】
1、编写InstrumentationHolder,持有Instrumentation实例,后续操作全靠它 。
public class InstrumentationHolder {private static Instrumentation INSTANCE;public static void init(Instrumentation ins) {INSTANCE = ins;}public static Instrumentation get() {if (INSTANCE == null) {throw new RuntimeException("检查 -javaagent 配置");}return INSTANCE;}}
2、编写MyAgentClass,保存Instrumentation实例 。public class MyAgentClass {public static void premain(String agentArgs, Instrumentation inst) {System.err.println("main before...");InstrumentationHolder.init(inst);}}
3、编写ClassDumpTransformer,获取Class文件字节数组,导出到磁盘 。public class ClassDumpTransformer implements ClassFileTransformer {private final File file;private final Set<Class<?>> classes = new HashSet<>();public ClassDumpTransformer(String path, Class<?>... classes) {this.file = new File(path);this.classes.addAll(Arrays.asList(classes));[email protected] byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {if (classes.contains(classBeingRedefined)) {FileUtil.writeBytes(classfileBuffer, file);}return null;}}
4、编写ClassUtil工具类,支持导出Class文件 。public class ClassUtil {public static void classDump(Class<?> c, String path) {ClassDumpTransformer transformer = new ClassDumpTransformer(path, c);Instrumentation inst = InstrumentationHolder.get();inst.addTransformer(transformer, true);try {inst.retransformClasses(c);} catch (UnmodifiableClassException e) {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文件导出 。public class AgentDemo {public static void main(String[] args) throws Exception {Object instance = Proxy.newProxyInstance(A.class.getClassLoader(), new Class[]{A.class}, new InvocationHandler() [email protected] Object invoke(Object proxy, Method method, Object[] args) throws Throwable {return null;}});ClassUtil.classDump(instance.getClass(),"/target/X.class");}public static interface A {void a();}}
7、设置-javaagent参数并启动程序 。java -javaagent:agent.jar AgentDemo
此时,target目录下就会生成X.class文件,通过IDEA打开即可看到JDK生成的代理类源码 。5. 总结
JavaAgent十分强大,通过它可以在JVM加载Class文件前修改字节码,甚至修改JVM已经加载的Class 。基于此,我们可以「零侵入」的对应用程序做增强,服务实现热部署等等 。
本文通过一个小示例,编写ClassFileTransformer实现类导出对象的Class文件,反编译查看其源码 。这对于ASM操作字节码、JDK动态代理等动态生成类的场景下,而我们又想看对象的具体实现时,提供了帮助 。
- 《STM32固件库使用手册》 stm32固件库在哪里下载
- 免费工程项目管理软件 免费的项目管理工具
- word2016格式工具栏在哪 格式工具栏在哪里
- 淘宝秒杀在哪儿 淘宝app秒杀在哪里找
- vs2013怎么新建c项目 vs2012如何新建c项目
- 安卓文件在哪个文件夹里 安卓的文件夹在哪里
- 软件项目试运行报告 运行测试报告模板
- 淘宝秒杀在哪里找手机 淘宝秒杀在哪里找手机平台
- win7字体库在哪个文件夹 win7系统字体在哪里
- 儿童教育加盟项目有哪些比较好 儿童教育类项目加盟