0%

jclasslib 「Attach To Running JVM」 实现

jclasslib 是一款字节码编辑工具,可以对 class 字节码进行可视化操作。

jclasslib 提供 Attach To Running JVM 操作,用于查看与修改系统运行的 JVM 加载的类。该功能借助了 Agent 和 JMX,Agent 负责将对应的 MBean 注册到 JMX 中,通过 JMX 通讯协议调用其 MBean 操作。

本文不会讲述 jclasslib 基本操作,对于这部分知识,大家可以参考 jclasslib github

JMX 是什么

https://en.wikipedia.org/wiki/Java_Management_Extensions

JMX 是 Java 平台提供管理与监控系统信息的工具,内部使用 MBean 进行消息传递,开发者可以向 MBeanServer 中注册自定义的 MBean。可以通过 jconsole 可视化工具或 JMX 通讯客户端,从指定的 JVM 中获取注册 MBean 信息或调用其暴露的方法。

jclasslib agent

jclasslib 源代码的 modules 目录中,包含一个 agent 模块,只有该模块使用 Java 进行编写的,其它都是使用 Kotlin,目的是为了解决兼容性问题。jclasslib 进行打包时,agent 模块会打包成 jclasslib-agent.jar,后续操作都要依赖它。

点击 jclasslib “Attach To Running JVM” 按钮,会触发如下的操作,首先调用 attachToVm 将 agent 加载到对应的 JVM 中,applyVmConnection 设置 connection flag,browseClasspathAction 触发选择 class 界面。

1
2
3
4
5
6
7
// org.gjt.jclasslib.browser.BrowserFrame#getAttachVmAction
val attachVmAction = DefaultAction(getString("action.attach.to.jvm"), getString("action.attach.to.jvm.description"), "attach.svg") {
attachToVm(this)?.let {
applyVmConnection(it)
browseClasspathAction()
}
}

来看到 attachToVm 方法,在进行 Attach To Running JVM 操作时,会列出当前系统运行的 JVM,然后让你选择想要 attach 的 JVM,目的是为了获取该 JVM id,从而拿到 VirtualMachine 来进行 load agent 操作。

总结一下,该方法可以划分为两个操作,1) 加载 agent,2)与 JVM 的 JMX 建立链接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// modules/browser/src/main/kotlin/browser/attach/Attach.kt:47
fun attachToVm(parentWindow: Window?): VmConnection? =
selectVm(parentWindow)?.let { attachableVm ->
val vm = try {
VirtualMachine.attach(attachableVm.descriptor.id()).also { vm ->
vm.loadAgent(getAgentPath())
}
} catch (e: Exception) {
alertFacade.showMessage(parentWindow, getString("message.attach.failed.0", e.message ?: ""), null, AlertType.ERROR)
return@let null
}
val connectorAddress = try {
vm.getConnectorAddress() ?: vm.run {
startLocalManagementAgent()
getConnectorAddress()
}
} catch (e: Exception) {
alertFacade.showMessage(parentWindow, getString("message.management.agent.error.0", e.message ?: ""), null, AlertType.ERROR)
return@let null
}
val connection = try {
JMXConnectorFactory.connect(JMXServiceURL(connectorAddress))
} catch (e: Exception) {
alertFacade.showMessage(parentWindow, getString("message.connection.failed.0", e.message ?: ""), null, AlertType.ERROR)
return@let null
}
val communicator = MBeanServerInvocationHandler.newProxyInstance(
connection.mBeanServerConnection,
ObjectName(AgentMain.MBEAN_NAME),
CommunicatorMBean::class.java,
false
)
VmConnection(communicator, connection, vm)
}

因要进行加载 agent 操作,我们将关注点放在 agent 模块中,看到 agent gradle 文件,从该 jar 配置信息可以得知,进行加载 agent 操作时,会调用 org.jclasslib.agent.AgentMain 的 agentMain 方法。

1
2
3
4
5
6
7
8
9
10
11
12
jar {
archiveFileName.set("jclasslib-agent.jar")
manifest {
attributes(
"Agent-Class" to "org.jclasslib.agent.AgentMain",
"Premain-Class" to "org.jclasslib.agent.AgentMain",
"Can-Redefine-Classes" to "true",
"Can-Retransform-Classes" to "true"
)
}
from(sourceSets["java9"].output)
}

egentMain 方法会直接调用其内部的 init 方法,该方法目的向 MBeanServer 注册一个名为 org.jclasslib:name=agent 的 MBean,该 MBean 是整个操作的核心。

1
2
3
4
5
6
7
8
9
10
11
12
// org.jclasslib.agent.AgentMain#agentMain
public static void agentmain(String args, Instrumentation instrumentation) throws Exception {
init(instrumentation);
}

private static void init(Instrumentation instrumentation) throws Exception {
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
ObjectName objectName = new ObjectName(MBEAN_NAME);
if (!server.isRegistered(objectName)) {
server.registerMBean(new Communicator(instrumentation), objectName);
}
}

Communicator 实现

MBean 是 Java 一个普通的类,但该类需要实现名为 XXXMBean 接口

Communicator 是一个 MBean,它实现了 CommunicatorMBean 接口,其接口向外部暴露了三个方法。

1
2
3
4
5
public interface CommunicatorMBean {
List<ClassDescriptor> getClasses();
byte[] getClassFile(String fileName);
ReplacementResult replaceClassFile(String fileName, byte[] bytes);
}

Communicator 接收 Instrumentation 作为构造参数,上述接口暴露的方法实现都要依靠于该 Instrumentation 实例。

下面对比较常用的 getClassFile 方法进行讲解,首先将 fileName 中的 / 替换成 .,然后从 instrumentation 加载的类中进行查找,如果未发现,说明该 JVM 并未加载该类,则不需要后续操作。进行 map 操作时调用其 getClassFile(Class<?> c) 方法,在该方法中会创建 ReadClassFileTransformer 实例,将该实例添加到 instrumentation 中,并调用其 retransformClasses 方法,目的是为了重新加载该 class,从而在 ReadClassFileTransformer#transform 拿到该类的字节码。重新加载 class 后,需要将 ReadClassFileTransformer 从 instrumentation 移除,最后将字节码 byte array 返回即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// org.jclasslib.agent.Communicator#getClassFile(String)
public byte[] getClassFile(String fileName) {
String className = fileName.replace('/', '.');
return findClass(className)
.map(this::getClassFile)
.orElse(null);
}

@SuppressWarnings("rawtypes")
private Optional<Class> findClass(String className) {
return Arrays.stream(instrumentation.getAllLoadedClasses())
.filter(c -> c.getName().equals(className))
.findFirst();
}

private byte[] getClassFile(Class<?> c) {
ReadClassFileTransformer transformer = new ReadClassFileTransformer();
try {
instrumentation.addTransformer(transformer, true);
instrumentation.retransformClasses(c);
} catch (Throwable e) {
e.printStackTrace();
return null;
} finally {
instrumentation.removeTransformer(transformer);
}
return transformer.bytes;
}

private static class ReadClassFileTransformer implements ClassFileTransformer {
byte[] bytes;

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (classBeingRedefined != null) {
this.bytes = classfileBuffer;
}
return null;
}
}

尾语

注册 Communicator MBean 后,整个实现也就完结了,后续只需通过 JMX 与该 MBean 进行通讯就可以实现后续的操作。

参考文档

  1. JMX wiki
  2. jclasslib