使用步骤
1.一个目标被修改的类
你要更新的Class对象 我这里就随便建立了一个
public class TC1 {
public void fun1(){
System.out.println("TC1 old function ");
}
}
public static void main(String[] args) {
String name = ManagementFactory.getRuntimeMXBean().getName();
String s = name.split("@")[0];
System.out.println("pid:"+s);
TC1 tc1 = new TC1();
new Thread(()->{
String tname=Thread.currentThread().getName();
while (true){
try {
Thread.sleep(5000);
System.out.print(tname+"运行中");
tc1.fun1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
3创建一个类继承ClassFileTransformer 接口 这里重写的方法 并返回的其实是 修改后的字节码,我这里采用的是直接 把新的.class文件变成字节码返回 这个url来源 我利用构造器传入的 传入我本地改好的.class文件路径
public class MyTransformer implements ClassFileTransformer {
private String url;
public MyTransformer(String url) {
this.url=url;
}
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException {
System.out.println("开始做字节码替换的步骤");
try {
System.out.println("开始进行修改");
//1.获取目标类(目标类的信息会封装到CtClass对象中)
//byte[] re=teacher();
byte[] re=myclass(url);
return re;
}catch (Exception e){
System.out.println(e.getMessage());
e.printStackTrace();
}
return null;
}
public byte[] myclass(String url) throws IOException {
FileInputStream in= new FileInputStream(url);
byte[] newClassByte= new byte[in.available()];
in.read(newClassByte);
return newClassByte;
}
}
4.创建Agent
这里的new MyTransformer(args)是 另一个程序 让这个程序执行 方法时候传入的参数 我这里让另一个程序传入的正式 本地用于替换的.class文件的路径
通过分析那个传入的路径 来得到修改类的名字 同时 要对应上这个程序等着被替换的类的对象 Class cs=Class.forName("tests.bytecode.javaagent."+classname)
public class MyAgent {
public static void premain(String args, Instrumentation inst){
inst.addTransformer(new MyTransformer(args), true);
}
public static void agentmain(String args, Instrumentation inst) throws IOException {
String url=args;
String classname=url.replace("\\"," ");
classname=classname.substring(classname.lastIndexOf(" ")).trim();
classname=classname.substring(0,classname.lastIndexOf("."));
System.out.println(classname);
//指定我们自己定义的 Transformer,在其中利用 Javassist 做字节码替换
inst.addTransformer(new MyTransformer(args), true);
try {
// System.out.println(classname);
Class cs=Class.forName("tests.bytecode.javaagent."+classname);
//重定义类并载入新的字节码
inst.retransformClasses(cs);
System.out.println("Agent Load Done.");
} catch (Exception e) {
System.out.println("agent load failed!");
}
}
}
3.要对这个类打包一下 这边的工作就完成了
主要是要拿到那个包的路径
在pom里添加Maven插件
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<!-- 可以选变成jar后运行主程序 <mainClass>tests.bytecode.javaagent.TC1test</mainClass>-->
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<!--Main方法执行之前可以执行的对象-->
<Premain-Class>
tests.bytecode.javaagent.DefaultAgent
</Premain-Class>
<!--Main方法执行之后可以执行的对象-->
<Agent-Class>
tests.bytecode.javaagent.DefaultAgent
</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
[INFO] --- maven-jar-plugin:3.2.0:jar (default-cli) @ tests ---
[INFO] Building jar: C:\Users\jj\IdeaProjects\tests\target\tests-0.0.1-SNAPSHOT.jar
主要是拿到这个C:\Users\jj\IdeaProjects\tests\target\tests-0.0.1-SNAPSHOT.jar
这里我先测试用另一个程序让上面正在运行的程序的类更新
public class AgentInstrumentTests {
public static void main(String[] args) throws Exception{
VirtualMachine vm = VirtualMachine.attach("29568");
String url="E:\\classloadertest\\TC1.class";
vm.loadAgent("C:\\Users\\jj\\IdeaProjects\\tests\\target\\tests-0.0.1-SNAPSHOT.jar",url);
}
}
注意 .attach("29568");这里填的是目标线程的pid
当这个程序启动后 会根据进程pid找到那个进程 然后那个进程就会执行againt里的方法
然后 那个方法又会调用Transformer里的方法 个人理解 就是把类加载器重写加载新的字节码并重新类加载