JMX 管理接口

JMX(Java Management Extensions)是JavaSE提供的监控应用程序状态、修改程序配置的标准接口。通过JMX,我们可以很方便的查看当前Java程序的内存堆栈、垃圾回收情况等许多信息。我们也可以自定义JMX接口,用于查看内部运行情况,以及修改应用配置。

很多偏向底层的中间件,其管理都是通过JMX实现的,比如Weblogic等。相比于自定义一组管理协议或是写一个管理页面,JMX提供了一种系统监控和管理的标准方式。

基本概念

JMX的架构如图所示:

  • MBean:提供了我们需要监控和管理的内容的JavaBean,其中的资源会通过MBeanServer暴露出去
  • MBeanServer:用于注册MBean以及响应JMX客户端的操作

上面图其实很直观,Java程序启动后,系统自带的MBean以及我们自定义的MBean会被注册到MBeanServer;MBeanServer这个JMX服务端组件集成了多种协议的Connector,支持包括RMI、HTTP/SOAP等协议;程序启动后,外部客户端就可以通过JMX端口以对应协议访问相关MBean提供的资源。

为应用程序开启JMX

JDK自带的JConsole工具其实就是一个JMX客户端,我们可以用该工具进行JMX的连接测试。

本地Java程序JMX支持

大部分JVM默认开启了JMX功能,我们不需要做任何配置,但默认情况仅限于本地进程间通讯,我们可以使用JDK自带的JConsole工具直接连接一个本地Java程序。

上图中,我们可以看到本地的Java进程,JConsole工具可以直接连接查看。

开启RMI协议JMX支持

很多情况我们可能需要远程访问JMX,例如我们开发一个监控程序通过JMX监控一组Java应用,那么此时就需要JMX通过网络协议来连接,其中最常用的是RMI。

开启基于RMI的JMX需要添加如下JVM启动参数:

-Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.rmi.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false

参数中指定了JMX的主机名(具体使用时填写连接时对应主机IP)、端口,以及不开启密码认证、不开启SSL的配置(生产环境可以考虑开启)。JConsole中的远程进程可以通过指定这些参数进行连接。

容器环境下的JMX支持

除此之外,如果是k8s容器环境,这里我们就无法填写hostname主机名了,因为k8s调度下的容器IP不是固定的。此时的最佳解决方案是这样的:我们主机名还是写127.0.0.1,同时使用kubectl命令,将容器内端口映射到本地,来实现JMX的远程连接。映射命令例子如下:

.\kubectl.exe --kubeconfig ./config port-forward -n dev order-687998bc67-srmjr --address 0.0.0.0 1099:1099

JConsole连接JMX

JConsole连接JMX效果如图所示:

连接上后,我们就可以通过图形界面查看各种信息,或者直接选择相应的MBean查看各种属性了。上图中,我们查看了com.sun.management.OperatingSystemMXBean这个类的Name属性,属性值表示我这里使用的操作系统是Windows11。

自定义MBean

上面图中我们可以看到大量系统自带的MBean,其中包含了许多系统状态信息。实际上,我们也可以注册自定义MBean。下面是一个例子:

AppInfoMBean.java

package com.gacfox.demojmx;

/**
 * @author gacfox
 */
public interface AppInfoMBean {
    String getInfo();
    void doSomething();
}

AppInfo.java

package com.gacfox.demojmx;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author gacfox
 */
public class AppInfo implements AppInfoMBean {
    @Override
    public String getInfo() {
        // 返回一些例子信息
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return String.format("Hello, JMX! Current Time is %s.", sdf.format(new Date()));
    }

    @Override
    public void doSomething() {
        System.out.println("Method doSomething called!");
    }

}

Main.java

package com.gacfox.demojmx;

import java.lang.management.ManagementFactory;

import javax.management.MBeanServer;
import javax.management.ObjectName;

/**
 * @author gacfox
 */
public class Main {
    public static void main(String[] args) {

        // 注册到MBeanServer
        try {
            MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
            AppInfo appInfo = new AppInfo();
            ObjectName objectName = new ObjectName("com.gacfox.demojmx:type=AppInfo");
            mBeanServer.registerMBean(appInfo, objectName);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 作为例子,写一个死循环防止程序结束
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

JMX中对MBean的写法有一定要求:

  1. 必须为“接口/实现类”模式
  2. 接口命名必须以MBeanMXBean结尾
  3. 实现类必须为标准JavaBean写法

遵循上述写法编写MBean后,我们即可将其注册到MBeanServer,这个对象可以通过ManagementFactory.getPlatformMBeanServer()方法获取。注册时需要指定一个MBean名字,通常写法都是包名:type=类型。注意这里MBean是单例的。

通过JConsole连接后,效果如下:

我们可以看到,MBean下有两类内容,属性和操作:

  1. 属性对应Get/Set方法,如果只有Get方法那么属性只读,如果有Set方法那么该属性还可以编辑
  2. 操作对应我们自定义方法,支持带参数

通过代码调用JMX

上面例子中我们是通过JConsole工具调用JMX,查看MBean资源的。如果我们基于JMX自己开发一个监控系统,其实可以直接通过代码方式调用JMX。下面是一个例子:

package com.gacfox.demojmxclient;

import javax.management.MBeanInfo;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

/**
 * @author gacfox
 */
public class Main {
    public static void main(String[] args) {
        try {
            // 建立JMX连接
            JMXServiceURL jmxServiceURL = new JMXServiceURL(
                    "service:jmx:rmi://localhost:1099/jndi/rmi://localhost:1099/jmxrmi");
            JMXConnector jmxConnector = JMXConnectorFactory.connect(jmxServiceURL);
            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();

            // 根据ObjectName对象调用
            ObjectName objectName = new ObjectName("com.gacfox.demojmx:type=AppInfo");
            // 获取MBean信息
            MBeanInfo mBeanInfo = connection.getMBeanInfo(objectName);
            System.out.println(mBeanInfo);
            // 获取属性(调用Get方法)
            Object info = connection.getAttribute(objectName, "Info");
            System.out.println(info);
            // 调用操作
            connection.invoke(objectName, "doSomething", null, null);

            // 关闭JMX连接
            jmxConnector.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

代码中,我们直接通过JMX的URL方式连接服务端,然后通过ObjectName定义即可找到MBean,接下来就可以读写属性,或是调用操作了。

上面JMX客户端的操作都是通过MBean名、属性名、操作名等实现,完全不与JMX服务端代码耦合,这也是Java语言反射机制的强大之处,C++就很难实现类似的效果。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。