黑魔法 JavaAgent,还有不会的吗 ?
点击关注公众号,Java干货及时送达👇


premain
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 HelloPremain {/*** premain()有两种写法,Instrumentation参数可以不传递,带有Instrumentation参数的方法优先级更高** @param agentArgs 字符串参数,可以在启动的时候手动传入* @param inst 此参数由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.");}/*** 这个方法没有上面那个方法的优先级高,程序运行时会优先找上面那个方法,如果没找到,才会用这个方法** @param agentArgs 字符串参数,可以在启动的时候手动传入*/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参数可以不传递,带有Instrumentation参数的方法优先级更高** @param agentArgs 字符串参数,可以在启动的时候手动传入* @param inst 此参数由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.");}/*** 这个方法没有上面那个方法的优先级高,程序运行时会优先找上面那个方法,如果没找到,才会用这个方法** @param agentArgs 字符串参数,可以在启动的时候手动传入*/public static void agentmain(String agentArgs) {System.out.println("Yes, I am a real Agent Class.");}}
Instrumentation
大家重点看 premain 和 agentmain 的第二个参数,如果我们想要在后续对 java 字节码进行修改,那么就必须通过 Instrumentation 来实现,它由 JVM 传入,是 JDK1.5 提供的 API,用于拦截类加载事件,并对字节码进行修改。
public interface Instrumentation {//注册一个转换器,类加载事件会被注册的转换器所拦截void addTransformer(ClassFileTransformer transformer, boolean canRetransform);//重新触发类加载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 {/*** 转换提供的类文件并返回一个新的替换类文件** @param loader 要转换的类的定义加载器,如果引导加载器可能为null* @param className Java 虚拟机规范中定义的完全限定类和接口名称的内部形式的类名称。例如, "java/util/List" 。* @param classBeingRedefined 如果这是由重新定义或重新转换触发的,则该类被重新定义或重新转换;如果这是一个类加载, null* @param protectionDomain 被定义或重新定义的类的保护域* @param classfileBuffer 类文件格式的输入字节缓冲区 - 不得修改* @return 转换后的字节码数组* @throws IllegalClassFormatException class文件格式异常*/public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {// 实现ClassFileTransformer接口之后,这里默认是return了一个[]空数组// 千万注意:// 如果不需要改变,那就return null,如果return了一个[],那么你的class就会被置空,程序就会报错// return new byte[0];return null;}}
在实战之前,我们来了解一下 JavaAgent 的使用方式。
正常我们运行一个 java 程序,都是需要找到一个 main 方法入口,如果是 jar 包的话,一般都是直接 java -jar
如果我们是用的是 premain 方式,那我们直接通过追加 -javaagent 参数来引入 agent。

java -javaagent:/Users/wolffy/agent.jar -jar /Users/wolffy/test.jar如果我们是用的是 agent 方式,那么就需要借助于 JDK 的 tools.jar 中的 API 了。
// 连接jvm,并利用相关的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;/*** 预先处理,程序启动时优先加载,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 方式使用起来简单便捷,但是有一个致命的问题,因为它在应用程序启动之前执行,一旦它出现了问题,就会导致应用程序也启不来,所以必须保证 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) {// 模拟应用程序,让其长时间运行}}}
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 {// 创建socket服务器ServerSocket server = new ServerSocket(9876);System.out.println("启动Socket Server完毕,开始监听9876端口\n");// 选择java应用程序的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 + "的应用程序加载agent完毕\n");// 接收socket客户端链接,这里会阻塞,直到有客户端连接上来Socket client = server.accept();// 获取客户端远程地址信息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,并利用相关的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 客户端发来的消息* @throws IOException IO异常*/private static String receiveMsg(Socket socket) throws IOException {// 打开客户端的输入流InputStream is = socket.getInputStream();// 创建字节流到字符流的桥接InputStreamReader isr = new InputStreamReader(is);// 借助缓存流来进行缓冲文本读取BufferedReader br = new BufferedReader(isr);// 读取一个文本行String msg = br.readLine();// 关闭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服务端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 + "]");// 关闭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) {// // 模拟应用程序,让其长时间运行// }}}
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 {/*** 转换提供的类文件并返回一个新的替换类文件** @param loader 要转换的类的定义加载器,如果引导加载器可能为null* @param className Java 虚拟机规范中定义的完全限定类和接口名称的内部形式的类名称。例如, "java/util/List" 。* @param classBeingRedefined 如果这是由重新定义或重新转换触发的,则该类被重新定义或重新转换;如果这是一个类加载, null* @param protectionDomain 被定义或重新定义的类的保护域* @param classfileBuffer 类文件格式的输入字节缓冲区 - 不得修改* @return 转换后的字节码数组* @throws IllegalClassFormatException class文件格式异常*/@Overridepublic byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {// 加入了自定义的transformer之后,所有需要加载的、但是还没有加载的类,当它们每一个要加载的时候,就需要通过transform方法// 所以需要加一个判断,只处理AgentTest类if (className.equals("com/wolffy/hello/test/AgentTest")) {System.out.println("<----------------- agent加载生效,开始更改class字节码 ----------------->");return dumpAgentTest();}// 其他AgentTest之外的类,我们直接返回null,就代表没有任何修改,还是用它原来的class// return new byte[0];return null;}/*** 更改AgentTest的字节码,将system.out打印的内容给替换掉*
* 设计到asm更改字节码的知识点,网络上很少有一个完整的知识体系,偶尔找到两篇文章也还都是抄来抄去,这里给大家提供一个学习的地址:https://lsieun.github.io/java/asm/index.html** @return 更改之后的class文件字节流*/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;/*** 预先处理,程序启动时优先加载,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通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上。
PS:因公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。
点“在看”支持小哈呀,谢谢啦😀

