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 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 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 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 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 进行通讯就可以实现后续的操作。
参考文档
JMX wiki
jclasslib