黑魔法 JavaAgent,還有不會的嗎 ?
點擊關(guān)注公眾號,Java干貨及時送達(dá)??


premain
package com.wolffy.hello;import java.lang.instrument.Instrumentation;/*** 預(yù)先處理,程序啟動時優(yōu)先加載,JavaAgent.class - By 「Java者說」 -> https://www.jiweichengzhu.com/* Created by wolffy on 2022/2/15.*/public class HelloPremain {/*** premain()有兩種寫法,Instrumentation參數(shù)可以不傳遞,帶有Instrumentation參數(shù)的方法優(yōu)先級更高** @param agentArgs 字符串參數(shù),可以在啟動的時候手動傳入* @param inst 此參數(shù)由jvm傳入,Instrumentation中包含了對class文件操作的一些api,可以讓我們基于此來進行class文件的編輯*/public static void premain(String agentArgs, Instrumentation inst) {System.out.println("Yes, I am a real Agent for premain Class.");}/*** 這個方法沒有上面那個方法的優(yōu)先級高,程序運行時會優(yōu)先找上面那個方法,如果沒找到,才會用這個方法** @param agentArgs 字符串參數(shù),可以在啟動的時候手動傳入*/public static void premain(String agentArgs) {System.out.println("Yes, I am a real Agent Class.");}}
agentmain
package com.wolffy.hello;import java.lang.instrument.Instrumentation;/*** 后置處理,啟動時無需加載,可在程序啟動之后進行加載,JavaAgent.class - By 「Java者說」 -> https://www.jiweichengzhu.com/* Created by wolffy on 2022/2/15.*/public class HelloAgentmain {/*** agentmain()有兩種寫法,Instrumentation參數(shù)可以不傳遞,帶有Instrumentation參數(shù)的方法優(yōu)先級更高** @param agentArgs 字符串參數(shù),可以在啟動的時候手動傳入* @param inst 此參數(shù)由jvm傳入,Instrumentation中包含了對class文件操作的一些api,可以讓我們基于此來進行class文件的編輯*/public static void agentmain(String agentArgs, Instrumentation inst) {System.out.println("Yes, I am a real Agent for agentmain Class.");}/*** 這個方法沒有上面那個方法的優(yōu)先級高,程序運行時會優(yōu)先找上面那個方法,如果沒找到,才會用這個方法** @param agentArgs 字符串參數(shù),可以在啟動的時候手動傳入*/public static void agentmain(String agentArgs) {System.out.println("Yes, I am a real Agent Class.");}}
Instrumentation?
大家重點看 premain 和 agentmain 的第二個參數(shù),如果我們想要在后續(xù)對 java 字節(jié)碼進行修改,那么就必須通過 Instrumentation 來實現(xiàn),它由 JVM 傳入,是 JDK1.5 提供的 API,用于攔截類加載事件,并對字節(jié)碼進行修改。
public?interface?Instrumentation?{//注冊一個轉(zhuǎn)換器,類加載事件會被注冊的轉(zhuǎn)換器所攔截void addTransformer(ClassFileTransformer transformer, boolean canRetransform);//重新觸發(fā)類加載void retransformClasses(Class>... classes) throws UnmodifiableClassException;//直接替換類的定義void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException;}
ClassFileTransformer
package com.wolffy.hello;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.security.ProtectionDomain;/*** Transformer.class - By 「Java者說」 -> https://www.jiweichengzhu.com/* Created by wolffy on 2022/2/15.*/public class HelloTransformer implements ClassFileTransformer {/*** 轉(zhuǎn)換提供的類文件并返回一個新的替換類文件** @param loader 要轉(zhuǎn)換的類的定義加載器,如果引導(dǎo)加載器可能為null* @param className Java 虛擬機規(guī)范中定義的完全限定類和接口名稱的內(nèi)部形式的類名稱。例如, "java/util/List" 。* @param classBeingRedefined 如果這是由重新定義或重新轉(zhuǎn)換觸發(fā)的,則該類被重新定義或重新轉(zhuǎn)換;如果這是一個類加載, null* @param protectionDomain 被定義或重新定義的類的保護域* @param classfileBuffer 類文件格式的輸入字節(jié)緩沖區(qū) - 不得修改* @return 轉(zhuǎn)換后的字節(jié)碼數(shù)組* @throws IllegalClassFormatException class文件格式異常*/public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {// 實現(xiàn)ClassFileTransformer接口之后,這里默認(rèn)是return了一個[]空數(shù)組// 千萬注意:// 如果不需要改變,那就return null,如果return了一個[],那么你的class就會被置空,程序就會報錯// return new byte[0];return null;}}
在實戰(zhàn)之前,我們來了解一下 JavaAgent 的使用方式。
正常我們運行一個 java 程序,都是需要找到一個 main 方法入口,如果是 jar 包的話,一般都是直接 java -jar
如果我們是用的是 premain 方式,那我們直接通過追加 -javaagent 參數(shù)來引入 agent。

java?-javaagent:/Users/wolffy/agent.jar?-jar?/Users/wolffy/test.jar如果我們是用的是 agent 方式,那么就需要借助于 JDK 的 tools.jar 中的 API 了。
// 連接jvm,并利用相關(guān)的api找到HelloTest工程運行時的進程id,也就是PIDVirtualMachine vm = VirtualMachine.attach("12345");// 加載agent,大家注意使用自己的路徑vm.loadAgent("/Users/wolffy/agent.jar");// 脫離jvmvm.detach();
Manifest-Version:?1.0Premain-Class:?com.wolffy.hello.HelloAgentCan-Redefine-Classes: trueCan-Retransform-Classes: trueCan-Set-Native-Method-Prefix: trueBuild-Jdk-Spec: 1.8Created-By: Maven Jar Plugin 3.2.0Main-Class:?com.wolffy.hello.HelloMain

package com.wolffy.hello;import java.lang.instrument.Instrumentation;/*** 預(yù)先處理,程序啟動時優(yōu)先加載,JavaAgent.class - By 「Java者說」 -> https://www.jiweichengzhu.com/* Created by wolffy on 2022/2/15.*/public class HelloPremain {public static void premain(String agentArgs, Instrumentation inst) {System.out.println("Yes, I am a real Agent for premain Class.");}}
org.apache.maven.plugins maven-jar-plugin 3.2.0 com.wolffy.hello.HelloMain com.wolffy.hello.HelloPremain true true true

package com.wolffy.hello.test;/*** AgentTest.class - By 「Java者說」 -> https://www.jiweichengzhu.com/* Created by wolffy on 2022/2/15.*/public class AgentTest {public static void main(String[] args) {System.out.println("hello, here is hello test for agent.");}}
org.apache.maven.plugins maven-jar-plugin 3.2.0 com.wolffy.hello.test.AgentTest



雖然 premain 方式使用起來簡單便捷,但是有一個致命的問題,因為它在應(yīng)用程序啟動之前執(zhí)行,一旦它出現(xiàn)了問題,就會導(dǎo)致應(yīng)用程序也啟不來,所以必須保證 agent 程序100%可用。
Manifest-Version: 1.0Agent-Class: com.wolffy.hello.HelloAgentmainCan-Redefine-Classes: trueCan-Retransform-Classes: trueCan-Set-Native-Method-Prefix: trueBuild-Jdk-Spec: 1.8Created-By: Maven Jar Plugin 3.2.0Main-Class: com.wolffy.hello.HelloMain

package com.wolffy.hello.test;/*** AgentTest.class - By 「Java者說」 -> https://www.jiweichengzhu.com/* Created by wolffy on 2022/2/15.*/public class AgentTest {public static void main(String[] args) {System.out.println("hello, here is hello test for agent.");while (true) {// 模擬應(yīng)用程序,讓其長時間運行}}}
package com.wolffy.hello;import com.sun.tools.attach.AgentInitializationException;import com.sun.tools.attach.AgentLoadException;import com.sun.tools.attach.AttachNotSupportedException;import com.sun.tools.attach.VirtualMachine;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.net.ServerSocket;import java.net.Socket;import java.net.SocketAddress;import java.util.Scanner;/*** Main.class - By 「Java者說」 -> https://www.jiweichengzhu.com/* Created by wolffy on 2022/2/15.*/public class HelloMain {public static void main(String[] args) {System.out.println("Hello, I am a JavaAgent demo, created by https://www.jiweichengzhu.com/\n");try {// 創(chuàng)建socket服務(wù)器ServerSocket server = new ServerSocket(9876);System.out.println("啟動Socket Server完畢,開始監(jiān)聽9876端口\n");// 選擇java應(yīng)用程序的pidScanner scanner = new Scanner(System.in);System.out.print("請輸入PID: ");String pid = scanner.next();System.out.println();// 模擬arthas選擇pid動作 + 加載agentsimulationAndLoad(pid);System.out.println("為PID=" + pid + "的應(yīng)用程序加載agent完畢\n");// 接收socket客戶端鏈接,這里會阻塞,直到有客戶端連接上來Socket client = server.accept();// 獲取客戶端遠(yuǎn)程地址信息SocketAddress address = client.getRemoteSocketAddress();System.out.println("客戶端[" + address + "]已連接\n");String msg = receiveMsg(client);System.out.println("客戶端[" + address + "]說: " + msg + "\n");} catch (IOException e) {e.printStackTrace();}}/*** 模擬arthas選擇pid動作 + 加載agent** @param pid 進程ID*/private static void simulationAndLoad(String pid) {try {// 連接jvm,并利用相關(guān)的api找到HelloTest工程運行時的進程id,也就是PIDVirtualMachine vm = VirtualMachine.attach(pid);// 加載agent,大家注意使用自己的路徑vm.loadAgent("D:\\workspace_idea\\HelloAgent\\target\\HelloAgent.jar");// 脫離jvmvm.detach();} catch (AttachNotSupportedException | IOException | AgentLoadException | AgentInitializationException e) {e.printStackTrace();}}/*** 接收客戶端消息,只做演示使用,所以只使用一次就行了** @param socket 客戶端鏈接* @return 客戶端發(fā)來的消息* @throws IOException IO異常*/private static String receiveMsg(Socket socket) throws IOException {// 打開客戶端的輸入流InputStream is = socket.getInputStream();// 創(chuàng)建字節(jié)流到字符流的橋接InputStreamReader isr = new InputStreamReader(is);// 借助緩存流來進行緩沖文本讀取BufferedReader br = new BufferedReader(isr);// 讀取一個文本行String msg = br.readLine();// 關(guān)閉IObr.close();isr.close();is.close();return msg;}}
package com.wolffy.hello;import java.io.IOException;import java.io.OutputStream;import java.io.PrintWriter;import java.lang.instrument.Instrumentation;import java.net.Socket;/*** 后置處理,啟動時無需加載,可在程序啟動之后進行加載,JavaAgent.class - By 「Java者說」 -> https://www.jiweichengzhu.com/* Created by wolffy on 2022/2/15.*/public class HelloAgentmain {public static void agentmain(String agentArgs, Instrumentation inst) {System.out.println("Yes, I am a real Agent for agentmain Class.");try {// 連接socket服務(wù)端Socket socket = new Socket("127.0.0.1", 9876);// 打開輸出流OutputStream os = socket.getOutputStream();// 格式化輸出流,自帶刷新PrintWriter pw = new PrintWriter(os, true);String project = System.getProperty("user.dir");pw.println("hay, i am project [" + project + "]");// 關(guān)閉IOpw.close();os.close();socket.close();} catch (IOException e) {e.printStackTrace();}}}





package com.wolffy.hello.test;/*** AgentTest.class - By 「Java者說」 -> https://www.jiweichengzhu.com/* Created by wolffy on 2022/2/15.*/public class AgentTest {public static void main(String[] args) {System.out.println("hello, here is hello test for agent.");// while (true) {// // 模擬應(yīng)用程序,讓其長時間運行// }}}
agent 工程中,我們自定義一個 Transformer 類
package com.wolffy.hello;import jdk.internal.org.objectweb.asm.ClassWriter;import jdk.internal.org.objectweb.asm.MethodVisitor;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.security.ProtectionDomain;import static jdk.internal.org.objectweb.asm.Opcodes.*;/*** Transformer.class - By 「Java者說」 -> https://www.jiweichengzhu.com/* Created by wolffy on 2022/2/15.*/public class HelloTransformer implements ClassFileTransformer {/*** 轉(zhuǎn)換提供的類文件并返回一個新的替換類文件** @param loader 要轉(zhuǎn)換的類的定義加載器,如果引導(dǎo)加載器可能為null* @param className Java 虛擬機規(guī)范中定義的完全限定類和接口名稱的內(nèi)部形式的類名稱。例如, "java/util/List" 。* @param classBeingRedefined 如果這是由重新定義或重新轉(zhuǎn)換觸發(fā)的,則該類被重新定義或重新轉(zhuǎn)換;如果這是一個類加載, null* @param protectionDomain 被定義或重新定義的類的保護域* @param classfileBuffer 類文件格式的輸入字節(jié)緩沖區(qū) - 不得修改* @return 轉(zhuǎn)換后的字節(jié)碼數(shù)組* @throws IllegalClassFormatException class文件格式異常*/@Overridepublic byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {// 加入了自定義的transformer之后,所有需要加載的、但是還沒有加載的類,當(dāng)它們每一個要加載的時候,就需要通過transform方法// 所以需要加一個判斷,只處理AgentTest類if (className.equals("com/wolffy/hello/test/AgentTest")) {System.out.println("<----------------- agent加載生效,開始更改class字節(jié)碼 ----------------->");return dumpAgentTest();}// 其他AgentTest之外的類,我們直接返回null,就代表沒有任何修改,還是用它原來的class// return new byte[0];return null;}/*** 更改AgentTest的字節(jié)碼,將system.out打印的內(nèi)容給替換掉** 設(shè)計到asm更改字節(jié)碼的知識點,網(wǎng)絡(luò)上很少有一個完整的知識體系,偶爾找到兩篇文章也還都是抄來抄去,這里給大家提供一個學(xué)習(xí)的地址:https://lsieun.github.io/java/asm/index.html** @return 更改之后的class文件字節(jié)流*/private static byte[] dumpAgentTest() {ClassWriter cw = new ClassWriter(0);MethodVisitor mv;cw.visit(52, ACC_PUBLIC + ACC_SUPER, "com/wolffy/hello/test/AgentTest", null, "java/lang/Object", null);{mv = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null); mv.visitCode();mv.visitVarInsn(ALOAD, 0);mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false); mv.visitInsn(RETURN);mv.visitMaxs(1, 1);mv.visitEnd();}{mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);mv.visitCode();mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitLdcInsn("hello world - https://www.jiweichengzhu.com/");mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);mv.visitInsn(RETURN);mv.visitMaxs(2, 1);mv.visitEnd();}cw.visitEnd();return cw.toByteArray();}}
package com.wolffy.hello;import java.lang.instrument.Instrumentation;/*** 預(yù)先處理,程序啟動時優(yōu)先加載,JavaAgent.class - By 「Java者說」 -> https://www.jiweichengzhu.com/* Created by wolffy on 2022/2/15.*/public class HelloPremain {public static void premain(String agentArgs, Instrumentation inst) {System.out.println("Yes, I am a real Agent for premain Class.");// 引入自定義的transformerinst.addTransformer(new HelloTransformer(), true);}}





掃描二維碼獲取
更多精彩
Java者說




最近面試BAT,整理一份面試資料《Java面試BATJ通關(guān)手冊》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點“在看”,關(guān)注公眾號并回復(fù)?Java?領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
PS:因公眾號平臺更改了推送規(guī)則,如果不想錯過內(nèi)容,記得讀完點一下“在看”,加個“星標(biāo)”,這樣每次新文章推送才會第一時間出現(xiàn)在你的訂閱列表里。
點“在看”支持小哈呀,謝謝啦??

