阿尔萨斯 原理探究 agentmain实践

前言

在上一篇中,我们采用了Java SE 5 中的premain实现对虚拟机层面上的AOP,开发者只能在 premain 当中施展想象力,所作的 Instrumentation 也仅限与 main 函数执行前,这样的方式存在一定的局限性。

Java SE 6 针对这种状况做出了改进,开发者可以在 main 函数开始执行以后,再启动自己的 Instrumentation 程序,这种方式通过agentMain。本文就来记录一下如何通过agentMain的实现方式,具体细节大家可以看文章后的参考文章。

agentMain编写

1
2
3
4
5
6
7
8
9
10
11
12
13
public class IAgentMain {

public static void agentmain(String agentArgs, Instrumentation inst)
throws UnmodifiableClassException {
inst.addTransformer(new ITransformer(), true);
/**
* 这段代码的意思是,重新转换目标类,也就是 Account 类。也就是说,你需要重新定义哪个类,需要指定,否则 JVM 不可能知道。
* 还有一个类似的方法 redefineClasses ,注意,这个方法是在类加载前使用的。类加载后需要使用 retransformClasses 方法
*/
inst.retransformClasses(Main.class);
}

}

在premain的时候,我们不需要去指定哪些类需要retransformer,因为大多数的类还没被加载如内存,加载内存的后类会经过ClassFileTransformer的转换的。

而在agentmain方式中,这里是在程序启动后,再发送指令到目标虚拟机上,这个过程之前类已经到内存了,所以是不会经过ClassFileTransformer,所以需要通过inst.retransformClasses来告诉虚拟机哪些类需要重新transformer,这样它就能经过ClassFileTransformer的转换了。

pom.xml

注意与premain类似,需要添加上Agent-Class来指定IAgentMain:

1
2
3
4
<Premain-Class>agent.agentDomain.IPreMain</Premain-Class>
<Agent-Class>agent.agentDomain.IAgentMain</Agent-Class>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Redefine-Classes>true</Can-Redefine-Classes>

pom.xml全部内容在后文。

ITransformer 编写

这个编写与premain是一样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ITransformer implements ClassFileTransformer {

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
className = className.replace("/", ".");
// 对agent.Main类中所有申明的方法进行加强
if (className.equals("agent.Main")) {
try {
CtClass ctClass = ClassPool.getDefault().get(className);
CtMethod[] ctMethods = ctClass.getDeclaredMethods();
for (CtMethod method : ctMethods) {
method.insertBefore("System.out.println(\"method begin time:\"+System.currentTimeMillis());");
method.insertAfter("System.out.println(\"method end time:\"+System.currentTimeMillis());");
}
return ctClass.toBytecode();
} catch (Exception e) {
System.out.println(e);
}
}
return new byte[0];
}
}

Main 编写

同样这个方法与premain是一样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Main {

public static void main( String[] args ) throws InterruptedException {
Main demo = new Main();
while (true) {
demo.sayHello();
demo.sayBye();
System.out.println("-------------------");
Thread.sleep(1000);
}
}

public void sayHello() {
System.out.println("Hello World!");
}

public void sayBye() {
System.out.println("Bye Bye");
}

}

JVMAttach 编写

注意!!这个方法通过pid连接到目标虚拟机,通知它加载agent的jar包。VirtualMachine需要将jdk目录下的tools.jar添加到工程中,不然导入不了该类,具体操作大家可以参看参考文章。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class JVMAttach {
public static void main(String[] args )
throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
// 这里填写Main执行后的pid
String vmPid = "66112";
// 这里的路径存放的是agentMain打包后的位置
String agentJarPath = "F:\\agentTest\\target\\agentTest-1.0-SNAPSHOT.jar";
VirtualMachine virtualMachine = VirtualMachine.attach(vmPid);
// 这里的csx参数会在 IAgentMain.agentmain(String args)中的args收到
virtualMachine.loadAgent(agentJarPath, "csx");
virtualMachine.detach();
}
}

先运行Main函数,是正常的输出:

你想输入的替代文字

再运行JVMAttach,通知运行Main函数的虚拟机去加载agent,这个agent通过编写ClassFileTransformer逻辑,对agent.Main方法进行transformer改变其类字节码,运行结果:

你想输入的替代文字

可以看到类的方法的确按照我们的逻辑进行了加强!!

参考文章

https://blog.csdn.net/qq_32169479/article/details/85321093

https://www.jianshu.com/p/6096bfe19e41