黑魔法 JavaAgent,还有不会的吗 ?
小哈学Java
共 19596字,需浏览 40分钟
·
2022-02-26 16:15
点击关注公众号,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,也就是PID
VirtualMachine vm = VirtualMachine.attach("12345");
// 加载agent,大家注意使用自己的路径
vm.loadAgent("/Users/wolffy/agent.jar");
// 脱离jvm
vm.detach();
Manifest-Version: 1.0
Premain-Class: com.wolffy.hello.HelloAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Can-Set-Native-Method-Prefix: true
Build-Jdk-Spec: 1.8
Created-By: Maven Jar Plugin 3.2.0
Main-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.0
Agent-Class: com.wolffy.hello.HelloAgentmain
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Can-Set-Native-Method-Prefix: true
Build-Jdk-Spec: 1.8
Created-By: Maven Jar Plugin 3.2.0
Main-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应用程序的pid
Scanner scanner = new Scanner(System.in);
System.out.print("请输入PID: ");
String pid = scanner.next();
System.out.println();
// 模拟arthas选择pid动作 + 加载agent
simulationAndLoad(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,也就是PID
VirtualMachine vm = VirtualMachine.attach(pid);
// 加载agent,大家注意使用自己的路径
vm.loadAgent("D:\\workspace_idea\\HelloAgent\\target\\HelloAgent.jar");
// 脱离jvm
vm.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();
// 关闭IO
br.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 + "]");
// 关闭IO
pw.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文件格式异常
*/
@Override
public 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.");
// 引入自定义的transformer
inst.addTransformer(new HelloTransformer(), true);
}
}
扫描二维码获取
更多精彩
Java者说
最近面试BAT,整理一份面试资料《Java面试BATJ通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上。
PS:因公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。
点“在看”支持小哈呀,谢谢啦😀
评论