java運行時 動態修改class 動態增加方法耗時統計

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章