在Java中,agent是一种可以在运行时修改字节码的机制,它可以用来实现诸如性能监控、代码覆盖率分析、日志记录等功能。java agent主要分为两种——premain
和agentmain
premain
首先新建一个maven项目,并创建一个PreDemo
类
1 2 3 4 5 6 7 8 9
| import java.lang.instrument.Instrumentation;
public class PreDemo { public static void premain(String args, Instrumentation inst) throws Exception{ for (int i = 0; i < 5; i++) { System.out.println("hello I`m premain agent!!!"); } } }
|
创建META-INF/MANIFEST.MF
,需要指定Premain-Class
1 2 3
| Manifest-Version: 1.0 Premain-Class: PreDemo
|

打包生成ttagent.jar

再新建一个maven项目,并创建一个HelloDemo
类
1 2 3 4 5
| public class HelloDemo { public static void main(String[] args) { System.out.println("Hello !!!"); } }
|
同样打包生成hello.jar
将两个jar包放到同一目录下
运行java -javaagent:ttagent -jar hello.jar

可以发现是先运行的的ttagent.jar
然后才是hello.jar
agentmain
agentmain
是在java程序运行中被调用的方法,可以通过VirtualMachine
向指定pid的jvm插入agentadmin程序
首先写一个AgentDemo
类
1 2 3 4 5 6 7 8 9
| import java.lang.instrument.Instrumentation;
public class AgentDemo { public static void agentmain(String agentArgs, Instrumentation inst) { for (int i = 0; i < 5; i++) { System.out.println("hello I`m agentMain!!!"); } } }
|
修改MANIFEST.MF
为:
1 2 3 4
| Manifest-Version: 1.0 Premain-Class: PreDemo Agent-Class: AgentDemo
|
打包重新生成ttagent.jar
修改HelloDemo
类
1 2 3 4 5 6 7 8
| public class HelloDemo { public static void main(String[] args) throws Exception{ while (true){ Thread.sleep(10000); System.out.println("hello!!!"); } } }
|
新建AttachDemo
类并通过VirtualMachine
这个类帮助我们连接到一个JVM
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import com.sun.tools.attach.VirtualMachine; import java.util.Scanner; public class AttachDemo { public static void main(String[] args) throws Exception{ Scanner scanner = new Scanner(System.in); System.out.print("Enter the pid: "); String pid = scanner.nextLine(); VirtualMachine virtualMachine = VirtualMachine.attach(pid); virtualMachine.loadAgent("D:\\ideaproject\\neicunma\\ttagent\\out\\artifacts\\ttagent_jar\\ttagent.jar"); virtualMachine.detach(); } }
|
运行HelloDemo,查看对应的pid进程

确认pid为47292(之前没有这个进程的,运行HelloDemo后出现)
运行AttachDemo,输入pid

查看HelloDemo的控制台发现成功调用ttagent.jar

Instrumentation
agentmain中可以看到我们传入了一个Instrumentation
类的inst,这个类可以帮助我们修改源程序中已加载的其他类,先讲两个方法getAllLoadedClasses
和isModifiableClass
getAllLoadedClasses
方法可以获取当前已经加载的类
isModifiableClass
方法可以判断当前类是否可修改
修改AgentDemo如下
1 2 3 4 5 6 7 8 9 10 11 12
| import java.lang.instrument.Instrumentation;
public class AgentDemo { public static void agentmain(String agentArgs, Instrumentation inst) { Class[] classes = inst.getAllLoadedClasses(); for (Class aclass : classes) { System.out.println("classname="+aclass.getName()+" "+"Modifiable="+ inst.isModifiableClass(aclass)); } } }
|
重新生成ttagent.jar
以后,先运行HelloDemo再运行AttachDemo并输入对应的pid,可以看到对应的类被输出了

修改HelloDemo为:
1 2 3 4 5 6 7 8 9 10 11 12
| public class HelloDemo { public static void main(String[] args) throws Exception{ HelloDemo helloDemo = new HelloDemo(); while (true){ Thread.sleep(10000); helloDemo.hello(); } } public void hello(){ System.out.println("hello!!!"); } }
|
接下来尝试把HelloDemo的hello方法给修改掉,这里需要调用Instrumentation类的两个方法:
addTransformer
方法可以向 JVM 注册一个类转换器,从而在类加载时对类进行转换
retransformClasses
方法可以重新转换已经加载的类
addTransformer方法要传入一个ClassFileTransformer
类对象,而retransformClasses
方法会触发 JVM 重新载入指定的一组类,并调用它们对应的类转换器中的 transform()
方法,将类转换为新的字节码形式。
1 2 3 4 5 6 7 8 9 10
| public interface ClassFileTransformer { ... byte[] transform( ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException; }
|
修改AgentDemo类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class AgentDemo { public static void agentmain(String agentArgs, Instrumentation inst) throws Exception{ Class[] classes = inst.getAllLoadedClasses(); for (Class aclass : classes) { if (aclass.getName().equals("HelloDemo")) { inst.addTransformer(new TransformerDemo(), true); inst.retransformClasses(aclass); }
} } }
|
新建一个TransformerDemo类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import javassist.*; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; public class TransformerDemo implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { try { ClassPool classpool = ClassPool.getDefault(); if (classBeingRedefined != null) { ClassClassPath classClassPath = new ClassClassPath(classBeingRedefined); classpool.insertClassPath(classClassPath); } CtClass ctclass = classpool.get("HelloDemo"); CtMethod method = ctclass.getDeclaredMethod("hello"); String source = "{System.out.println(\"hello transformer\");}"; method.setBody(source); byte[] bytes = ctclass.toBytecode(); ctclass.detach(); return bytes; } catch (Exception e){ e.printStackTrace(); } return null; } }
|
这里要导入javassist的依赖(两个项目都要导入)
1 2 3 4 5
| <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.27.0-GA</version> </dependency>
|
以上代码的功能主要是判断当前运行的类是否是HelloDemo,如果是的话,则将该类的hello方法输出结果修改为”hello transformer“
修改MANIFEST.MF加上
1 2
| Can-Redefine-Classes: true Can-Retransform-Classes: true
|
重新生成ttagent.jar以后,同样先运行HelloDemo再运行AttachDemo并输入对应的pid,即可发现HelloDemo类的hello方法被修改了

接下来试着将tomcat中的filter给改掉
先在80端口起一个tomcat,根据端口判断对应的pid

可以得到pid为53396
可以看看已经加载了哪些类

可以看到之前的Myfilter类

尝试将Myfilter类的doFileter方法进行修改
但这里总是会报一个错Caused by: java.lang.ClassNotFoundException: javassist.ClassPath

应该是javassist依赖没有导入的问题,但尝试了很多办法(改版本、将javassist依赖一起导入ttagent.jar、JavaWeb主动添加javassist依赖),都没有效果