——————*脑袋空空,口袋空空 q (^(oo)^) P ——————
这是一个关于JavaAgent的故事…
如何在线修复bug? 其中的一个方法就是,通过JavaAgent。
这是个很大的问题,因为没有给出前提,很多情况下,为了避免降低用户体验,我们更多的是想不重启应用的情况下把有问题的代码修复掉,可是很矛盾的一点是,任何在生产环境的改动都必须是谨慎安全的,打开了这道大门,从另外一个角度来说又会使生产环境变得危机四伏,如果这个能力被别有用心的人利用,后果将不堪设想。
好也好,坏也罢,不懂,就得学,我尝试者使用JavaAgent,首先做一个简单的尝试,从一个虚拟机 attach 到另外一个虚拟机,并执行指定的代码。
首先我们先创建一个进程1,并让它保持生存,同时打印出进程ID
public class RunningProcess {
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("Current processid = " + getCurrentThreadID());
while(true) {
try {
System.out.println("I'm living...");
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
/**
* 获取当前进程ID
*/
private static Integer getCurrentThreadID()
{
RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
String name = runtime.getName();
return Integer.parseInt(name.substring(0, name.indexOf("@")));
}
}
接着我们来处理agent,agent用来做什么? 老规矩先科普一下
JavaAgent 是JDK 1.5 以后引入的,也可以叫做Java代理。
JavaAgent 是运行在 main方法之前的拦截器,JavaAgent有两个拦截状态,一个是premain还有agentmain(注意agentmain是JDK1.6后引入的),他们的执行顺序如下:
premain –> agentmain –> main
废话不多说,先创建一个agent代理处理hook后要执行的代码
public class JavaAgentTest {
public static void agentmain(String arg) {
System.out.println("Hello, " + arg);
}
}
接着在导出agent的jar的时候,记得修改Manifest.MF
Manifest-Version: 1.0
Premain-Class: com.java.agent.demo.JavaAgentTest
Name: JavaAgentTest
Agent-Class: com.java.agent.demo.JavaAgentTest
Can-Retransform-Classes: true
Can-Redefine-Classes: true
然后在eclipse导出的时候,选择修改后的Manifest.MF
最后一步了,当然就是启动另外一个进程2,然后attach过去正在执行的进程1,并在进程1上执行agent上的代码。
public class AttachAPI {
public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
String processId = "11456";
String jarFileName = "J:/JavaAgent.jar";
VirtualMachine virtualMachine = VirtualMachine.attach(processId);
try {
virtualMachine.loadAgent(jarFileName, "World!");
} finally {
virtualMachine.detach();
}
}
}
Console结果如下,没有比这更简单的了…
Current processid = 5572
I'm living...
I'm living...
I'm living...
Hello, World!
I'm living...
I'm living...
接着我继续做深入一点的试验,替换正在运行的class,于是我把进程1修改了下。
public class RunningProcess {
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("Current processid = " + getCurrentThreadID());
new DemoPrint().print();
}
/**
* 获取当前进程ID
*/
private static Integer getCurrentThreadID()
{
RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
String name = runtime.getName();
return Integer.parseInt(name.substring(0, name.indexOf("@")));
}
}
创建等待被替换的类DemoPrint.java
package com.nathan.attachAPI.demo;
public class DemoPrint {
public void print() {
while(true) {
try {
System.out.println("Running.....");
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
修改了用来替换class的agent代理jar
public class BugFixAgent {
public static void agentmain(String arg, Instrumentation inst)
throws Exception {
System.out.println("Hello, " + arg);
Class> runningProcess = Class.forName("com.nathan.attachAPI.demo.DemoPrint");
//Get current path
System.out.println(ClassLoader.getSystemResource("") );
File f = new File(arg);
byte[] reporterClassFile = new byte[(int) f.length()];
DataInputStream in = new DataInputStream(new FileInputStream(f));
in.readFully(reporterClassFile);
in.close();
// Apply the redefinition
inst.redefineClasses(new ClassDefinition(runningProcess, reporterClassFile));
System.out.println("Agent Main Done");
}
}
Manifest.MF
Manifest-Version: 1.0
Premain-Class: com.java.agent.demo.BugFixAgent
Name: BugFixAgent
Agent-Class: com.java.agent.demo.BugFixAgent
Can-Retransform-Classes: true
Can-Redefine-Classes: true
修改我们的进程2 AttachAPI.java
public class AttachAPI {
public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
// the following strings must be provided by us
String processId = "11820";
String jarFileName = "J:/BugFixAgent.jar";
VirtualMachine virtualMachine = VirtualMachine.attach(processId);
try {
virtualMachine.loadAgent(jarFileName, "J:/DemoPrint.class");
} finally {
//virtualMachine.detach();
}
}
}
最后,把需要修复的类生成字节码,并复制到我本地的J盘(根据你放的地方而定,填在线程2唤醒agent那里),为了简单,修复类在打印那里从 “Running…..” 改成 “Running fix…..”
public class DemoPrint {
public void print() {
while(true) {
try {
System.out.println("Running fix.....");
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
我以为,打印会从 “Running…..” 改成 “Running fix…..”,然鹅….
Current processid = 6320
Running.....
Running.....
Running.....
Running.....
Hello, J:/DemoPrint.class
file:/J:/eclipse-workspace/hotswap-demo/target/classes/
Agent Main Done
Running.....
Running.....
wtf ???为啥还是 “Running…..” !!!!
于是我把DemoPrint改了下…
DemoPrint.java
public class DemoPrint {
public void print() {
System.out.println("Running.....");
}
}
把循环的代码放到线程1
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("Current processid = " + getCurrentThreadID());
while(true) {
try {
new DemoPrint().print();
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
好,继续试验,按原来的步骤,console终于显示正常的结果了 !!!
Current processid = 2780
Running.....
Running.....
Running.....
Running.....
Hello, J:/DemoPrint.class
file:/J:/eclipse-workspace/hotswap-demo/target/classes/
Agent Main Done
Running fix.....
Running fix.....
Running fix.....
个人猜想…
为什么第一次替换字节码没有成功?我想很大原因是因为如果改字节码正在被执行,它就无法被替换,而第二次替换的时候,agent应该是在while循环的间隙,即DemoPrint还没被调用的时候进行替换的,如有不对,各位请赐教!