簡介
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這個接口中的幾個方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
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
1
2
3
4
5
|
public class Agent1 { public static void premain(String agent){ System.out.println( "Agent1 premain :" + agent); } } |
Demo1.java
1
2
3
4
5
6
7
8
9
10
|
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
1
2
3
4
5
6
7
|
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
1
2
3
4
5
6
7
8
9
10
11
12
|
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
1
2
3
4
5
6
7
8
9
10
11
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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
1
2
3
4
5
6
7
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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
1
2
3
4
5
6
7
8
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
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
1
2
3
4
5
6
7
8
9
|
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
1
2
3
4
5
6
7
8
9
|
[ 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
1
2
3
4
5
6
7
|
Agent4 premain 2param:input hello hello Agent4 agentmain 2param :param hello agented hello agented hello agented |
JavaAgent-agentmain方法2-實現動態修改日志級別效果:
實現運行時 修改程序 模擬項目中的動態日志 info <-> debug
Agent5.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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
1
2
3
4
5
6
7
8
9
|
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
1
2
3
4
5
6
7
8
9
|
[ 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
1
2
3
4
5
6
|
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等
源代碼
1
2
3
4
5
6
|
{ "author" : "大火yzs" , "title" : "【JavaAgent】JavaAgent入門教程" , "tag" : "JavaAgent,Instrumentation,運行時動態修改源程序" , "createTime" : "2020-08-02 18:30" } |
總結
到此這篇關于JDK1.6“新“特性Instrumentation之JavaAgent的文章就介紹到這了,更多相關JDK1.6“新“特性Instrumentation內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://blog.csdn.net/qq_34173920/article/details/107748852