系列文章目录
Java管理扩展JMX入门学习
Spring JMX编程学习(一)- 手动注册bean到MBeanServer
Spring JMX编程学习(二)- 以Bean的方式注册MbeanServer
Spring JMX编程学习(三)- 自定义JMX客户端
Spring JMX编程学习(四)- MBeans自动探测与注解
Spring JMX编程学习(五)- SpringBoot自动注册
前言
在前面的章节中我们从Java原生编程到Spring,在原生Java中从创建一个MBean,到创建MBeanServer,注册MBean,在Spring中通过自动探测模式查找包含有相关注解(比如org.springframework.jmx.export.annotation.ManagedResource
)的Bean,通过org.springframework.jmx.export.assembler.MBeanInfoAssembler
查找ModelMBeanInfo,然后将对应的Bean适配为javax.management.modelmbean.RequiredModelMBean(org.springframework.jmx.export.SpringModelMBean),同时还提供了org.springframework.jmx.export.naming.ObjectNamingStrategy
策略接口用于从注解中获取MBean的ObjectName,而在SpringBoot当中又会是怎样的呢?
提示:以下是本篇文章正文内容,下面案例可供参考
一、Spring Boot中使用JMX的方式
创建一个接口以及实现类
package com.example.spring.jmx;
public interface IJmxTestBean {
int add(int x, int y);
long myOperation();
int getAge();
void setAge(int age);
String getName();
void setName(String name);
}
和
package com.example.spring.jmx;
import org.springframework.jmx.export.annotation.*;
@ManagedResource(
objectName = "com.example.spring.jmx:name=testBean4",
description = "My Managed Bean",
log = true,
logFile = "jmx.log",
currencyTimeLimit = 15,
persistPolicy = "OnUpdate",
persistPeriod = 200,
persistLocation = "foo",
persistName = "bar")
public class AnnotationTestBean implements IJmxTestBean {
private String name;
private int age;
@Override
@ManagedAttribute(description = "The Age Attribute", currencyTimeLimit = 15)
public int getAge() {
return age;
}
@Override
public void setAge(int age) {
this.age = age;
}
@Override
@ManagedAttribute(defaultValue = "foo", persistPeriod = 300)
public String getName() {
return name;
}
@Override
@ManagedAttribute(description = "The Name Attribute",
currencyTimeLimit = 20,
defaultValue = "bar",
persistPolicy = "OnUpdate")
public void setName(String name) {
this.name = name;
}
@Override
@ManagedOperation(description = "Add two numbers")
@ManagedOperationParameters({
@ManagedOperationParameter(name = "x", description = "The first number"),
@ManagedOperationParameter(name = "y", description = "The second number")})
public int add(int x, int y) {
return x + y;
}
@Override
public long myOperation() {
return 0;
}
public void dontExposeMe() {
throw new RuntimeException();
}
}
然后创建一个配置类用于注册一个Bean
package com.example.spring.jmx;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BootConfig {
@Bean
public AnnotationTestBean testBean() {
AnnotationTestBean testBean = new AnnotationTestBean();
testBean.setAge(18);
testBean.setName("如花");
return testBean;
}
}
编写配置文件,注意配置文件中修改了org.springframework.jmx
包的日志级别为debug,方便启动后查看注册相关日志
server.port=8081
logging.level.root=info
logging.level.org.springframework.jmx=debug
创建一个启动类
package com.example.spring.jmx;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class JmxSpringBootStartMain {
public static void main(String[] args) {
SpringApplication.run(JmxSpringBootStartMain.class, args);
}
}
启动之后查看日志,可以看出容器注册了MBean.那么Spring Boot是怎么做到的呢?首先在org.springframework.boot.autoconfigure.SpringBootApplication注解当中包含了org.springframework.boot.autoconfigure.EnableAutoConfiguration注解
而EnableAutoConfiguration注解又通过AutoConfigurationImportSelector这个类在Spring加载bean配置的时候从META-INF/spring.factories配置文件中读取所有键值为org.springframework.boot.autoconfigure.EnableAutoConfiguration的类进行加载,而这其中就包含了自动注册JMX相关Bean的类:org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration
对应的类声明如下,这是一个配置类,如果当前classpath下包含有类MBeanExporter(ConditionalOnClass)这个配置类就会被注册到容器当中了。因为spring.jmx.enabled
如果没有配置也是true。
在这个配置类中,分别注册了MBeanServer、objectNamingStrategy以及mbeanExporter,都是前面提到过的,只不过现在由Spring Boot自动注册了。但是如果需要通过客户端访问我们还差一步,就是需要注册一个serverConnector
。我们在BootConfig中添加如下代码
@Bean
public JMXConnectorServer serverConnector(Registry registry) throws IOException, JMException {
ConnectorServerFactoryBean factory = new ConnectorServerFactoryBean();
factory.setServiceUrl("service:jmx:rmi://localhost/jndi/rmi://localhost:1099/jmxrmi");
factory.afterPropertiesSet();
return factory.getObject();
}
@Bean
public Registry registry() throws Exception {
RmiRegistryFactoryBean factory = new RmiRegistryFactoryBean();
factory.setPort(1099);
factory.afterPropertiesSet();
return factory.getObject();
}
因为当前创建的serverConnector依赖于RMI协议,所以需要创建一个registry,而且必须是在serverConnector之前初始化,因为在serverConnector方法当中将registry作为参数传入,这样就能保证先后顺序了。还有以下方式,也可以保证顺序性
这里改为static方法
@Bean
public static Registry registry() throws Exception {
RmiRegistryFactoryBean factory = new RmiRegistryFactoryBean();
factory.setPort(1099);
factory.afterPropertiesSet();
return factory.getObject();
}
@Bean
public JMXConnectorServer serverConnector() throws IOException, JMException {
ConnectorServerFactoryBean factory = new ConnectorServerFactoryBean();
factory.setServiceUrl("service:jmx:rmi://localhost/jndi/rmi://localhost:1099/jmxrmi");
factory.afterPropertiesSet();
return factory.getObject();
}
这里不分析具体原因了,感兴趣的话可以参考博客:@Bean注册Bean,你有多了解?
二、客户端调用的问题
创建一个SpringBoot客户端
package com.example.spring.client.jmx;
import com.example.spring.jmx.IJmxTestBean;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.jmx.access.MBeanProxyFactoryBean;
import org.springframework.jmx.support.MBeanServerConnectionFactoryBean;
import org.springframework.util.ClassUtils;
import javax.management.MBeanServerConnection;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import java.io.IOException;
@SpringBootApplication
public class JmxSpringBootStartClient {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(JmxSpringBootStartClient.class, args);
IJmxTestBean jmxTestBean = applicationContext.getBean(IJmxTestBean.class);
System.out.println(jmxTestBean.getName());
System.out.println(jmxTestBean.add(1, 2));
}
@Bean
public MBeanServerConnection mBeanServerConnection() throws IOException {
MBeanServerConnectionFactoryBean factoryBean = new MBeanServerConnectionFactoryBean();
factoryBean.setServiceUrl("service:jmx:rmi://localhost/jndi/rmi://localhost:1099/jmxrmi");
factoryBean.setConnectOnStartup(false);
factoryBean.afterPropertiesSet();
return factoryBean.getObject();
}
@Bean
public IJmxTestBean proxy() throws IOException, MalformedObjectNameException {
MBeanProxyFactoryBean factoryBean = new MBeanProxyFactoryBean();
factoryBean.setObjectName(new ObjectName("com.example.spring.jmx:name=testBean4"));
factoryBean.setProxyInterface(IJmxTestBean.class);
factoryBean.setServer(mBeanServerConnection());
factoryBean.afterPropertiesSet();
return (IJmxTestBean) factoryBean.getObject();
}
}
执行结果如下所示
三、是否面向接口的问题
在上面的服务端或是客户端,我们将com.example.spring.jmx.AnnotationTestBean
类型的bean转为MBean进行暴露,不知道大家有没有疑问,这个类一定需要实现一个接口吗?我们不妨去掉实现的接口,修改类如下:
package com.example.spring.jmx;
import org.springframework.jmx.export.annotation.*;
@ManagedResource(
objectName = "com.example.spring.jmx:name=testBean4",
description = "My Managed Bean",
log = true,
logFile = "jmx.log",
currencyTimeLimit = 15,
persistPolicy = "OnUpdate",
persistPeriod = 200,
persistLocation = "foo",
persistName = "bar")
public class AnnotationTestBean {
private String name;
private int age;
@ManagedAttribute(description = "The Age Attribute", currencyTimeLimit = 15)
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@ManagedAttribute(defaultValue = "foo", persistPeriod = 300)
public String getName() {
return name;
}
@ManagedAttribute(description = "The Name Attribute",
currencyTimeLimit = 20,
defaultValue = "bar",
persistPolicy = "OnUpdate")
public void setName(String name) {
this.name = name;
}
@ManagedOperation(description = "Add two numbers")
@ManagedOperationParameters({
@ManagedOperationParameter(name = "x", description = "The first number"),
@ManagedOperationParameter(name = "y", description = "The second number")})
public int add(int x, int y) {
return x + y;
}
public long myOperation() {
return 0;
}
public void dontExposeMe() {
throw new RuntimeException();
}
}
然后再次启动服务端,从这个结果看是没有问题的、
但是问题出在了客户端上,为啥呢?现在对应的类没有实现IJmxTestBean接口,关于proxy的定义必须修改了。首先proxyInterface或managementInterface这两个参数必须要传。
而现在没有所谓的接口,能不能传递类呢?我们尝试修改如下
@Bean
public AnnotationTestBean proxy() throws IOException, MalformedObjectNameException {
MBeanProxyFactoryBean factoryBean = new MBeanProxyFactoryBean();
factoryBean.setObjectName(new ObjectName("com.example.spring.jmx:name=testBean4"));
factoryBean.setProxyInterface(AnnotationTestBean.class);
factoryBean.setServer(mBeanServerConnection());
factoryBean.afterPropertiesSet();
return (AnnotationTestBean) factoryBean.getObject();
}
运行客户端这个时候抛出如下的异常
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.example.spring.jmx.AnnotationTestBean]: Factory method 'proxy' threw exception; nested exception is java.lang.IllegalArgumentException: [com.example.spring.jmx.AnnotationTestBean] is not an interface
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185) ~[spring-beans-5.1.10.RELEASE.jar:5.1.10.RELEASE]
at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:622) ~[spring-beans-5.1.10.RELEASE.jar:5.1.10.RELEASE]
... 19 common frames omitted
Caused by: java.lang.IllegalArgumentException: [com.example.spring.jmx.AnnotationTestBean] is not an interface
at org.springframework.aop.framework.AdvisedSupport.addInterface(AdvisedSupport.java:210) ~[spring-aop-5.1.10.RELEASE.jar:5.1.10.RELEASE]
at org.springframework.aop.framework.ProxyFactory.<init>(ProxyFactory.java:72) ~[spring-aop-5.1.10.RELEASE.jar:5.1.10.RELEASE]
at org.springframework.jmx.access.MBeanProxyFactoryBean.afterPropertiesSet(MBeanProxyFactoryBean.java:97) ~[spring-context-5.1.10.RELEASE.jar:5.1.10.RELEASE]
at com.example.spring.client.jmx.JmxSpringBootStartClient.proxy(JmxSpringBootStartClient.java:42) [classes/:na]
at com.example.spring.client.jmx.JmxSpringBootStartClient$$EnhancerBySpringCGLIB$$d645d62b.CGLIB$proxy$0(<generated>) ~[classes/:na]
at com.example.spring.client.jmx.JmxSpringBootStartClient$$EnhancerBySpringCGLIB$$d645d62b$$FastClassBySpringCGLIB$$a38b3d09.invoke(<generated>) ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:244) ~[spring-core-5.1.10.RELEASE.jar:5.1.10.RELEASE]
at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:363) ~[spring-context-5.1.10.RELEASE.jar:5.1.10.RELEASE]
at com.example.spring.client.jmx.JmxSpringBootStartClient$$EnhancerBySpringCGLIB$$d645d62b.proxy(<generated>) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_121]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_121]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_121]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_121]
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154) ~[spring-beans-5.1.10.RELEASE.jar:5.1.10.RELEASE]
... 20 common frames omitted
那么此时该如何办呢?当然了面向接口编程,就可以避免这这种问题了,不过我们可以尝试从Java原生接口来解决这个问题。首先去掉代理相关配置
通过原生java获取MBean
ConfigurableApplicationContext applicationContext = SpringApplication.run(JmxSpringBootStartClient.class, args);
MBeanServerConnection mbsc = (MBeanServerConnection) applicationContext.getBean("mBeanServerConnection");
String name = "com.example.spring.jmx:name=testBean4";
ObjectName mbeanName = new ObjectName(name);
MBeanInfo mBeanInfo = mbsc.getMBeanInfo(mbeanName);
System.out.println(mBeanInfo);
ObjectInstance objectInstance = mbsc.getObjectInstance(mbeanName);
Object add = mbsc.invoke(mbeanName, "add", new Object[]{1, 2}, new String[]{int.class.getName(), int.class.getName()});
System.out.println(add);
此时控制台打印如下信息
javax.management.modelmbean.ModelMBeanInfoSupport[description=My Managed Bean, attributes=[ModelMBeanAttributeInfo: Age ; Description: The Age Attribute ; Types: int ; isReadable: true ; isWritable: false ; Descriptor: currencyTimeLimit=15, default=, descriptorType=attribute, displayName=Age, getMethod=getAge, name=Age, ModelMBeanAttributeInfo: Name ; Description: The Name Attribute ; Types: java.lang.String ; isReadable: true ; isWritable: true ; Descriptor: currencyTimeLimit=20, default=foo, descriptorType=attribute, displayName=Name, getMethod=getName, name=Name, persistPeriod=300, persistPolicy=OnUpdate, setMethod=setName], constructors=[], operations=[ModelMBeanOperationInfo: add ; Description: Add two numbers ; Descriptor: descriptorType=operation, displayName=add, name=add, role=operation ; ReturnType: int ; Signature: int, int, , ModelMBeanOperationInfo: getName ; Description: getName ; Descriptor: descriptorType=operation, displayName=getName, name=getName, role=getter, visibility=(4) ; ReturnType: java.lang.String ; Signature: , ModelMBeanOperationInfo: setName ; Description: The Name Attribute ; Descriptor: descriptorType=operation, displayName=setName, name=setName, role=setter, visibility=(4) ; ReturnType: void ; Signature: java.lang.String, , ModelMBeanOperationInfo: getAge ; Description: The Age Attribute ; Descriptor: descriptorType=operation, displayName=getAge, name=getAge, role=getter, visibility=(4) ; ReturnType: int ; Signature: ], notifications=[], descriptor=currencyTimeLimit=15, descriptorType=mbean, displayName=com.example.spring.jmx.AnnotationTestBean, log=true, logFile=jmx.log, name=com.example.spring.jmx.AnnotationTestBean, persistLocation=foo, persistName=bar, persistPeriod=200, persistPolicy=OnUpdate, visibility=1]
3

总结
本章学习在Spring Boot中JMX的使用,其实与前面都是大同小异的,只是通过Spring Boot的自动配置功能减少了一些功能bean的注册而已