从现有的前置知识来说,我们能够认识到两个事实:
全限定名
进行区分。当需要加载新的类时,ClassLoader 通过双亲委派机制判断是否已经加载过这个类。 另一个方面,也许绝大多数人都听说过所谓的热部署
。但是究竟怎么才能做到 热部署
(话题开得有点大哈。Y_Y 本文不讲这个)
操作字节码一定是一个逃不开的话题,毕竟 Class 就是所谓的被加载到内存的字节码嘛。
如何操作字节码? ASM, CGlib, Java Proxy, Javassist ? 不过这些都要等到需要被操作的类被加载了才行啊,似乎有点晚…
Java 提供了一个可行的机制,用来在 ClassLoader 加载字节码之前完成对操作字节码的目的
java.lang.instrument.Instrumentation
类为提供直接操作 Java 字节码的又一个途径(虽然 Java Doc 的说明是用来检测 Java 代码的)
相信我这个说明是没有问题的。毕竟完成对代码检测的途径是直接修改字节码。
下列有两种方法可以达到目的
话不多说,下面将全部以实例来展现对这种 JVM 检测机制(虽然例子已经脱离了检测的目的)的使用
基本实例: 将对特定包 me.fangfeng.client
下的每个方法执行计时
首先了解一下 client 包的内容:
package me.fangfeng.client;
/**
* Main.java
* 执行两个方法,rand() & sleep()
*
* @author fangfeng
* @since 2018/8/7
*/
public class Main {
static void sleep() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Rand rand = new Rand();
for (int i=0;i<10;i++) {
System.out.println(">>> start Rand.run() <<<");
rand.run();
System.out.println(">>> end Rand.run() <<<");
System.out.println();
System.out.println(">>> start Main.sleep() <<<");
Main.sleep();
System.out.println(">>> end MAin.sleep() <<<");
System.out.println();
}
}
}
package me.fangfeng.client;
/**
* Rand.java
* @author fangfeng
* @since 2018/8/7
*/
public class Rand {
public void run() {
while (true) {
double rand = Math.random();
if (rand > 0.995) {
System.out.println(String.format("get random, values %f", rand));
return;
}
}
}
}
接着,来构造一个代理类,以及最重要的 premain
方法
package me.fangfeng.javaagent;
import java.lang.instrument.Instrumentation;
/**
* Agent - 代理
* 基于 JVM TI (JVM Tool Interface) 实现的 Java ClassFile 的增强
* @author fangfeng
* @since 2018/8/7
*/
public class Agent {
// premain 将 JVM 初始化后,main(String... ) 执行前调用
public static void premain(String args, Instrumentation instrumentation) {
// new 一个转换器实例
ClassTimer transformer = new ClassTimer();
instrumentation.addTransformer(transformer);
}
// 之后的 agentmain(...) 将在这里提供,暂时隐去,避免对对读者产生干扰
}
package me.fangfeng.javaagent;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
/**
* @author fangfeng
* @since 2018/8/7
*/
public class ClassTimer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
// 这里涉及到了 ASM 的内容,主要目的是向每个方法块的开始及方法块的结束部分插入与计时器有关的代码
// 如果想了解 ASM 的内容,请参阅 https://dormouse-none.github.io/2018-06-25-ASM-Core/ 提供了一些基础性的内容,更多的请自行学习
// 不了解具体内容将不影响对主体内容的理解
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
MyClassWriter mcw = new MyClassWriter(Opcodes.ASM6, cw);
cr.accept(mcw, ClassReader.EXPAND_FRAMES);
return cw.toByteArray();
}
}
其它代码略,详见附件。
Java 这种对操作字节码的支持有个坑爹的地方,就是不得不打包成 Jar 来使用。
具体来看一下
me.fangfeng.javaagent
包中包括
将被打包成 agent.jar
来使用
首先,来看一下需要打包在 agent.jar
的 MANIFEST.MF 的内容
Manifest-Version: 1.0
Class-Path: /Users/fangfeng/.m2/repository/org/ow2/asm/asm/6.1.1/asm-6.1.1.jar
Premain-Class: me.fangfeng.javaagent.Agent
Can-Retransform-Classes: true
再来个 SHELL 脚本,用来给打包这个 Jar
#!/bin/bash
# 编译 me.fangfeng.javaagent 包下的类
javac -cp .:/Users/fangfeng/.m2/repository/org/ow2/asm/asm/6.1.1/asm-6.1.1.jar me/fangfeng/javaagent/Agent.java me/fangfeng/javaagent/ClassTimer.java me/fangfeng/javaagent/MyClassWriter.java me/fangfeng/javaagent/MyMethodWriter.java me/fangfeng/javaagent/StaticTimer.java
# 打包 me.fangfeng.javaagent 的 .class -> agent.jar
jar cvfm agent.jar MANIFEST-agent.MF me/fangfeng/javaagent/Agent.class me/fangfeng/javaagent/ClassTimer.class me/fangfeng/javaagent/MyClassWriter.class me/fangfeng/javaagent/MyMethodWriter.class me/fangfeng/javaagent/StaticTimer.class
# 编译 me.fangfeng.client 包下的类
javac me/fangfeng/client/Main.java me/fangfeng/client/Rand.java
# 以 me.fangfeng.client.Main 作为主类启动
java -javaagent:agent.jar me.fangfeng.client.Main
执行后,可以看到类似如下内容:
而直接用 java me.fangfeng.client.Main
的执行结果是:
从理论上来讲,-javaagent:agent.jar
配合 agent.jar
中的 MANIFEST.MF 文件,
使得 JVM 在初始化之后触发了被声明为 Pre-Main
的 me.fangfeng.javaagent.Agent 类的 premain(…) 方法。
并为 ClassLoader 在加载类的流程上增加了一层拦截器 (这里是 ClassTimer.java 类,它实现了 ClassFileTransformer
接口
另外,Can-Retransform-Classes: true
的配置使得 ClassTimer 被允许对字节码进行重新转换。(而操作字节码是通过 ASM 来实现的)
随着程序启动时直接使用了 -javaagent
选项。
那么是否存在在程序运行中进行额外代理操作的支持呢?当然是可以的。这里要借助 Java 提供的另一个类 com.sun.tools.attach.VirtualMachine 。
启动一个新的进程来连接到 正在运行中的进程,并令其加载 java agent。
基本的类与上一节的描述相同,主要是包 me.fangfeng.javaagent.*
和 me.fangfeng.client.*
新增一个类 me.fangfeng.javaagent.Main
用来启动另一个进程,并要求运行中的 java 进程加载 agent.jar 来进行增强。
package me.fangfeng.javaagent;
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 java.io.IOException;
/**
* @author fangfeng
* @since 2018/8/7
*/
public class Main {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
VirtualMachine vm = null;
try {
// 通过 VirtualMachine 连接到 运行中的进程 (可以通过 jps 找到进程号)
vm = VirtualMachine.attach();
vm.loadAgent();
} finally {
if (vm != null) {
vm.detach();
}
}
}
}
public class Agent {
public static void premain(String args, Instrumentation instrumentation) {
ClassTimer transformer = new ClassTimer();
instrumentation.addTransformer(transformer);
}
// 现在在 Agent.java 上补上 agentmain(...) 的具体实现
public static void agentmain(String args, Instrumentation instrumentation) throws UnmodifiableClassException {
System.out.println("SUCCESS AGENTMAIN");
ClassTimer transformer = new ClassTimer();
// add Transformer
instrumentation.addTransformer(transformer, true);
// 对 Rand.class 进行重新转换
instrumentation.retransformClasses(Rand.class);
}
}
其它内容基本相同
首先需要先打包 agent.jar 。当然,如果是顺着本文的顺序进行本机实验,则 agent.jar 已经存在
先启动进程 java me.fangfeng.client.Main
通过 jps
获取 Main 进程的 PID
在 java me.fangfeng.javaagent.Main
中替换上进程号,并执行
从执行结果可以看到,原进程首先正常执行代码,等到被 load Agent 之后,字节码已经有了新的变化,从而导致输出结果动态的产生了变化。
当然,需要注意的是,执行中的进程被要求 load Agent 之后,运行中的 Class 将被改写,并始终如此,知道进程终止。再下一次重新启动
以上描述的内容也可以理解为是 BTrace 实现的基础。毕竟,JVMTI (JVM Tool Interface) 原本的目的就是赋予使用者一个在运行中
查询系统各项数据的权利
当然,实现上,上述代码直接将各种增强(计时)硬编码到该进程中,同时统一使用了该进程的输入输出。
但是,BTrace 通过 Socket 将这些分离,检测代码通过 Socket 发回新的进程来维持输入输出。
在此,不再细说。
[1]. 示例代码: instru.zip
[2]. java.lang.instrument.Instrumentation
[3]. Package java.lang.instrument