简单来说,Javaagent可以让我们在不修改程序代码的前提下通过Instrumentation API改变运行中的java程序。
当Java虚拟机启动时,在执行 main 函数之前,JVM会先运行-javaagent所指定jar包内Premain-Class这个类的premain方法。
这个premain方法应该怎么写呢?这就是我们要讲的。
前言
基础描述什么的我就懒得复制粘贴了,找了俩博文放在这里,先粗略了解一下。
基础案例:重写Test对象的main方法
假设有demo.Test
如下,我们要将main方法里的sayHello("java");
改为sayHello("修改过后的java");
package demo;
public class Test{
String aa = "value-aa";
String bb = "value-bb";
public static void main(String[] args) {
System.out.println(new Test());
sayHello("java");
}
public static void sayHello(String name) {
System.out.println("Hello, " + name);
}
}
我们采取最傻瓜的办法,自己重新再写一个Test类,然后编译成字节码,当需要的时候直接将编译好的内容给上。
生成字节码
编译写好的demo.Test
,生成demo.Test.class
,为了方便区别,重命名为Test.class.modified
以下为具体实现:
package demo;
public class Test{
String aa = "value-aa";
String bb = "value-bb";
public static void main(String[] args) {
System.out.println(new Test());
sayHello("修改过后的java");
}
public static void sayHello(String name) {
System.out.println("Hello, " + name);
}
}
创建agent
如果待处理的类是demo.Test
,那么提供修改后的字节码;否则原封不动。
public class MyAgent {
public static void premain(String args, Instrumentation inst) {
// args 是命令行的入参
inst.addTransformer(new Transformer(args));
}
private static class Transformer implements ClassFileTransformer {
public Transformer(String args) {
}
static byte[] readAll(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
for (int len = 0; (len = in.read(buffer)) != -1;) {
out.write(buffer, 0, len);
}
in.close();
return out.toByteArray();
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
//System.out.println(className);
// 转换指定包名开头的类
if (className.equals("demo/Test")) {
try {
System.out.println("demo/test被修改");
return readAll(MyAgent.class.getResourceAsStream("/resources/Test.class.modified"));
} catch (Throwable t) {
t.printStackTrace();
return classfileBuffer;
}
} else {
return classfileBuffer;
}
}
}
}
创建MANIFEST.MF
创建MANIFEST.MF,指定上面premain方法所在的类
为了方便,我把agent和待测试的都打包成一个test.jar
,所以下面还多了一行Main-Class: demo.Test
Manifest-Version: 1.0
Class-Path: .
Main-Class: demo.Test
Premain-Class: demo.javaagent.MyAgent
Can-Redefine-Classes: true
测试效果
> java -jar test.jar
demo.Test@75b84c92
Hello, java
> java -javaagent:test.jar=args -jar test.jar
demo/test被修改
demo.Test@75b84c92
Hello, 修改过后的java
基础案例:添加对象的toString方法
Test对象并没有重载Object的toString方法,所以我们打印的时候会直接出现内存地址。
我们期望的Test应该如下:
package demo;
public class Test{
String aa = "value-aa";
String bb = "value-bb";
public String toString() {
StringBuilder sb =new StringBuilder("Test(");
sb.append("aa").append("=").append(aa).append(",");
sb.append("bb").append("=").append(bb).append(",");
sb.append(")");
return sb.toString();
}
public static void main(String[] args) {
System.out.println(new Test());
sayHello("java");
}
public static void sayHello(String name) {
System.out.println("Hello, " + name);
}
}
现在我们换个方法,不再死死的自己去重写代码然后编译,而是使用javaassisit去实现agent。
public class MyAgentJavaAssist {
public static void premain(String args, Instrumentation inst) {
// args 是命令行的入参
inst.addTransformer(new Transformer(args));
}
private static class Transformer implements ClassFileTransformer {
public Transformer(String args) {
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
ClassPool pool = ClassPool.getDefault();
ByteArrayInputStream in = new ByteArrayInputStream(classfileBuffer);
CtClass cc = pool.makeClass(in);
// 判断有无声明 toString() 方法,没有的话就生成一个
try {
cc.getDeclaredMethod("toString", new CtClass[] {});
} catch (NotFoundException e) {
CtMethod cm = new CtMethod(pool.getCtClass("java.lang.String"), "toString", new CtClass[] {}, cc);
StringBuilder sBody = new StringBuilder();
sBody.append("{StringBuilder sb =new StringBuilder(\"").append(cc.getSimpleName()).append("(\");");
CtField[] cfs = cc.getDeclaredFields();
for (CtField cf : cfs) {
sBody.append("sb.append(\"").append(cf.getName()).append("\").append(\"=\").append(")
.append(cf.getName()).append(").append(\",\");");
}
sBody.append("sb.append(\")\");").append("return sb.toString();}");
cm.setBody(sBody.toString());
cc.addMethod(cm);
}
// 判断是否是 demo/Test, 是的话修改 main方法
if (className.equals("demo/Test")) {
CtMethod cm = cc.getDeclaredMethod("main",
new CtClass[] { pool.getCtClass("[Ljava.lang.String;") });
cm.setBody("{System.out.println(new demo.Test());sayHello(\"修改过后的java\");}");
}
return cc.toBytecode();
} catch (Throwable t) {
t.printStackTrace();
return classfileBuffer;
}
}
}
}
测试效果