JMX的一些简要知识点
2017-06-11 / modified at 2022-04-04 / 1.4k words / 5 mins
️This article has been over 2 years since the last update.

JMX在实际开发中很多人都用过,鼠标点一下就可以执行Java代码。本文适用于已经会在JConsole等工具中调用JMX,但希望进一步了解JMX的人。

什么是JMX,有什么用?

JMX是 Java 管理扩展(Java Management Extensions, JSR 160)的缩写,实际上就是一个基于TCP的远程eval后门,是sun闭源的实现,在日常开发中,一般用于

  • 热部署: 比如Log4j(更新Log等级), Script(比如Java内置js实现浅层定制等), Hibernate对象(数据库字段)等。这里可以搞出很多黑科技,提高开发效率
  • 监控统计: 查看DCS缓存命中率,Zk健康,MQ流控,DDS热点等中间件的状态。
  • 远程操作: 远程清空缓存,dump数据等,这个一般用得很少。

从鼠标点击到Bean被调用的端到端流程分析

下面以zkServer为例,Zookeeper提供了很多JMX调试接口,在Jconsole都是可以直接点击的。

为了方便折腾Zk,我们首先下载zk的源码工程,然后用IDEA导入后,配置如下Debug的参数,然后点击启动即可

1
2
Main class: org.apache.zookeeper.server.quorum.QuorumPeerMain
Program arguments: ./zookeeper-3.4.9/conf/zoo_sample.cfg

接着在ZooKeeperServerBean中的resetStatistics打上断点。然后打开Jconsole,找到此Bean,点击此方法运行。

JXM Debug

没有意外地话,断点应该被Trigger上了,这里我们就可以通过调用栈进行简要分析了。

通过对调用栈分析,我们可以发现

  • JVM专门开了一个RMI(Java Method Invocation)的线程进行监听,并自己通过TCP搞了一套协议(也就是说可以用WireShark抓包),通过对TCP的InputStream进行查看,可以发现JMX实现了自己的应用层抽象语法。
  • 通过一个大for/switch实现了路由匹配,并通过一个Map找到Method进行invoke调用:javax.management.remote.rmi.RMIConnection.invoke()
  • 最终路由到实际Bean,并执行Java代码。

可以看出,JMX主要是在做传输协议,更像是在做【通信】,这部分具体分析工作量非常大,本文就不分析了。

如何开发自己第一个JMX?

我个人不太喜欢从零开始创建一个工程,而是喜欢先定制,再移植出公共模块(详见我之前写的团队技能培训相关Tag)。同样以Zookeeper为例,我们可以按照如下方法进行折腾:

  • 写一个扩展ZooKeeperServerBean的class,并注册到zk上
  • 尝试添加属性与方法到刚刚写的class上
  • 删除extend的类,尝试自己实现MBean接口造轮子

这样步骤有一个好处就是,你最开始学JMX就接触到的是行业水平最高的实践,比如MBeanRegistry的单例化,ObjectName的生成方法,JConsole对应Class的直观映射,都是很好的封装。

经过折腾,例子如下

1
2
3
4
// 接口必须是 MXBean 的后缀
public interface CalcMXBean {
Integer eval(String script);
}

实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CalcBean implements CalcMXBean{

public CalcBean() {
}

@Override
public Integer eval(String script) {
// 此处仅供演示使用,有安全风险与性能问题
ScriptEngine engine = new ScriptEngineManager().getEngineByName("javascript");
Integer eval = 0;
try {
eval = ((Integer) engine.eval(script));
}catch (Exception e){
eval = 0;
e.printStackTrace();
}
return eval;
}
}

然后我们再进行注册就搞定了

1
2
3
4
//如下代码放到 org.apache.zookeeper.server.ZooKeeperServer#registerJMX
//jmx在客户端就是通过objectName进行method调用的
ObjectName objName = new ObjectName("com.test.calctest:name=calcBean")
ManagementFactory.getPlatformMBeanServer().registerMBean(calcBean,objName);

这样我们就成功地设计了一个简陋的JMX计算器,由于它可以eval任何js脚本,因此再加一些黑科技就相当于是一个后门了(生产环境绝不能这样搞)。

总结

JMX并不是什么非常专业高深的技术,因为它在各种框架中早被高度封装,它主要代码量体现在闭源的【通信】代码上。因此作为开发,只需要合理地暴露接口即可

  • 只要写一个 /.*MXBean/的接口与其实现类,并配置好ObjectName,就可以注册到JMX中,然后通过JConsole或者MBeanServerConnection进行invoke
  • JMX与REST服务类似,只是一个桩,代码实现应该是调用Spring或者微服务,而不是全部写到MXBean中
  • 本文没有讲get/set属性,这个可以下来试一下,本文实验入门成功后就可以直接看文档

开发JMX业务需要注意什么?

JMX提供了直接调用Java代码的能力,因此非常方便开发定位,同时也有极大的安全风险

  • 业务要满足安全要求: 比如不能接收String去eval,不能跳过鉴权去操作业务,最好不要返回任何业务信息,而只返回一个统计。这类代码可以私下调试用,但是不能上代码库,更不能说去配置一个开关在生产环境上屏蔽后门。也就是说,最终发布的二进制不能有任何有安全风险的JMX调用。
  • 在Product环境中要关闭JMX: 这个一般在setEnv.sh等类似文件中均可以配置,如果开发有调试需求,可以配置鉴权的Key,并只监听Localhost,避免在公网暴露。
  • 当前的TCP实现是sun的闭源实现,如果对安全性要求极高的企业级应用,需要自己实现定制私有协议的JMX(jmx.default.class.loader)。