引用: java-instrumentation
引用:Instrumentation 新功能
java Instrumentation指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。
Java代理将修改ATM字节码,允许我们测量提取时间,而无需修改ATM应用程序。
通常,java代理只是一个特制的jar文件。它利用JVM提供的Instrumentation API
来更改JVM中加载的现有字节代码。
要使代理工作,我们需要定义两个方法:
premain
: 将在JVM启动时使用-javaagent参数静态加载代理
agentmain
: 将使用Java Attach API
将代理动态加载
到JVM中。JVM实现(如Oracle,OpenJDK等)都提供动态启动代理的机制。首先,让我们看看我们如何使用现有的Java代理。
在那之后,我们将看看我们如何从头开始创建一个在字节码中添加所需的功能。
为了能够使用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();
具体的操作步骤是:
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;
}
@Override
public 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);
//添加局部变量 startTime
m.addLocalVariable("startTime", CtClass.longType);
//方法头部,为startTime赋值
m.insertBefore("startTime = System.currentTimeMillis();");
//添加局部变量 endTime , opTime
m.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 forName
try {
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 want
for(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
异常.
执行
使用命令: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;
}
@Override
public 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();
}
}