引用: java-instrumentation
引用:Instrumentation 新功能
简介
java Instrumentation指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。
场景
- 一个ATM应用程序,允许我们取钱
- 还有一个Java代理,它允许我们通过衡量投入的时间来衡量我们的ATM的性能
Java代理将修改ATM字节码,允许我们测量提取时间,而无需修改ATM应用程序。
什么是java agent
通常,java代理只是一个特制的jar文件。它利用JVM提供的Instrumentation API
来更改JVM中加载的现有字节代码。
要使代理工作,我们需要定义两个方法:
premain
: 将在JVM启动时使用-javaagent参数静态加载代理
agentmain
: 将使用Java Attach API
将代理动态加载
到JVM中。JVM实现(如Oracle,OpenJDK等)都提供动态启动代理的机制。
首先,让我们看看我们如何使用现有的Java代理。
在那之后,我们将看看我们如何从头开始创建一个在字节码中添加所需的功能。
加载 agent
为了能够使用Java代理,我们必须首先加载它。我们有两种加载方式:
static
: 使用premain来使用-javaagent选项加载代理dynamic
: 使用agentmain使用Java Attach API将代理加载到JVM中
接下来,我们将看看每种类型的负载并解释它是如何工作的。
静态加载
在应用程序启动时加载Java代理称为静态加载
。
静态加载可以在main方法执行前,修改任意代码的字节码。
静态加载使用premain方法,该方法将在任何应用程序代码运行之前运行,
通过如下命令,可以开启静态加载:
# 我们应该始终将-javaagent参数放在-jar参数之前。
# agent_param 可选参数,与main方法接收参数不同,它只能接收一个string类型的参数.
java -javaagent:agent.jar [agent_param] -jar application.jar
动态加载
将Java代理加载到已运行的JVM中的过程称为动态加载
。
代理程序通过Java Attach API
附加到应用程序。
更为复杂的情况是,当我们的ATM应用程序已经在生产环境中运行,我们希望在不停机的情况下,动态的添加监控项,如:动态添加事务的总时间。
下面给出关键代码片段:
//pid 为需要监控的应用程序pid
VirtualMachine jvm = VirtualMachine.attach(jvmPid);
jvm.loadAgent(agentFile.getAbsolutePath());
jvm.detach();
具体的操作步骤是:
- 首先启动应用程序
- 在确保应用程序正常的启用一段时间后,获取该应用的pid,
- 启动agent程序,启动时需传递应用程序pid
coding
应用程序
ATM
有ATM
类,并提供withdrawMoney
取款方法。
public class ATM {private static Logger LOGGER = LoggerFactory.getLogger(ATM.class);private static int TOTAL_MONEY = 10000;/*** 取钱** @param amount* @throws InterruptedException*/public static void withdrawMoney(int amount) throws InterruptedException {//模拟一个取钱的动作Thread.sleep(ThreadLocalRandom.current().nextLong(1000));int rest = TOTAL_MONEY -= amount;LOGGER.info("[Application] 取款 [{}] 元,余额[{}]!", amount, rest);//当账户余额不足时,退出系统if (rest <= 0) {System.exit(1);}}
}
App
应用启动类App
,用来启动应用程序.
public class App {private static Logger LOGGER = LoggerFactory.getLogger(App.class);/*** 应用主程序** @param args:* @throws Exception*/public static void main(String[] args) throws Exception {LOGGER.info("**************************************************");LOGGER.info("===========欢迎使用xx银行ATM无人取款机=============");LOGGER.info("**************************************************\n");while (true){ATM.withdrawMoney(new Random().nextInt(100));TimeUnit.SECONDS.sleep(2);}}
}
打包,执行
将上述两个应用程序打包成可执行jar包,执行命令java -jar application.jar
,效果如下图:
Maven生成可以直接运行的jar包的多种方式
静态加载
在编写加载类前,需要先了解下如下几点知识:
premain函数
编写一个 Java 类,包含如下两个方法当中的任何一个:
//[1] 的优先级比 [2] 高,将会被优先执行([1] 和 [2] 同时存在时,[2] 被忽略)public static void premain(String agentArgs, Instrumentation inst); //[1]
public static void premain(String agentArgs); //[2]
ClassFileTransformer-接口
public interface ClassFileTransformer {byte[]transform( ClassLoader loader,String className,Class<?> classBeingRedefined,ProtectionDomain protectionDomain,byte[] classfileBuffer)throws IllegalClassFormatException;
}
通过这个方法,代理可以得到虚拟机载入的类的字节码(通过 classfileBuffer
参数)。代理的各种功能一般是通过操作这一串字节码得以实现的。
字节码编辑器:AtmTransformer
使用
javassist
来操作字节码
public class AtmTransformer implements ClassFileTransformer {private static Logger LOGGER = LoggerFactory.getLogger(AtmTransformer.class);private static final String WITHDRAW_MONEY_METHOD = "withdrawMoney";private String targetClassName;private ClassLoader targetClassLoader;public AtmTransformer(String targetClassName, ClassLoader targetClassLoader) {this.targetClassName = targetClassName;this.targetClassLoader = targetClassLoader;}@Overridepublic byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {byte[] byteCode = classfileBuffer;String finalTargetClassName = this.targetClassName.replaceAll("\\.", "/"); //replace . with /if (!className.equals(finalTargetClassName)) {return byteCode;}if (className.equals(finalTargetClassName) && loader.equals(targetClassLoader)) {LOGGER.info("[Agent] Transforming class ATM");try {ClassPool cp = ClassPool.getDefault();CtClass cc = cp.get(targetClassName);CtMethod m = cc.getDeclaredMethod(WITHDRAW_MONEY_METHOD);//添加局部变量 startTimem.addLocalVariable("startTime", CtClass.longType);//方法头部,为startTime赋值m.insertBefore("startTime = System.currentTimeMillis();");//添加局部变量 endTime , opTimem.addLocalVariable("endTime", CtClass.longType);m.addLocalVariable("opTime", CtClass.longType);StringBuilder endBlock = new StringBuilder();endBlock.append("endTime = System.currentTimeMillis();");endBlock.append("opTime = (endTime-startTime);");endBlock.append("LOGGER.info(\"[Application] 取款总计耗时:\" + opTime + \" ms!\");");//方法尾部,为endTime,optTime赋值,并打印日志m.insertAfter(endBlock.toString());//返回 更改后的字节码byteCode = cc.toBytecode();cc.detach();} catch (NotFoundException | CannotCompileException | IOException e) {LOGGER.error("Exception", e);}}return byteCode;}
}
计算取款耗时代理类:TimeInstrumentationAgent
public class TimeInstrumentationAgent {private static Logger LOGGER = LoggerFactory.getLogger(TimeInstrumentationAgent.class);/**** @param agentArgs 通过命令:java -javaagent:agent.jar [agent_param] -jar application.jar* 传递的代理参数,只有一个以字符串形式接受* @param inst*/public static void premain(String agentArgs, Instrumentation inst) {LOGGER.info("[Agent] In premain method");String className = "cn.jhs.application.service.ATM";transformClass(className,inst);}private static void transformClass(String className, Instrumentation instrumentation) {Class<?> targetCls = null;ClassLoader targetClassLoader = null;// see if we can get the class using forNametry {targetCls = Class.forName(className);targetClassLoader = targetCls.getClassLoader();transform(targetCls, targetClassLoader, instrumentation);return;} catch (Exception ex) {LOGGER.error("Class [{}] not found with Class.forName");}// otherwise iterate all loaded classes and find what we wantfor(Class<?> clazz: instrumentation.getAllLoadedClasses()) {if(clazz.getName().equals(className)) {targetCls = clazz;targetClassLoader = targetCls.getClassLoader();transform(targetCls, targetClassLoader, instrumentation);return;}}throw new RuntimeException("Failed to find class [" + className + "]");}private static void transform(Class<?> clazz, ClassLoader classLoader, Instrumentation instrumentation) {AtmTransformer dt = new AtmTransformer(clazz.getName(), classLoader);instrumentation.addTransformer(dt, true);try {instrumentation.retransformClasses(clazz);} catch (Exception ex) {throw new RuntimeException("Transform failed for class: [" + clazz.getName() + "]", ex);}}}
打包:agent.jar
将agent程序打包,并在MANIFEST.MF
指定Premain-Class
,
如下图:
注意 : 经常有人会忘记指定
Can-Retransform-Classes
和Can-Redefine-Classes
而抛出类似java.lang.UnsupportedOperationException: adding retransformable transformers is not supported in this environment
异常.<!-- 添加任意 key-value --> <manifestEntries><Premain-Class>cn.jhs.jvm.agent.instrument.TimeInstrumentationAgent</Premain-> ```Class><Can-Redefine-Classes>true</Can-Redefine-Classes><Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries>
执行
使用命令:java -javaagent:agent.jar -jar application.jar
执行,效果如下:
动态加载
在 Java SE 5 当中,开发者只能通过premain
来修改完成代理工作。
但是必须在应用启动前指定Instrumentation
,这样的方式存在一定的局限性。
在 Java SE 5 的基础上,Java SE 6 针对这种状况做出了改进,开发者可以在应用程序启动之后,启动自己的Instrumentation
程序。
在 Java SE 6 的 Instrumentation 当中,有一个跟premain
功能类似的agentmain
方法,可以在 main 函数运行之后再运行。
agentmain函数
//[1] 的优先级比 [2] 高,将会被优先执行。
public static void agentmain (String agentArgs, Instrumentation inst // [1]
public static void agentmain (String agentArgs) // [2]
与Premain-Class
类似,开发者必须在manifest
文件里面设置Agent-Class
来指定包含 agentmain
函数的类。
修改:TimeInstrumentationAgent
修改字节码的逻辑不变,唯一的变化就是在TimeInstrumentationAgent
添加agentmain
方法:
public static void agentmain(String agentArgs, Instrumentation inst) {LOGGER.info("[Agent] In agentmain method");String className = "cn.jhs.application.service.ATM";transformClass(className,inst);}
打包:agent.jar
指定Agent-Class
,打包后效果如下图:
编写监听程序
public class AttachTask implements Runnable{private static Logger LOGGER = LoggerFactory.getLogger(AttachTask.class);private String agentJarFullPath;private String applicationName;/**** @param agentJarFullPath agent.jar 完整路径* @param applicationName 需要添加代理的目标程序*/public AttachTask(String agentJarFullPath, String applicationName) {this.agentJarFullPath = agentJarFullPath;this.applicationName = applicationName;}@Overridepublic void run() {while(true) {Optional<String> jvmProcessOpt = Optional.ofNullable(VirtualMachine.list().stream().filter(jvm -> {LOGGER.info("jvm:{}", jvm.displayName());return jvm.displayName().contains(applicationName);}).findFirst().get().id());//如果没有找到目标应用程序,sleep ,然后从新监听if(!jvmProcessOpt.isPresent()) {try {LOGGER.info("未捕获到应用程序: " + applicationName);TimeUnit.SECONDS.sleep(5L);} catch (InterruptedException e) {//}break;}//File agentFile = new File(agentJarFullPath);try {String jvmPid = jvmProcessOpt.get();LOGGER.info("捕获到应用程序[{}], JVM PID:[{}] ",applicationName,jvmPid);VirtualMachine jvm = VirtualMachine.attach(jvmPid);jvm.loadAgent(agentFile.getAbsolutePath());jvm.detach();LOGGER.info("目标应用程序添加代理成功!");} catch (Exception e) {throw new RuntimeException(e);}//添加完代理之后,直接退出..return ;}}public static void main(String[] args) {new Thread(new AttachTask("D:/instrumentation/agent.jar","application.jar")).start();}
}
执行
1.启动应用
: java -jar application.jar
2.启动监听程序
3.监听启动后,应用程序日志变化