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指的是可以獨立於應用程序之外的代理程序,<br>
* 可以用來監控和擴展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