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

强烈推荐一套Java进阶博客,都是干货,走向架构师不是梦!

Java进阶全套博客

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值