1.有一个需求,我们的业务java服务正在运行,有一天我们定位到系统中某方法可能出现异常,我们要在不修改代码重新发版的情况下,统计出此方法的耗时,怎么办?
业务服务代码:
public class OrderService {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100000; i++) {
makeOrder(23, "110112");
Thread.sleep(1000);
}
}
private static String makeOrder(int userId, String itemId) throws InterruptedException {
System.err.println("有人下单>> userId=" + userId + ",itemId=" + itemId);
Thread.sleep(new Random().nextInt(5) * 1000);
return System.currentTimeMillis() + "";
}
}
先运行此业务代码。
然后编写我们的代理处理工程
package com.hadluo.test;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
import com.hadluo.test.Javassists;
public class FixAgent {
private static String targetClassName = "jvm.OrderService";
private static String targetMethdName = "makeOrder";
/***
* Instrumentation接口位于jdk1.6包java.lang.instrument包下,Instrumentation指的是可以独立于应用程序之外的代理程序,
* 可以用来监控和扩展JVM上运行的应用程序,相当于是JVM层面的AOP
*
* @param args
* @param inst
*/
public static void agentmain(String args, Instrumentation inst) {
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
System.err.println(targetClassName);
System.err.println(classBeingRedefined.getName());
// 是我们要代理的类我们才处理
if (targetClassName.equals(classBeingRedefined.getName())) {
System.err.println("enter");
// 修改字节码class
classfileBuffer = Javassists.reDefineClass(classBeingRedefined.getName(), targetMethdName);
}
} catch (Exception e) {
e.printStackTrace();
}
return classfileBuffer;
}
}, true);
for (Class<?> clazz : inst.getAllLoadedClasses()) {
if (clazz.getName().equals(targetClassName)) {
try {
// 上面的addTransformer只是针对未加载的class进行增加代理层,retransformClasses让jvm对已经加载的class重新加上代理层
inst.retransformClasses(clazz);
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
}
}
}
}
代码解释: 利用java的探针技术
为解决运行时启动代理类的问题,Java SE6开始,提供了在应用程序的VM启动后在动态添加代理的方式,即agentmain方式。
下面是我们要动态修改类的工具
package com.hadluo.test;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
public class Javassists {
/***
* 修改 class字节码,为指定方法增加打印时间
*
* @param className
* @return
* @throws Exception
*/
public static byte[] reDefineClass(String className, String methdName) throws Exception {
try {
ClassPool pool = new ClassPool();
pool.appendSystemPath();
// 定义类
CtClass ctClass = pool.get(className);
// 需要修改的方法
CtMethod method = ctClass.getDeclaredMethod(methdName);
// 增加本地变量
method.addLocalVariable("start", CtClass.longType);
//增加统计耗时代码
method.insertBefore("start = System.currentTimeMillis();\n");
String pre = "\"" + className + "." + methdName + "()方法耗时:\"";
String after = "System.out.println(" + pre + " + (System.currentTimeMillis()-start) + \"ms\");";
method.insertAfter(after);
return ctClass.toBytecode();
} catch (Throwable e) {
System.err.println("===== " + e.getMessage());
e.printStackTrace();
}
return null;
}
}
代码解释:利用javassist工具来操作修改类,为原类的方法增加代码,来统计打印出耗时。
接下来我们要在src 目录下新建MANIFEST.MF文件,用于agent的一些参数,注意:最后一行必须是空行。
接下来,我们需要将上面代码export 出一个 jar包并制定MANIFEST.MF文件
项目右键->export->Jar File
几层next后,指定MANIFEST.MF文件
jar包就生成好了,接下来我们编写绑定程序,用于指定哪个客户端java进程生效我们编写的agent代码。
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
public class Test {
public static void main(String[] args)
throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
// 查找所有的jvm 进程
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor descriptor : list) {
if (descriptor.displayName().equals("") || descriptor.displayName().equals(Test.class.getName())) {
// 过滤本进程
continue;
}
System.err.println("main启动类名称:" + descriptor.displayName() + ",id=" + descriptor.id());
}
System.err.println("请输入要监控的进程pid:");
String id = new Scanner(System.in).next();
VirtualMachine vm = VirtualMachine.attach(id);
// 加载 agent jar包
vm.loadAgent("C:\\Users\\皮吉\\Desktop\\fix-agent.jar");
// 开始绑定
vm.detach();
}
}
代码解释: 运行到客户端机器上面,输入要绑定的进程id,没报错,就成功为业务代码makeOrder增加了耗时打印。
找不到VirtualMachineDescriptor 类的话,就把jdk的tools.jar 加到工程里面来。
老生常谈:深圳有爱好音乐的会打鼓(吉他,键盘,贝斯等)的程序员和其它职业可以一起交流加入我们乐队一起嗨。我的QQ:657455400