JDK1.6“新“特性Instrumentation之JavaAgent(推荐)
大火yzs 人气:0本文着重讲解了JDK1.6“新“特性Instrumentation之JavaAgent,本文为大家做详细介绍,希望能够帮助到您,欢迎大家阅读和收藏
简介
Java Agent是在JDK1.5以后,我们可以使用agent技术构建一个独立于应用程序的代理程序(即为Agent),用来协助监测、运行甚至替换其他JVM上的程序。使用它可以实现虚拟机级别的AOP功能。
Agent分为两种,一种是在主程序之前运行的Agent,一种是在主程序之后运行的Agent(前者的升级版,1.6以后提供)。
JavaAgent的作用Agent给我们程序带来的影响.jpg
使用Agent-premain方法影响的程序效果图.jpg
使用Agent-agentmain方法影响的程序效果图.jpg
JavaAgent相关的API
在java.lang.instrument包下 给我们提供了相关的API
而最为主要的就是Instrumentation这个接口中的几个方法
public interface Instrumentation { /** * 添加Transformer(转换器) * ClassFileTransformer类是一个接口,通常用户只需实现这个接口的 byte[] transform()方法即可; * transform这个方法会返回一个已经转换过的对象的byte[]数组 * @param transformer 拦截器 * @return canRetransform 是否能重新转换 */ void addTransformer(ClassFileTransformer transformer, boolean canRetransform); /** * 重新触发类加载, * 该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名 * @param classes Class对象 * @throws UnmodifiableClassException 异常 */ void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; /** * 直接替换类的定义 * 重新转换某个对象,并已一个新的class格式,进行转化。 * 该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名 * @param definitions ClassDefinition对象[Class定义对象] * @throws ClassNotFoundException,UnmodifiableClassException 异常 */ void redefineClasses(ClassDefinition... definitions)throws ClassNotFoundException, UnmodifiableClassException; /** * 获取当前被JVM加载的所有类对象 * @return Class[] class数组 */ Class[] getAllLoadedClasses(); }
后面我们会在代码中具体用到这些方法。再详细说明。
JavaAgent-premain方法1-初探效果:
实现main方法前执行业务逻辑
Agent1.java
public class Agent1 { public static void premain(String agent){ System.out.println("Agent1 premain :" + agent); } }
Demo1.java
public class Demo1 { /** * VM参数 * -javaagent:D:\desktop\text\code\mycode\JavaAgentDemo\agent\target/agent.jar=input * */ public static void main(String[] args) throws Exception { System.out.println("demo1"); } }
resources/META-INF/MANIFEST.MF
Manifest-Version: 1.0 Archiver-Version: Plexus Archiver Built-By: dahuoyzs Created-By: Apache Maven 3.6.0 Build-Jdk: 1.8.0_171 Premain-Class: cn.bigfire.Agent1 Can-Retransform-Classes: true
运行效果
Agent1 premain :input
demo1
JavaAgent-premain方法2-实现修改代码逻辑效果:
实现 修改 程序源代码 hello -> hello agented
Agent2.java
public class Agent2 { /** * 可以运行在main方法启动前 * @param agent 输入的参数 * @param instrumentation 输入的参数 */ public static void premain(String agent, Instrumentation instrumentation){ System.out.println("Agent2 premain 2param :" + agent); instrumentation.addTransformer(new ConsoleTransformer(),true); } }
ConsoleTransformer.java
public class ConsoleTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if (className.equals("cn/bigfire/Console")){ String root = StrUtil.subBefore(System.getProperty("user.dir"), "JavaAgentDemo", true); String classFile = root + "JavaAgentDemo/agent/src/main/resources/Console.class"; return FileUtil.readBytes(classFile); } return classfileBuffer; } }
Demo2.java
public class Demo2 { /** * VM参数 * -javaagent:D:\desktop\text\code\mycode\JavaAgentDemo\agent\target/agent.jar=input * */ public static void main(String[] args) throws Exception { new Thread(()->{ while (true){ Console.hello();// public static void hello(){System.out.println("hello"); } ThreadUtil.sleep(2000); } }).start(); } }
resources/META-INF/MANIFEST.MF
Manifest-Version: 1.0 Archiver-Version: Plexus Archiver Built-By: dahuoyzs Created-By: Apache Maven 3.6.0 Build-Jdk: 1.8.0_171 Premain-Class: cn.bigfire.Agent2 Can-Retransform-Classes: true
运行效果
Agent2 premain 2param :input
满足条件
hello agented
hello agented
hello agented
hello agented
JavaAgent-premain方法3-无侵入动态修改程序源代码实现方法耗时统计效果:
实现main方法外的所有方法统计时间
Agent3.java
public class Agent3 { /** * 可以运行在main方法启动前 * @param agent 输入的参数 * @param instrumentation instrumentation对象由JVM提供并传入 */ public static void premain(String agent, Instrumentation instrumentation) { System.out.println("Agent3 premain :" + agent); instrumentation.addTransformer(new TimeCountTransformer()); } /** * 时间统计Transformer 给要代理的方法添加时间统计 */ private static class TimeCountTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { try { className = className.replace("/", "."); if (className.equals("cn.bigfire.Demo3")) { //使用全称,用于取得字节码类<使用javassist> CtClass ctclass = ClassPool.getDefault().get(className); //获得方法列表 CtMethod[] methods = ctclass.getDeclaredMethods(); //给方法设置代理 Stream.of(methods).forEach(method-> agentMethod(ctclass,method)); //CtClass转byte[]数组 return ctclass.toBytecode(); } } catch (Exception e) { e.printStackTrace(); } return null; } } /** * 代理方法,把传入的方法经写代理,并生成带时间统计的方法, * @param ctClass javassist的Class类 * @param ctMethod javassist的ctMethod方法 * */ public static void agentMethod(CtClass ctClass,CtMethod ctMethod){ try { String mName = ctMethod.getName(); if (!mName.equals("main")){//代理除了main方法以外的所有方法 String newName = mName + "$Agent"; ctMethod.setName(newName); CtMethod newMethod = CtNewMethod.copy(ctMethod, mName, ctClass, null); // 构建新的方法体 String bodyStr = "{\n" + "long startTime = System.currentTimeMillis();\n" + newName + "();\n" + "long endTime = System.currentTimeMillis();\n" + "System.out.println(\""+newName+"() cost:\" +(endTime - startTime));\n" + "}"; newMethod.setBody(bodyStr);// 替换新方法 ctClass.addMethod(newMethod);// 增加新方法 } }catch (Exception e){ e.printStackTrace(); } } }
Demo3.java
public class Demo3 { /** * VM参数 * -javaagent:D:\desktop\text\code\mycode\JavaAgentDemo\agent\target/agent.jar=input */ public static void main(String[] args) throws Exception { sleep1(); sleep2(); } public static void sleep1(){ ThreadUtil.sleep(1000); } public static void sleep2(){ ThreadUtil.sleep(2000); } }
resources/META-INF/MANIFEST.MF
Manifest-Version: 1.0 Archiver-Version: Plexus Archiver Built-By: dahuoyzs Created-By: Apache Maven 3.6.0 Build-Jdk: 1.8.0_171 Class-Path: ../javassist-3.12.1.GA.jar Premain-Class: cn.bigfire.Agent3 Can-Retransform-Classes: true
运行效果
Agent3 premain :input
sleep1$Agent() cost:1005
sleep2$Agent() cost:2001
JavaAgent-agentmain方法1-实现运行时修改程序效果:
实现运行时 修改程序 hello -> hello agented
Agent4.java
public class Agent4 { public static void premain(String agent){ System.out.println("Agent4 premain 1param:" + agent); } public static void premain(String agent, Instrumentation instrumentation) { System.out.println("Agent4 premain 2param:" + agent); //premain时,由于堆里还没有相应的Class。所以直接addTransformer,程序就会生效。 // instrumentation.addTransformer(new ConsoleTransformer(),true); } public static void agentmain(String agent, Instrumentation instrumentation){ System.out.println("Agent4 agentmain 2param :" + agent); instrumentation.addTransformer(new ConsoleTransformer(),true); //agentmain运行时 由于堆里已经存在Class文件,所以新添加Transformer后 // 还要再调用一个 inst.retransformClasses(clazz); 方法来更新Class文件 for (Class clazz:instrumentation.getAllLoadedClasses()) { if (clazz.getName().contains("cn.bigfire.Console")){ try { instrumentation.retransformClasses(clazz); } catch (Exception e) { e.printStackTrace(); } } } } public static void agentmain(String agent){ System.out.println("Agent4 agentmain 1param :" + agent); } }
Demo4
public class Demo4 { /** * 打包agent4 -> 先运行demo2 -> 运行demo4 ->选择程序demo2结尾的程序,即可运行时修改文件 * VM参数 * -javaagent:D:\desktop\text\code\mycode\JavaAgentDemo\agent\target/agent.jar=input * */ public static void main(String[] args) throws Exception { while (true){ List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (int i = 0; i < list.size(); i++) { VirtualMachineDescriptor jvm = list.get(i);; System.out.println("[" +i+ "]ID:"+jvm.id()+",Name:"+jvm.displayName()); } System.out.println("请选择第几个"); Scanner scanner = new Scanner(System.in); int s = scanner.nextInt(); VirtualMachineDescriptor virtualMachineDescriptor = list.get(s); VirtualMachine attach = VirtualMachine.attach(virtualMachineDescriptor.id()); String root = StrUtil.subBefore(System.getProperty("user.dir"), "JavaAgentDemo", true); String agentJar = root + "JavaAgentDemo\\agent\\target\\agent.jar"; File file = new File(agentJar); System.out.println(file.exists()); attach.loadAgent(agentJar,"param"); attach.detach(); } } }
resources/META-INF/MANIFEST.MF
Manifest-Version: 1.0 Archiver-Version: Plexus Archiver Built-By: dahuoyzs Created-By: Apache Maven 3.6.0 Build-Jdk: 1.8.0_171 Premain-Class: cn.bigfire.Agent4 Agent-Class: cn.bigfire.Agent4 Can-Retransform-Classes: true Can-Redefine-Classes: true
此时的运行顺序
打包agent4 -> 先运行demo2 -> 运行demo4 ->选择程序demo2结尾的程序,即可运行时修改文件
运行效果
Demo2
Agent4 premain 2param:input
hello
hello
Demo4
[0]ID:12480,Name:cn.bigfire.Demo2 [1]ID:14832,Name:org.jetbrains.kotlin.daemon.KotlinCompileDaemon --daemon-runFilesPath xxx [2]ID:14864,Name: [3]ID:3952,Name:cn.bigfire.Demo4 [4]ID:14852,Name:org.jetbrains.idea.maven.server.RemoteMavenServer36 [5]ID:11928,Name:org.jetbrains.jps.cmdline.Launcher xxx 请选择第几个 0 true
Demo2
Agent4 premain 2param:input hello hello Agent4 agentmain 2param :param hello agented hello agented hello agented
JavaAgent-agentmain方法2-实现动态修改日志级别效果:
实现运行时 修改程序 模拟项目中的动态日志 info <-> debug
Agent5.java
public class Agent5 { public static void premain(String agent, Instrumentation instrumentation){ System.out.println("Agent5 premain 2param :" + agent); instrumentation.addTransformer(new StartTransformer(),true); //这个方式不行。因为启动时Class都还没有呢。 // for (Class clazz:inst.getAllLoadedClasses()) { // if (clazz.getName().equals("cn.bigfire.LogLevelStarter")){ // try { // switchDebug(clazz); // instrumentation.retransformClasses(clazz); // } catch (Exception e) { // e.printStackTrace(); // } // } // } } public static void agentmain(String agent, Instrumentation instrumentation){ System.out.println("Agent5 agentmain 2param :" + agent); for (Class clazz:instrumentation.getAllLoadedClasses()) { if (clazz.getName().equals("cn.bigfire.LogLevelStarter")){ try { switchAtomicDebug(clazz); instrumentation.retransformClasses(clazz); } catch (Exception e) { e.printStackTrace(); } } } } public static class StartTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { //此时由于classBeingRedefined是空,所以还是不能用这个Class修改属性呢,只能通过 读取byte[]往堆里丢,才能用。 if (className.equals("cn/bigfire/LogLevelStarter")){ //【这是一个错误的思路】 premain的时候 classBeingRedefined是空的因为很多的Class还没加载到堆中 // if (classBeingRedefined!=null){ // switchDebug(classBeingRedefined); // return toBytes(classBeingRedefined); // } //正常的读取一共文件byte[]数组 String root = StrUtil.subBefore(System.getProperty("user.dir"), "JavaAgentDemo", true); String classFile = root + "JavaAgentDemo/agent/src/main/resources/LogLevelStarter.class"; return FileUtil.readBytes(classFile); } return classfileBuffer; } } /** * 可序列化对象转byte[]数组 * @param clazz 要转byte[]数组的对象 * @return byte[] 返回byte[]数组 */ public static byte[] toBytes(Serializable clazz){ try { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream stream = new ObjectOutputStream(byteArrayOutputStream); stream.writeObject(clazz); return byteArrayOutputStream.toByteArray(); }catch (Exception e){ e.printStackTrace(); } return null; } public static void switchDebug(Class clazz){ try { Field field1 = clazz.getDeclaredField("isDebug"); field1.setAccessible(true); boolean debug = field1.getBoolean(clazz); field1.setBoolean(clazz,!debug); }catch (Exception e){ e.printStackTrace(); } } public static void switchAtomicDebug(Class clazz){ try { Field field2 = clazz.getDeclaredField("atomicDebug"); field2.setAccessible(true); AtomicBoolean atomicDebug = (AtomicBoolean)field2.get(clazz); atomicDebug.set(!atomicDebug.get()); }catch (Exception e){ e.printStackTrace(); } } }
注意,需要先把LogLevelStarter.java中的isDebug 改为true编译一下。放到src/main/resources/目录下;
LogLevelStarter.java
public class LogLevelStarter { public static volatile boolean isDebug = false; public static AtomicBoolean atomicDebug = new AtomicBoolean(false); /** * VM参数 * -javaagent:D:\desktop\text\code\mycode\JavaAgentDemo\agent\target/agent.jar=input */ public static void main(String[] args) throws Exception { new Thread(()->{ for (;;){ //死循环,每隔两秒打印一个日志。 System.out.print(isDebug ? "volatile debug" : "volatile info"); System.out.print("\t"); System.out.println(atomicDebug.get() ? "atomicDebug debug" : "atomicDebug info"); ThreadUtil.sleep(2000); } }).start(); } }
resources/META-INF/MANIFEST.MF
Manifest-Version: 1.0 Archiver-Version: Plexus Archiver Built-By: dahuoyzs Created-By: Apache Maven 3.6.0 Build-Jdk: 1.8.0_171 Premain-Class: cn.bigfire.Agent5 Agent-Class: cn.bigfire.Agent5 Can-Retransform-Classes: true Can-Redefine-Classes: true
此时的运行顺序
打包agent5 -> 先运行LogLevelStarter -> 运行demo4 ->选择程序LogLevelStarter结尾的程序,即可运行时修改文件
运行效果
LogLevelStarter
Agent5 premain 2param :input
volatile debug atomicDebug info
volatile debug atomicDebug info
Demo4
[0]ID:12592,Name:cn.bigfire.LogLevelStarter [1]ID:12880,Name:cn.bigfire.Demo4 [2]ID:14832,Name:org.jetbrains.kotlin.daemon.KotlinCompileDaemon --daemon-runFilesPath xxx [3]ID:14864,Name: [4]ID:14852,Name:org.jetbrains.idea.maven.server.RemoteMavenServer36 [5]ID:8116,Name:org.jetbrains.jps.cmdline.Launcher xxx 请选择第几个 0 true
LogLevelStarter
Agent5 premain 2param :input volatile debug atomicDebug info volatile debug atomicDebug info Agent5 agentmain 2param :param volatile debug atomicDebug debug volatile debug atomicDebug debug
在Agent5中,其实使用premain和agentmain。
premain把volatile修饰的isDbug给修改为true了。
而agentmain时把atomicDebug的值进行多次取反操作。
自己实现一个热部署功能的大致思路
当运行完本项目中的几个demo之后。
读者可能对Java Agent有了一些基本的概念
最起码我们知道了premain是可以运行在main函数前的。
而agentmain是可以在程序运行时,修改程序内的一些类文件的。
那么热部署很明显就是使用的agentmain这个特性了
那么热部署具体应该怎么实现呢?
这里先有个大概的思路。后续如果有经历,可以简单按照这个思路实现一下
思路
当我们文件发生修改的时候,项目会重新加载我们的类。
那么这里肯定会涉及到文件变化的观察。 即 观察者设计模式跑不了
首先递归当前项目目录。并根据文件类型,如(.java ,xml,yml等)将此类文件注册观察者模式。
当文件内容发生变化时,会调用 监听器中的回调方法;
在回调中完成如下(具体实现时未必需要)
使用Java1.6的JavaCompiler编译Java文件;
自定义ClassLoader 装载 编译好的Class到堆中
使用agentmain修改原Class文件替换成新的Class文件
完成热加载
JavaAgent的应用场景
apm:(Application Performance Management)应用性能管理。pinpoint、cat、skywalking等都基于Instrumentation实现
idea的HotSwap、Jrebel等热部署工具
应用级故障演练
Java诊断工具Arthas、Btrace等
{ "author": "大火yzs", "title": "【JavaAgent】JavaAgent入门教程", "tag": "JavaAgent,Instrumentation,运行时动态修改源程序", "createTime": "2020-08-02 18:30" }
总结
加载全部内容