最近工作中会用到JMX这个技术,所以在官网看了一些关于JMX的文档,也在网上找了一些介绍JMX的博客来看,并动手开始实践,整个下来的感觉就是有点云里雾里,总觉得对JMX还是没有很深刻的认识,所以决定多分析,提取关键点,以自己熟悉的文字和方式记录下来,从而加强对JMX原理的理解,也希望能够帮助到需要了解JMX技术的同学。这是第一篇,以一个Standard MBean开始,从代码编写层面分析JMX。实验环境采用的JDK版本为JDK 8。
首先,要明白的是JMX是一份规范,除了SUN公司自己实现了这份规范外,还有其它的一些实现,比如JBoss的JMX实现、MX4J等等。但是在JDK 4及之前,SUN公司自己实现的JMX是可选的,并不包含在JDK中,直到JDK 5才包含了JMX这一部分。另外,JDK 5中也不是包含了JMX的全部,有一些工具类还是没有包含在JDK 5中,比如实验代码中还会使用到HtmlAdaptorServer,所以还需要单独引入下面这个依赖
<dependency>
<groupId>com.sun.jdmk</groupId>
<artifactId>jmxtools</artifactId>
<version>1.2.1</version>
</dependency>
接着,编写一个叫做StudentMBean的接口
package com.cenmee.jmx.smb;
public interface StudentMBean {
public String getName();
public void setName(String name);
public void study(String course);
}
接着,编写一个实现了StudentMBean接口的Java类,名叫Student
package com.cenmee.jmx.smb;
public class Student implements StudentMBean {
private String nickName = "zhangsan";
@Override
public String getName() {
return this.nickName;
}
@Override
public void setName(String name) {
this.nickName = name;
}
@Override
public void study(String course) {
System.out.println(this.nickName + " is studying " + course);
}
}
接着,编写一个带主函数的Java类
package com.cenmee.jmx.smb;
import com.sun.jdmk.comm.HtmlAdaptorServer;
import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.ObjectName;
public class StudentMBeanAgent {
public static void main(String[] args) throws Exception {
String domain = "com.cenmee.jmx";
// MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
MBeanServer mBeanServer = MBeanServerFactory.createMBeanServer();
ObjectName studentMBeanName = new ObjectName(domain + ":name=StudentMBean");
mBeanServer.registerMBean(new Student(), studentMBeanName);
ObjectName htmlAdaptorServerName = new ObjectName(domain + ":name=HtmlAdaptorServer");
HtmlAdaptorServer htmlAdaptorServer = new HtmlAdaptorServer(8082);
mBeanServer.registerMBean(htmlAdaptorServer, htmlAdaptorServerName);
htmlAdaptorServer.start();
System.out.println("start html adaptor server ok...");
}
}
最后,执行StudentMBeanAgent类中的主函数,打开浏览器并访问:http://localhost:8082/
点击name=StudentMBean进入MBean详情页就可以看到我们编写的MBean的详细信息
在JMX中MBean是Managed Bean的缩写,它代表着拥有一些状态和行为且可以被管理的资源;如果想要获取它们的状态和操纵它们的行为,就必须通过某种方式将它们的状态和行为暴露出来。简单地说,MBean就是一个Java对象,但是作为MBean,有以下一些特殊的地方:
- 对象中属性字段并不表示MBean的属性,且getter和setter方法并不表示MBean的方法,而是表示属性,比如getName()这个getter方法表示的是这个MBean的Name属性(注意不是小写的n)
- getter方法表示这个MBean的属性是可读的;setter方法表示这个MBean的属性是可写的
此外,在JMX中一共有四种MBean,分别是Standard MBean、Model MBean、Dynamic MBean和Open MBean。要开发一个Standard MBean,必须符合以下规范:
- 定义一个接口,接口的名字必须是XXXMBean,接口中定义的方法就是暴露的Standard MBean的属性和方法,这些属性和方法就是可以被访问的
- 定义一个实现了XXXMBean接口的类,名字必须是XXX
所以以上例子中的Student类就是一个Standard MBean,它实现了StudentMBean接口,该接口中一共定义了三个方法,其中的getName和setName方法符合getter方法和setter方法的编写规范,所以它们分别就是getter方法和setter方法,所以这个MBean拥有一个名叫study的方法和一个名叫Name的属性,且这个属性是可读写的
有了可被管理的MBean之后,就需要将这些MBean管理起来,此时就用到了MBeanServer,MBeanServer作为MBean的容器,所有的MBean会在MBeanServer中进行注册以便MBeanServer可以管理这些MBean,且MBeanServer中注册的每一个MBean都有一个唯一的对象名称标识,这个对象名称标识由域和属性键值对组成,比如这里的“com.cenmee.jmx:name=StudentMBean”,在查找和定位MBean时,正是通过这个唯一的对象名称标识来完成的。另外,还有一个概念叫做JMX Agent,Agent就是一个Java进程,也是MBeanServer的容器,所以Agent的核心组件就是MBeanServer;此外,Agent还会提供一些系统服务,且这些系统服务也是通过MBean形式注册在MBeanServer上的
MBean被管理起来后,我们就可以操控这些MBean了,但是远程应用程序(运行JMX Agent的JVM之外的应用程序)并不会直接和注册在MBeanServer上的MBean进行通信,而是通过Protocol Adaptors或Connectors向JMX Agent发送管理和控制请求
Adaptor和Connector的区别在于Adaptor是使用某种协议(比如HTTP)来与JMX Agent取得联系的,JMX Agent会有一个对象(Adaptor)来处理有关协议的细节,比如HTML Adaptor;而Connector则是使用类似RPC方式来访问JMX Agent的,在JMX Agent和远程应用程序两端都必须有这么一个对象来处理相应的请求和应答,比如RMI Connector。整个JMX Agent可以有多个Adaptor和Connector,所以可以使用多种不同的方式来访问JMX Agent,从而达到管理MBean的目的
在以上的Standard MBean例子中,我们就是使用了HTML Adaptor,比如我们还可以同时使用RMI Connector来连接JMX Agent
package com.cenmee.jmx.smb;
import com.sun.jdmk.comm.HtmlAdaptorServer;
import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.ObjectName;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import java.rmi.registry.LocateRegistry;
public class StudentMBeanAgent {
public static void main(String[] args) throws Exception {
String domain = "com.cenmee.jmx";
MBeanServer mBeanServer = MBeanServerFactory.createMBeanServer();
ObjectName studentMBeanName = new ObjectName(domain + ":name=StudentMBean");
mBeanServer.registerMBean(new Student(), studentMBeanName);
ObjectName htmlAdaptorServerName = new ObjectName(domain + ":name=HtmlAdaptorServer");
HtmlAdaptorServer htmlAdaptorServer = new HtmlAdaptorServer(8082);
mBeanServer.registerMBean(htmlAdaptorServer, htmlAdaptorServerName);
htmlAdaptorServer.start();
System.out.println("start html adaptor server ok...");
int rmiPort = 1099;
LocateRegistry.createRegistry(rmiPort);
JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + rmiPort + "/" + domain);
JMXConnectorServer jmxConnectorServer = JMXConnectorServerFactory.newJMXConnectorServer(url, null, mBeanServer);
jmxConnectorServer.start();
System.out.println("start rmi connector server ok...");
}
}
以上代码中就同时使用了HTML Adaptor和RMI Connector,所以不仅可以通过在浏览器输入http://localhost:8082/来访问JMX Agent之外,还可以通过RMI方式来访问,要注意的是在使用RMI Connector时,“LocateRegistry.createRegistry(rmiPort);”这一行代码不可少,否则就会出现类似下面这样的错误;这一行代码的作用就是可以在特定的端口创建名字服务,从而无需程序员手动开启
Exception in thread "main" java.io.IOException: Cannot bind to URL [rmi://localhost:1099/com.cenmee.jmx]: javax.naming.ServiceUnavailableException [Root exception is java.rmi.ConnectException: Connection refused to host: localhost; nested exception is:
java.net.ConnectException: Connection refused (Connection refused)]
at javax.management.remote.rmi.RMIConnectorServer.newIOException(RMIConnectorServer.java:827)
at javax.management.remote.rmi.RMIConnectorServer.start(RMIConnectorServer.java:432)
at com.cenmee.jmx.smb.StudentMBeanAgent.main(StudentMBeanAgent.java:35)
Caused by: javax.naming.ServiceUnavailableException [Root exception is java.rmi.ConnectException: Connection refused to host: localhost; nested exception is:
java.net.ConnectException: Connection refused (Connection refused)]
at com.sun.jndi.rmi.registry.RegistryContext.bind(RegistryContext.java:161)
at com.sun.jndi.toolkit.url.GenericURLContext.bind(GenericURLContext.java:228)
at javax.naming.InitialContext.bind(InitialContext.java:425)
at javax.management.remote.rmi.RMIConnectorServer.bind(RMIConnectorServer.java:644)
at javax.management.remote.rmi.RMIConnectorServer.start(RMIConnectorServer.java:427)
... 1 more
Caused by: java.rmi.ConnectException: Connection refused to host: localhost; nested exception is:
java.net.ConnectException: Connection refused (Connection refused)
at sun.rmi.transport.tcp.TCPEndpoint.newSocket(TCPEndpoint.java:619)
at sun.rmi.transport.tcp.TCPChannel.createConnection(TCPChannel.java:216)
at sun.rmi.transport.tcp.TCPChannel.newConnection(TCPChannel.java:202)
at sun.rmi.server.UnicastRef.newCall(UnicastRef.java:342)
at sun.rmi.registry.RegistryImpl_Stub.bind(Unknown Source)
at com.sun.jndi.rmi.registry.RegistryContext.bind(RegistryContext.java:155)
... 5 more
Caused by: java.net.ConnectException: Connection refused (Connection refused)
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)
at java.net.Socket.connect(Socket.java:538)
at java.net.Socket.<init>(Socket.java:434)
at java.net.Socket.<init>(Socket.java:211)
at sun.rmi.transport.proxy.RMIDirectSocketFactory.createSocket(RMIDirectSocketFactory.java:40)
at sun.rmi.transport.proxy.RMIMasterSocketFactory.createSocket(RMIMasterSocketFactory.java:148)
at sun.rmi.transport.tcp.TCPEndpoint.newSocket(TCPEndpoint.java:613)
... 10 more
如果非要省略这一行代码而采用手动开启的话,那么在运行JMX Agent之前需要先使用rmiregistry命令来创建名字服务
cd `echo $JAVA_HOME/bin`
rmiregistry 1099 &
启动好JMX Agent之后,我们可以通过JConsole作为RMI客户端来访问JMX Agent,打开JConsole之后,在本地进程中就可以看到我们的JMX Agent进程(DEBUG启动方式才能看到这个进程),通过双击这个本地进程同样可以连接上JMX Agent;由于我们使用了RMI Connector,所以我们还可以在远程进程中输入service:jmx:rmi:///jndi/rmi://127.0.0.1:1099/com.cenmee.jmx这个URL来连接JMX Agent
连接上之后也能看到我们自己编写的这个Standard MBean
此外,除了使用现有的JConsole作为RMI客户端来使用,我们还可以自己编写RMI客户端来访问JMX Agent
package com.cenmee.jmx.smb;
import javax.management.Attribute;
import javax.management.MBeanServerConnection;
import javax.management.MBeanServerInvocationHandler;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
public class CustomRMIClient {
public static void main(String[] args) throws Exception {
int rmiPort = 1099;
String domain = "com.cenmee.jmx";
JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://127.0.0.1:" + rmiPort + "/" + domain);
JMXConnector jmxConnector = JMXConnectorFactory.connect(url);
MBeanServerConnection jmxConnection = jmxConnector.getMBeanServerConnection();
// MBean唯一名字,通过它来定位查找MBean
ObjectName studentMBeanName = new ObjectName(domain + ":name=StudentMBean");
// 设置和读取MBean的属性
jmxConnection.setAttribute(studentMBeanName, new Attribute("Name", "wangwu"));
System.out.println(jmxConnection.getAttribute(studentMBeanName, "Name"));;
// 通过代理方式来调用MBean的方法
StudentMBean studentMBean = MBeanServerInvocationHandler.newProxyInstance(jmxConnection, studentMBeanName, StudentMBean.class, false);
studentMBean.study("chinese");
// 通过RMI方式来调用MBean的方法
jmxConnection.invoke(studentMBeanName, "study", new String[] {"chinese"}, new String[] {String.class.getName()});
jmxConnector.close();
}
}
以上内容就是编写一个MBean并管理这个MBean所需要最基本的概念与原理,整体来说JMX基本框架与调用过程可以用下面这幅图来描述
参考文章: