Spring JMX编程学习(三)- 自定义JMX客户端

6 篇文章 1 订阅

系列文章目录

Java管理扩展JMX入门学习
Spring JMX编程学习(一)- 手动注册bean到MBeanServer
Spring JMX编程学习(二)- 以Bean的方式注册MbeanServer
Spring JMX编程学习(三)- 自定义JMX客户端
Spring JMX编程学习(四)- MBeans自动探测与注解
Spring JMX编程学习(五)- SpringBoot自动注册



前言

在前面的项目当中,我们通过Spring创建了MBeanServer并且将Spring的Bean作为MBean注册到其中,并且通过JConsole客户端查看MBean的属性和操作,其实这些都还只是属于本地操作,使用的是一个JVM(JConsole和运行的Server小程序),本章将学习通过Spring创建自定义的JMX客户端,通过客户端调用服务端的MBean,此时客户端和服务端其实是运行在不同的JVM当中的,属于远程调用


提示:以下是本篇文章正文内容,下面案例可供参考

一、暴露MBeanServer

如果想MBeanServer被远程服务访问到,首先需要通过JMXServerConnector进行暴露,然后在客户端通过MBeanServerConnection进行连接。
在这里插入图片描述
在服务端的配置文件spring-jmx-server.xml中添加一个ConnectorServerFactoryBean类型的Bean,这个类型的bean会在服务端创建一个符合JSR-160标准的JMXConnectorServer并把它注册到指定的MBeanServer(所谓的attach),然后开启连接。

<bean id="serverConnector"
      class="org.springframework.jmx.support.ConnectorServerFactoryBean" depends-on="registry">
    <property name="serviceUrl"
              value="service:jmx:rmi://localhost/jndi/rmi://localhost:1099/jmxrmi"/>
</bean>

<!--通过rmi方式-->
<bean id="registry" class="org.springframework.remoting.rmi.RmiRegistryFactoryBean">
    <property name="port" value="1099"/>
</bean>

这里有两点需要注意,第一个是serviceUrl参数必须配置,因为如果不配置的话,默认值为service:jmx:jmxmp://localhost:9875,这个是一个jmxmp协议的连接。它会通过jmxmp协议在9875端口上暴露本地的MBeanServer,但是这个协议在JSR-160标准中是标识为可选择的,当前的JDK8并不支持这个协议。如果使用这个协议,会抛出如下的异常

Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'serverConnector' defined in class path resource [spring-jmx-server.xml]: Invocation of init method failed; nested exception is java.net.MalformedURLException: Unsupported protocol: jmxmp
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1778)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:593)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:828)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:877)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:549)
	at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:144)
	at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:85)
	at com.example.spring.jmx.JmxStartServerMain.main(JmxStartServerMain.java:12)
Caused by: java.net.MalformedURLException: Unsupported protocol: jmxmp
	at javax.management.remote.JMXConnectorServerFactory.newJMXConnectorServer(JMXConnectorServerFactory.java:344)
	at org.springframework.jmx.support.ConnectorServerFactoryBean.afterPropertiesSet(ConnectorServerFactoryBean.java:159)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1837)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1774)
	... 12 more

此处我们选用了RMI协议,使用了RMI协议有个前提,这也是我们需要注意的第二点。当使用RMI协议的连接器时,我们需要响应的查找服务(tnameserv or rmiregistry)已经开启了,在Spring当中可以通过RmiRegistryFactoryBean来完成,这也是上面引入这个registry的原因,通过RMI协议,我们定义的serviceUrl格式为service:jmx:rmi://localhost/jndi/rmi://localhost:1099/jmxrmi,在本地1099端口暴露服务,另外这里需要registry这个Bean首先初始化,然后才是serverConnector,否则无法建立连接,抛出以下异常

17:08:42.599 [main] WARN org.springframework.context.support.ClassPathXmlApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'serverConnector' defined in class path resource [spring-jmx-server.xml]: Invocation of init method failed; nested exception is java.io.IOException: Cannot bind to URL [rmi://localhost:1099/jmxrmi]: javax.naming.ServiceUnavailableException [Root exception is java.rmi.ConnectException: Connection refused to host: localhost; nested exception is: 
	java.net.ConnectException: Connection refused: connect]
17:08:42.599 [main] DEBUG org.springframework.jmx.export.MBeanExporter - Unregistering JMX-exposed beans on shutdown
Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'serverConnector' defined in class path resource [spring-jmx-server.xml]: Invocation of init method failed; nested exception is java.io.IOException: Cannot bind to URL [rmi://localhost:1099/jmxrmi]: javax.naming.ServiceUnavailableException [Root exception is java.rmi.ConnectException: Connection refused to host: localhost; nested exception is: 
	java.net.ConnectException: Connection refused: connect]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1778)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:593)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:828)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:877)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:549)
	at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:144)
	at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:85)
	at com.example.spring.jmx.JmxStartServerMain.main(JmxStartServerMain.java:12)
Caused by: java.io.IOException: Cannot bind to URL [rmi://localhost:1099/jmxrmi]: javax.naming.ServiceUnavailableException [Root exception is java.rmi.ConnectException: Connection refused to host: localhost; nested exception is: 
	java.net.ConnectException: Connection refused: connect]
	at javax.management.remote.rmi.RMIConnectorServer.newIOException(RMIConnectorServer.java:827)
	at javax.management.remote.rmi.RMIConnectorServer.start(RMIConnectorServer.java:432)
	at org.springframework.jmx.support.ConnectorServerFactoryBean.afterPropertiesSet(ConnectorServerFactoryBean.java:193)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1837)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1774)
	... 12 more
Caused by: javax.naming.ServiceUnavailableException [Root exception is java.rmi.ConnectException: Connection refused to host: localhost; nested exception is: 
	java.net.ConnectException: Connection refused: connect]
	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)
	... 15 more
Caused by: java.rmi.ConnectException: Connection refused to host: localhost; nested exception is: 
	java.net.ConnectException: Connection refused: connect
	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)
	... 19 more
Caused by: java.net.ConnectException: Connection refused: connect
	at java.net.DualStackPlainSocketImpl.connect0(Native Method)
	at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:79)
	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.PlainSocketImpl.connect(PlainSocketImpl.java:172)
	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)
	... 24 more

我们可以通过将这两个bean在xml中换个位置来保证顺序,但是这样既不优雅也可能为以后修改导致bug,我们可以通过在serverConnector配置中添加depends-on="registry"来解决这个问题。这属于Spring的基础知识了,此处不详述原理。

启动主类,打印日志如下

"C:\Program Files\Java\jdk1.8.0_121\bin\java.exe" "-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2019.3.1\lib\idea_rt.jar=58496:D:\Program Files\JetBrains\IntelliJ IDEA 2019.3.1\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_121\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\rt.jar;D:\app\embrace\target\classes;D:\maven\repo\org\springframework\boot\spring-boot-starter-web\2.1.9.RELEASE\spring-boot-starter-web-2.1.9.RELEASE.jar;D:\maven\repo\org\springframework\boot\spring-boot-starter\2.1.9.RELEASE\spring-boot-starter-2.1.9.RELEASE.jar;D:\maven\repo\org\springframework\boot\spring-boot\2.1.9.RELEASE\spring-boot-2.1.9.RELEASE.jar;D:\maven\repo\org\springframework\boot\spring-boot-autoconfigure\2.1.9.RELEASE\spring-boot-autoconfigure-2.1.9.RELEASE.jar;D:\maven\repo\org\springframework\boot\spring-boot-starter-logging\2.1.9.RELEASE\spring-boot-starter-logging-2.1.9.RELEASE.jar;D:\maven\repo\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;D:\maven\repo\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;D:\maven\repo\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar;D:\maven\repo\org\apache\logging\log4j\log4j-to-slf4j\2.11.2\log4j-to-slf4j-2.11.2.jar;D:\maven\repo\org\apache\logging\log4j\log4j-api\2.11.2\log4j-api-2.11.2.jar;D:\maven\repo\org\slf4j\jul-to-slf4j\1.7.28\jul-to-slf4j-1.7.28.jar;D:\maven\repo\javax\annotation\javax.annotation-api\1.3.2\javax.annotation-api-1.3.2.jar;D:\maven\repo\org\springframework\spring-core\5.1.10.RELEASE\spring-core-5.1.10.RELEASE.jar;D:\maven\repo\org\springframework\spring-jcl\5.1.10.RELEASE\spring-jcl-5.1.10.RELEASE.jar;D:\maven\repo\org\yaml\snakeyaml\1.23\snakeyaml-1.23.jar;D:\maven\repo\org\springframework\boot\spring-boot-starter-json\2.1.9.RELEASE\spring-boot-starter-json-2.1.9.RELEASE.jar;D:\maven\repo\com\fasterxml\jackson\core\jackson-databind\2.9.9.3\jackson-databind-2.9.9.3.jar;D:\maven\repo\com\fasterxml\jackson\core\jackson-annotations\2.9.0\jackson-annotations-2.9.0.jar;D:\maven\repo\com\fasterxml\jackson\core\jackson-core\2.9.9\jackson-core-2.9.9.jar;D:\maven\repo\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.9.9\jackson-datatype-jdk8-2.9.9.jar;D:\maven\repo\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.9.9\jackson-datatype-jsr310-2.9.9.jar;D:\maven\repo\com\fasterxml\jackson\module\jackson-module-parameter-names\2.9.9\jackson-module-parameter-names-2.9.9.jar;D:\maven\repo\org\springframework\boot\spring-boot-starter-tomcat\2.1.9.RELEASE\spring-boot-starter-tomcat-2.1.9.RELEASE.jar;D:\maven\repo\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.26\tomcat-embed-websocket-9.0.26.jar;D:\maven\repo\org\hibernate\validator\hibernate-validator\6.0.17.Final\hibernate-validator-6.0.17.Final.jar;D:\maven\repo\javax\validation\validation-api\2.0.1.Final\validation-api-2.0.1.Final.jar;D:\maven\repo\org\jboss\logging\jboss-logging\3.3.2.Final\jboss-logging-3.3.2.Final.jar;D:\maven\repo\com\fasterxml\classmate\1.3.4\classmate-1.3.4.jar;D:\maven\repo\org\springframework\spring-web\5.1.10.RELEASE\spring-web-5.1.10.RELEASE.jar;D:\maven\repo\org\springframework\spring-beans\5.1.10.RELEASE\spring-beans-5.1.10.RELEASE.jar;D:\maven\repo\org\springframework\spring-webmvc\5.1.10.RELEASE\spring-webmvc-5.1.10.RELEASE.jar;D:\maven\repo\org\springframework\spring-aop\5.1.10.RELEASE\spring-aop-5.1.10.RELEASE.jar;D:\maven\repo\org\springframework\spring-context\5.1.10.RELEASE\spring-context-5.1.10.RELEASE.jar;D:\maven\repo\org\springframework\spring-expression\5.1.10.RELEASE\spring-expression-5.1.10.RELEASE.jar;D:\maven\repo\javax\servlet\javax.servlet-api\3.1.0\javax.servlet-api-3.1.0.jar;D:\maven\repo\org\apache\tomcat\embed\tomcat-embed-core\8.5.23\tomcat-embed-core-8.5.23.jar;D:\maven\repo\org\apache\tomcat\tomcat-annotations-api\8.5.23\tomcat-annotations-api-8.5.23.jar;D:\maven\repo\org\apache\tomcat\embed\tomcat-embed-jasper\8.5.16\tomcat-embed-jasper-8.5.16.jar;D:\maven\repo\org\apache\tomcat\embed\tomcat-embed-el\8.5.16\tomcat-embed-el-8.5.16.jar;D:\maven\repo\org\eclipse\jdt\ecj\3.12.3\ecj-3.12.3.jar" com.example.spring.jmx.JmxStartServerMain
17:19:07.169 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@12bb4df8
17:19:07.453 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 5 bean definitions from class path resource [spring-jmx-server.xml]
17:19:07.564 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'mbeanServer'
17:19:07.743 [main] DEBUG org.springframework.jmx.support.JmxUtils - Found MBeanServer: com.sun.jmx.mbeanserver.JmxMBeanServer@553a3d88
17:19:07.747 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'exporter'
17:19:07.761 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'testBean'
17:19:07.796 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'registry'
17:19:07.802 [main] DEBUG org.springframework.remoting.rmi.RmiRegistryFactoryBean - Looking for RMI registry at port '1099'
17:19:09.447 [main] DEBUG org.springframework.remoting.rmi.RmiRegistryFactoryBean - Could not detect RMI registry - creating new one
17:19:09.455 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'serverConnector'
17:19:09.459 [main] DEBUG org.springframework.jmx.support.JmxUtils - Found MBeanServer: com.sun.jmx.mbeanserver.JmxMBeanServer@553a3d88
17:19:09.553 [main] INFO org.springframework.jmx.support.ConnectorServerFactoryBean - JMX connector server started: javax.management.remote.rmi.RMIConnectorServer@1dd02175
17:19:09.553 [main] DEBUG org.springframework.jmx.export.MBeanExporter - Registering beans for JMX exposure on startup
17:19:09.556 [main] DEBUG org.springframework.jmx.export.MBeanExporter - Located managed bean 'com.example.spring.jmx:name=testBean': registering with JMX server as MBean [com.example.spring.jmx:name=testBean]

在这里插入图片描述

二、自定义客户端

1. 创建客户端连接clientConnector

这里需要首先创建一个项目,依赖配置相同,另外还需要共享com.example.spring.jmx.IJmxTestBean接口,然后创建spring-jmx-client.xml配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="clientConnector"
          class="org.springframework.jmx.support.MBeanServerConnectionFactoryBean">
        <property name="serviceUrl" value="service:jmx:rmi://localhost/jndi/rmi://localhost:1099/jmxrmi"/>
    </bean>
    
</beans>

通过MBeanServerConnectionFactoryBean创建一个MBeanServerConnection去连接远程的JMXServerConnector。这里的serviceUrl与服务端是一样的,因为虽然是两个项目(两个虚拟机),但还是在同一台主机之上,如果不是,其中的localhost需要改为指定的IP地址。

2. 注册MBean代理Bean

当创建好了客户端与服务端的连接之后,我们需要通过代理的方式引用MBeanServer中的MBean了,我们通过Spring的MBeanProxyFactoryBean就可以了。在spring-jmx-client.xml中添加如下bean配置

<bean id="proxy" class="org.springframework.jmx.access.MBeanProxyFactoryBean">
    <property name="objectName" value="com.example.spring.jmx:name=testBean"/>
    <property name="proxyInterface" value="com.example.spring.jmx.IJmxTestBean"/>
    <property name="server" ref="clientConnector"/>
</bean>

3. 创建客户端启动类

创建如下的启动类,启动Spring容器,根据beanName获取代理类并调用。

package com.example.spring.jmx;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class JmxStartClientMain {

    public static void main(String[] args) {
        // 启动Spring容器
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-jmx-client.xml");
        IJmxTestBean proxy = (IJmxTestBean) applicationContext.getBean("proxy");
        System.out.println(proxy.getName());
        System.out.println(proxy.add(2, 3));
    }
}

执行结果如下,说明我们的自定义客户端是没有问题的。
在这里插入图片描述

三、源码分析

1. 服务端

首先查看org.springframework.remoting.rmi.RmiRegistryFactoryBean这个类的继承结构,实现了FactoryBean和InitializingBean这两个接口,老套路,主要的逻辑还是在afterPropertiesSet这个方法当中
在这里插入图片描述

@Override
public void afterPropertiesSet() throws Exception {
	// Check socket factories for registry.
	1. 默认情况下clientSocketFactory、serverSocketFactory都是没有配置的 
	if (this.clientSocketFactory instanceof RMIServerSocketFactory) {
		this.serverSocketFactory = (RMIServerSocketFactory) this.clientSocketFactory;
	}
	if ((this.clientSocketFactory != null && this.serverSocketFactory == null) ||
			(this.clientSocketFactory == null && this.serverSocketFactory != null)) {
		throw new IllegalArgumentException(
				"Both RMIClientSocketFactory and RMIServerSocketFactory or none required");
	}
	2 获取RMI注册进行暴露
	// Fetch RMI registry to expose.
	this.registry = getRegistry(this.host, this.port, this.clientSocketFactory, this.serverSocketFactory);
}

如果配置了registryHost则说明要获取远程的Registry,如果不是则在本地获取

protected Registry getRegistry(String registryHost, int registryPort,
		@Nullable RMIClientSocketFactory clientSocketFactory, @Nullable RMIServerSocketFactory serverSocketFactory)
		throws RemoteException {

	if (registryHost != null) {
		// Host explicitly specified: only lookup possible.
		if (logger.isDebugEnabled()) {
			logger.debug("Looking for RMI registry at port '" + registryPort + "' of host [" + registryHost + "]");
		}
		Registry reg = LocateRegistry.getRegistry(registryHost, registryPort, clientSocketFactory);
		testRegistry(reg);
		return reg;
	}

	else {
		return getRegistry(registryPort, clientSocketFactory, serverSocketFactory);
	}
}

最后会调用到如下的方法,最后创建一个Registry用于RMI服务注册。

protected Registry getRegistry(int registryPort) throws RemoteException {
	1. alwaysCreate属性默认为false 不需要每次都创建
	if (this.alwaysCreate) {
		logger.debug("Creating new RMI registry");
		this.created = true;
		return LocateRegistry.createRegistry(registryPort);
	}
	if (logger.isDebugEnabled()) {
		logger.debug("Looking for RMI registry at port '" + registryPort + "'");
	}
	synchronized (LocateRegistry.class) {
		try {
			// Retrieve existing registry.
			2 尝试获取已经存在的registry
			Registry reg = LocateRegistry.getRegistry(registryPort);
			3 获取的registry不一定真实有用 此处进行测试 如果有问题 就会抛出异常
			testRegistry(reg);
			return reg;
		}
		catch (RemoteException ex) {
			logger.trace("RMI registry access threw exception", ex);
			logger.debug("Could not detect RMI registry - creating new one");
			// Assume no registry found -> create new one.
			this.created = true;
			4. 从本地获取的registry存在的问题 需要再次重新创建一个
			return LocateRegistry.createRegistry(registryPort);
		}
	}
}

对于ConnectorServerFactoryBean这个类与上面的类继承结构是一样的,都是套路呀,查看afterPropertiesSet方法即可
在这里插入图片描述

@Override
public void afterPropertiesSet() throws JMException, IOException {
    1. 获取本地的MBeanServer
	if (this.server == null) {
		this.server = JmxUtils.locateMBeanServer();
	}

    2. 根据serviceUrl创建JMXConnectorServer并且附着到MBeanServer当中
	// Create the JMX service URL.
	JMXServiceURL url = new JMXServiceURL(this.serviceUrl);

	// Create the connector server now.
	this.connectorServer = JMXConnectorServerFactory.newJMXConnectorServer(url, this.environment, this.server);

	// Set the given MBeanServerForwarder, if any.
	if (this.forwarder != null) {
		this.connectorServer.setMBeanServerForwarder(this.forwarder);
	}
    
    3. 如果设置了objectName也会将当前bean注册到MBeanServer当中,此处没有设置,不会注册
	// Do we want to register the connector with the MBean server?
	if (this.objectName != null) {
		doRegister(this.connectorServer, this.objectName);
	}

	try {
	4. 开启一个新的线程用于JMX Connector,通过设置threaded参数开启,通过daemon设置是否为守护线程,默认这两个参数都是false
		if (this.threaded) {
			// Start the connector server asynchronously (in a separate thread).
			final JMXConnectorServer serverToStart = this.connectorServer;
			Thread connectorThread = new Thread() {
				@Override
				public void run() {
					try {
						serverToStart.start();
					}
					catch (IOException ex) {
						throw new JmxException("Could not start JMX connector server after delay", ex);
					}
				}
			};

			connectorThread.setName("JMX Connector Thread [" + this.serviceUrl + "]");
			connectorThread.setDaemon(this.daemon);
			connectorThread.start();
		}
		else {
		    5. 开启JMXConnectorServer通道,此时客户端才能发起连接,前面仅仅是创建了而已
			// Start the connector server in the same thread.
			this.connectorServer.start();
		}

		if (logger.isInfoEnabled()) {
			logger.info("JMX connector server started: " + this.connectorServer);
		}
	}

	catch (IOException ex) {
		// Unregister the connector server if startup failed.
		unregisterBeans();
		throw ex;
	}
}

以上代码非常简单。

2. 客户端

对于客户端来说首先是通过MBeanServerConnectionFactoryBean创建MBeanServerConnection,然后通过这个连接到服务端已经创建好的JMXServerConnector。看一下类的继承结构,依然很简单,主要的逻辑还是在afterPropertiesSet方法中创建所需的连接。
在这里插入图片描述

private boolean connectOnStartup = true;
/**
 * Creates a {@code JMXConnector} for the given settings
 * and exposes the associated {@code MBeanServerConnection}.
 */
@Override
public void afterPropertiesSet() throws IOException {
	if (this.serviceUrl == null) {
		throw new IllegalArgumentException("Property 'serviceUrl' is required");
	}
	是否在启动时创建连接,默认为true
	if (this.connectOnStartup) {
		启动时(也就是当前)创建连接
		connect();
	}
	else {
		在第一次请求的时候创建连接
		createLazyConnection();
	}
}

对于在当前创建连接来说,非常简单,就是直接调用JDK的方法,没啥好说的。

private Map<String, Object> environment = new HashMap<>();
@Nullable
private JMXConnector connector;

@Nullable
private MBeanServerConnection connection;
/**
 * Connects to the remote {@code MBeanServer} using the configured service URL and
 * environment properties.
 */
private void connect() throws IOException {
	Assert.state(this.serviceUrl != null, "No JMXServiceURL set");
	this.connector = JMXConnectorFactory.connect(this.serviceUrl, this.environment);
	this.connection = this.connector.getMBeanServerConnection();
}

而对于懒加载的模式,这里使用了代理设置模式,通过创建代理的方式当前仅仅创建一个代理返回,而实际的连接在第一次请求时创建。

@Nullable
private JMXConnectorLazyInitTargetSource connectorTargetSource;

/**
 * Creates lazy proxies for the {@code JMXConnector} and {@code MBeanServerConnection}.
 */
private void createLazyConnection() {
	this.connectorTargetSource = new JMXConnectorLazyInitTargetSource();
	TargetSource connectionTargetSource = new MBeanServerConnectionLazyInitTargetSource();

	this.connector = (JMXConnector)
			new ProxyFactory(JMXConnector.class, this.connectorTargetSource).getProxy(this.beanClassLoader);
	this.connection = (MBeanServerConnection)
			new ProxyFactory(MBeanServerConnection.class, connectionTargetSource).getProxy(this.beanClassLoader);
}

这里出现了两个TargetSource实现类。它们会在第一次调用代理的时候才会通过createObject方法创建真实的对象,当前返回的connector和connection都是代理对象,这样就实现了懒加载的创建连接对象。
在这里插入图片描述
调用情况如下所示

/**
 * Lazily creates a {@code JMXConnector} using the configured service URL
 * and environment properties.
 * @see MBeanServerConnectionFactoryBean#setServiceUrl(String)
 * @see MBeanServerConnectionFactoryBean#setEnvironment(java.util.Properties)
 */
private class JMXConnectorLazyInitTargetSource extends AbstractLazyCreationTargetSource {

	@Override
	protected Object createObject() throws Exception {
		Assert.state(serviceUrl != null, "No JMXServiceURL set");
		return JMXConnectorFactory.connect(serviceUrl, environment);
	}

	@Override
	public Class<?> getTargetClass() {
		return JMXConnector.class;
	}
}


/**
 * Lazily creates an {@code MBeanServerConnection}.
 */
private class MBeanServerConnectionLazyInitTargetSource extends AbstractLazyCreationTargetSource {

	@Override
	protected Object createObject() throws Exception {
		Assert.state(connector != null, "JMXConnector not initialized");
		return connector.getMBeanServerConnection();
	}

	@Override
	public Class<?> getTargetClass() {
		return MBeanServerConnection.class;
	}
}

在这里插入图片描述
在这里插入图片描述
创建好了连接之后,就是在客户端调用MBean的操作了,因为实际的MBean对象是在服务端,此处需要调用必须通过网络请求,通常为了方便使用(面向接口编程),在本地创建一个对应接口的代理对象,然后在方法调用时发起网络请求获取结果,看起来更本地调用一样。而这里创建对象使用的是org.springframework.jmx.access.MBeanProxyFactoryBean这个类。
在这里插入图片描述
我们可以看到这个类除了我们之前见过的FactoryBeanInitializingBean类之外,还继承了MBeanClientInterceptor这样一个类,其实如果熟悉Spring AOP源码的话,看到MethodInterceptor的时候也没法好奇怪的。在上面我们创建连接代理时是通过传入TargetSource对象(用于代理执行时获取真实对象),此处创建代理传入的是Interceptor对象,也就是MBeanProxyFactoryBean自己了,这样在调用代理方法的时候其实需要执行org.aopalliance.intercept.MethodInterceptor#invoke方法。
创建代理的逻辑如下

@Nullable
private Object mbeanProxy;

@Override
public void afterPropertiesSet() throws MBeanServerNotFoundException, MBeanInfoRetrievalException {
    1. 保证父类的初始化逻辑
	super.afterPropertiesSet();
	2. 初始化必须包含proxyInterface不能为空 这里也可以看出配置managementInterface也是可以的	
	if (this.proxyInterface == null) {
		this.proxyInterface = getManagementInterface();
		if (this.proxyInterface == null) {
			throw new IllegalArgumentException("Property 'proxyInterface' or 'managementInterface' is required");
		}
	}
	else {
	    3. 如果设置了参数proxyInterface而没有设置managementInterface参数,则取proxyInterface作为managementInterface参数
		if (getManagementInterface() == null) {
			setManagementInterface(this.proxyInterface);
		}
	}
	4. 创建MBean代理对象 实现的接口为proxyInterface,增强逻辑为当前对象逻辑,其实就是MethodInteceptor的invoke方法的逻辑
	this.mbeanProxy = new ProxyFactory(this.proxyInterface, this).getProxy(this.beanClassLoader);
}

对于在父类中的初始化方法逻辑如下,主要还是关于懒加载的问题

public void afterPropertiesSet() {
	if (this.server != null && this.refreshOnConnectFailure) {
		throw new IllegalArgumentException("'refreshOnConnectFailure' does not work when setting " +
				"a 'server' reference. Prefer 'serviceUrl' etc instead.");
	}
	// 是否在启动过程中进行连接 这个参数默认为true
	if (this.connectOnStartup) {
		prepare();
	}
}
/**
 * Route the invocation to the configured managed resource..
 * @param invocation the {@code MethodInvocation} to re-route
 * @return the value returned as a result of the re-routed invocation
 * @throws Throwable an invocation error propagated to the user
 * @see #doInvoke
 * @see #handleConnectFailure
 */
@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
	// Lazily connect to MBeanServer if necessary.
	synchronized (this.preparationMonitor) {
		if (!isPrepared()) {
			prepare();
		}
	}
	try {
		return doInvoke(invocation);
	}
	catch (MBeanConnectFailureException | IOException ex) {
		return handleConnectFailure(invocation, ex);
	}
}
/**
 * Ensures that an {@code MBeanServerConnection} is configured and attempts
 * to detect a local connection if one is not supplied.
 */
public void prepare() {
	synchronized (this.preparationMonitor) {
		1. 如果配置了server参数,则通过server获取连接
		if (this.server != null) {
			this.serverToUse = this.server;
		}
		else {
		1.b 如果没有配置server参数,也可以通过serviceUrl来获取连接
			this.serverToUse = null;
			this.serverToUse = this.connector.connect(this.serviceUrl, this.environment, this.agentId);
		}

以下为配置serviceUrl的方式获取连接

<bean id="proxy" class="org.springframework.jmx.access.MBeanProxyFactoryBean">
    <property name="objectName" value="com.example.spring.jmx:name=testBean"/>
    <property name="proxyInterface" value="com.example.spring.jmx.IJmxTestBean"/>
    <!--server参数和serviceUrl参数二选一-->
<!--        <property name="server" ref="clientConnector"/>-->
    <property name="serviceUrl" value="service:jmx:rmi://localhost/jndi/rmi://localhost:1099/jmxrmi"/>
</bean>

以下通过创建JDK中的MBeanServerInvocationHandler对象

@Nullable
private MBeanServerInvocationHandler invocationHandler;
		this.invocationHandler = null;
		2. useStrictCasing参数默认为true
		if (this.useStrictCasing) {
			Assert.state(this.objectName != null, "No ObjectName set");
			// Use the JDK's own MBeanServerInvocationHandler, in particular for native MXBean support.
			this.invocationHandler = new MBeanServerInvocationHandler(this.serverToUse, this.objectName,
					(this.managementInterface != null && JMX.isMXBeanInterface(this.managementInterface)));
		}
		else {
			// Non-strict casing can only be achieved through custom invocation handling.
			// Only partial MXBean support available!
			3. useStrictCasing设置为false的话,则根据MBeanInfo填充allowedAttributes和allowedOperations属性值,然后在调用的时候,通过javax.management.MBeanServerConnection#getAttribute方法获取属性或者javax.management.MBeanServerConnection#invoke调用MBean操作
			retrieveMBeanInfo(this.serverToUse);
		}
	}
}

初始化完成以后,当客户端调用MBean代理的时候,会调用代理的invoke方法,同样也会进入到MBeanProxyFactoryBean的invoke方法(AOP增强逻辑),如下所示:

@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
	// Lazily connect to MBeanServer if necessary.
	1. 如果连接还没有创建的话,再次获取连接
	synchronized (this.preparationMonitor) {
		if (!isPrepared()) {
			prepare();
		}
	}
	try {
	    2. 调用方法
		return doInvoke(invocation);
	}
	catch (MBeanConnectFailureException | IOException ex) {
		return handleConnectFailure(invocation, ex);
	}
}

在默认情况下还是比较简单的。
在这里插入图片描述


总结

本章通过自定义JMX客户端与MBeanServer进行了连接,并调用MBean的方法

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lang20150928

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值