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指的是可以独立于应用程序之外的代理程序,
* 可以用来监控和扩展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的一些参数,注意:最后一行必须是空行。
java运行时 动态修改class 动态增加方法耗时统计_第1张图片

接下来,我们需要将上面代码export 出一个 jar包并制定MANIFEST.MF文件
项目右键->export->Jar File
java运行时 动态修改class 动态增加方法耗时统计_第2张图片
几层next后,指定MANIFEST.MF文件
java运行时 动态修改class 动态增加方法耗时统计_第3张图片
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 加到工程里面来。

java运行时 动态修改class 动态增加方法耗时统计_第4张图片
老生常谈:深圳有爱好音乐的会打鼓(吉他,键盘,贝斯等)的程序员和其它职业可以一起交流加入我们乐队一起嗨。我的QQ:657455400

你可能感兴趣的:(java虚拟机)