系列文章目录
Java管理扩展JMX入门学习
Spring JMX编程学习(一)- 手动注册bean到MBeanServer
Spring JMX编程学习(二)- 以Bean的方式注册MbeanServer
Spring JMX编程学习(三)- 自定义JMX客户端
Spring JMX编程学习(四)- MBeans自动探测与注解
Spring JMX编程学习(五)- SpringBoot自动注册
文章目录
前言
在上一章当中,我们学习了原生java中JMX的知识,本章开始学习在Spring当中如何使用JMX编程。
具体来说,Spring的JMX支持提供了四个核心功能:
- 将任何Spring bean自动注册为JMX MBean
- 灵活的机制来控制您的bean的管理界面
- 通过远程JSR-160连接器以声明方式公开MBean
- 本地和远程MBean资源的简单代理
这些功能旨在在不将应用程序组件耦合到Spring或JMX接口和类的情况下工作。 确实,在大多数情况下,您的应用程序类无需了解Spring或JMX即可利用Spring JMX功能。
提示:以下是本篇文章正文内容,下面案例可供参考
一、手动将bean导出到JMX
1. 引入库
因为是普通案例项目,直接引入一个Spring Boot的依赖即可。对应的版本为2.1.9.RELEASE。对应的Spring版本为5.1.10.RELEASE。
<springboot.version>2.1.9.RELEASE</springboot.version>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${springboot.version}</version>
</dependency>
2. 创建接口和实现类
接口如下
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;
public class JmxTestBean implements IJmxTestBean {
private String name;
private int age;
private boolean isSuperman;
@Override
public int getAge() {
return age;
}
@Override
public void setAge(int age) {
this.age = age;
}
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public int add(int x, int y) {
return x + y;
}
@Override
public long myOperation() {
return 0;
}
public void dontExposeMe() {
throw new RuntimeException();
}
}
3. 注册Bean并进行暴露
Spring是面向bean的,所以首先将上面的MBean实例注册为Spring的bean,然后再进行暴露到JMX,Spring中主要是通过了org.springframework.jmx.export.MBeanExporter来进行暴露的,MBeanExporter,名字取得很棒。创建一个Spring的配置文件spring-jmx-server.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">
<!--MBeans手动注册-->
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter" lazy-init="false">
<property name="beans">
<map>
<entry key="com.example.spring.jmx:name=testBean" value-ref="testBean"/>
</map>
</property>
</bean>
<bean id="testBean" class="com.example.spring.jmx.JmxTestBean">
<property name="name" value="I am a manual Server Side testBean"/>
<property name="age" value="100"/>
</bean>
</beans>
4. 创建一个主类并启动
在主程序当中读取以上xml启动Spring容器,然后启动
package com.example.spring.jmx;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class JmxStartServerMain {
public static void main(String[] args) throws InterruptedException {
// 启动Spring容器
new ClassPathXmlApplicationContext("spring-jmx-server.xml");
// 等待两个小时
new CountDownLatch(1).await(2, TimeUnit.HOURS);
}
}
启动控制台打印日志如下
18:37:57.561 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@12bb4df8
18:37:57.974 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 2 bean definitions from class path resource [spring-jmx-server.xml]
18:37:58.060 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'exporter'
18:37:58.112 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'testBean'
18:37:58.331 [main] DEBUG org.springframework.jmx.support.JmxUtils - Found MBeanServer: com.sun.jmx.mbeanserver.JmxMBeanServer@710726a3
18:37:58.332 [main] DEBUG org.springframework.jmx.export.MBeanExporter - Registering beans for JMX exposure on startup
18:37:58.340 [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]
通过日志不难看出已经将 MBean [com.example.spring.jmx:name=testBean]注册到了JMX server当中了。
通过JDK工具下的JConsole查看对应的进程,可以看出我们的Mean已经生效了(注册到了MBeanServer当中)。
对应的属性值
二、源码分析
通过Spring简单的一个MBeanExporter类型bean的配置,一方面启动了一个JMX server,另一方面对配置的MBean进行了注册操作。究竟是如何完成的呢?我们分析一下MBeanExporter这个类。
这个类实现了Spring中的BeanClassLoaderAware, BeanFactoryAware, InitializingBean, SmartInitializingSingleton, DisposableBean这五个接口,第一个和第二个在MBeanExporter类型bean初始化的时候传入相关的属性(classLoader和beanFactory),InitializingBean也是在初始化的时候执行的,不过在前面两个类的回调之后,对应的方法为afterPropertiesSet,SmartInitializingSingleton则是在所有Spring的所有非懒加载的bean初始化完成之后回调afterSingletonsInstantiated方法,以上这些接口都是在容器启动过程中执行的,而DisposableBean的接口是在容器销毁时回调的,与本次无关。我们的重点就放在afterPropertiesSet方法和afterSingletonsInstantiated两个方法上了。回调的顺序如下所示:
setBeanFactory
-> setResourceLoader
->afterPropertiesSet
-> afterSingletonsInstantiated
1. afterPropertiesSet设置MBeanServer属性
/**
* The {@code MBeanServer} instance being used to register beans.
*/
@Nullable
protected MBeanServer server;
@Override
public void afterPropertiesSet() {
// If no server was provided then try to find one. This is useful in an environment
// where there is already an MBeanServer loaded.
if (this.server == null) {
this.server = JmxUtils.locateMBeanServer();
}
}
这个回调方法用于保证当前对象的MBeanServer属性不为空,如果为空的话,则进行查找。
/**
* Attempt to find a locally running {@code MBeanServer}. Fails if no
* {@code MBeanServer} can be found. Logs a warning if more than one
* {@code MBeanServer} found, returning the first one from the list.
* @return the {@code MBeanServer} if found
* @throws MBeanServerNotFoundException if no {@code MBeanServer} could be found
* @see javax.management.MBeanServerFactory#findMBeanServer
*/
org.springframework.jmx.support.JmxUtils#locateMBeanServer(){
return locateMBeanServer(null);
}
public static MBeanServer locateMBeanServer(@Nullable String agentId) throws MBeanServerNotFoundException {
MBeanServer server = null;
// null means any registered server, but "" specifically means the platform server
if (!"".equals(agentId)) {
List<MBeanServer> servers = MBeanServerFactory.findMBeanServer(agentId);
if (!CollectionUtils.isEmpty(servers)) {
// Check to see if an MBeanServer is registered.
if (servers.size() > 1 && logger.isInfoEnabled()) {
logger.info("Found more than one MBeanServer instance" +
(agentId != null ? " with agent id [" + agentId + "]" : "") +
". Returning first from list.");
}
server = servers.get(0);
}
}
if (server == null && !StringUtils.hasLength(agentId)) {
// Attempt to load the PlatformMBeanServer.
try {
server = ManagementFactory.getPlatformMBeanServer();
}
catch (SecurityException ex) {
throw new MBeanServerNotFoundException("No specific MBeanServer found, " +
"and not allowed to obtain the Java platform MBeanServer", ex);
}
}
if (server == null) {
throw new MBeanServerNotFoundException(
"Unable to locate an MBeanServer instance" +
(agentId != null ? " with agent id [" + agentId + "]" : ""));
}
if (logger.isDebugEnabled()) {
logger.debug("Found MBeanServer: " + server);
}
return server;
}
上面的agentId用于标识不同的MBeanServer的,此处传入的为null,因此首先通过MBeanServerFactory.findMBeanServer来查找MBeanServer,如果之前系统还未创建的话就不存在,再通过ManagementFactory#getPlatformMBeanServer去获取,这个方法我们在上一章用过,如果当前环境中存在MBeanServer则直接使用,不存在则创建一个。此处就会去创建一个可用的MBeanServer。与原生Java是一样的。
2. afterSingletonsInstantiated注册MBean
/**
* Kick off bean registration automatically after the regular singleton instantiation phase.
* @see #registerBeans()
*/
@Override
public void afterSingletonsInstantiated() {
try {
logger.debug("Registering beans for JMX exposure on startup");
registerBeans();
registerNotificationListeners();
}
catch (RuntimeException ex) {
// Unregister beans already registered by this exporter.
unregisterNotificationListeners();
unregisterBeans();
throw ex;
}
}
在这个方法中,主要是两个逻辑,一个是将满足条件的Bean作为MBean注册到MBeanServer中,另一个就是注册NotificationListener
。这里的重点是在第一个。在registerBeans这个方法中会根据beans这个属性(Map类型,在上面的xml中我们就是通过这个属性配置了MBean的)和autodetectMode属性首先收集需要进行注册到MBeanServer中的bean,其中autodetectMode默认情况下为null,所以这里只会去注册用户自己配置的beans。(autodetectMode很重要,后续会详细讲解)
protected void registerBeans() {
// The beans property may be null, for example if we are relying solely on autodetection.
if (this.beans == null) {
1. 如果配置的beans为null 则会转为自动探测模式
this.beans = new HashMap<>();
// Use AUTODETECT_ALL as default in no beans specified explicitly.
if (this.autodetectMode == null) {
this.autodetectMode = AUTODETECT_ALL;
}
}
// Perform autodetection, if desired.
int mode = (this.autodetectMode != null ? this.autodetectMode : AUTODETECT_NONE);
if (mode != AUTODETECT_NONE) {
if (this.beanFactory == null) {
throw new MBeanExportException("Cannot autodetect MBeans if not running in a BeanFactory");
}
2. 自动探测用户定义的MBean
if (mode == AUTODETECT_MBEAN || mode == AUTODETECT_ALL) {
// Autodetect any beans that are already MBeans.
logger.debug("Autodetecting user-defined JMX MBeans");
autodetect(this.beans, (beanClass, beanName) -> isMBean(beanClass));
}
// Allow the assembler a chance to vote for bean inclusion.
if ((mode == AUTODETECT_ASSEMBLER || mode == AUTODETECT_ALL) &&
this.assembler instanceof AutodetectCapableMBeanInfoAssembler) {
autodetect(this.beans, ((AutodetectCapableMBeanInfoAssembler) this.assembler)::includeBean);
}
}
3. 如果用户有设置beans或者自动探测到MBean添加到beans,进行实例的注册
if (!this.beans.isEmpty()) {
this.beans.forEach((beanName, instance) -> registerBeanNameOrInstance(instance, beanName));
}
}
收集完成之后,就会进行registerBeanNameOrInstance操作了。对应源码如下
protected ObjectName registerBeanNameOrInstance(Object mapValue, String beanKey) throws MBeanExportException {
try {
1. 传入的实例为字符串类型
if (mapValue instanceof String) {
// Bean name pointing to a potentially lazy-init bean in the factory.
if (this.beanFactory == null) {
throw new MBeanExportException("Cannot resolve bean names if not running in a BeanFactory");
}
String beanName = (String) mapValue;
if (isBeanDefinitionLazyInit(this.beanFactory, beanName)) {
ObjectName objectName = registerLazyInit(beanName, beanKey);
replaceNotificationListenerBeanNameKeysIfNecessary(beanName, objectName);
return objectName;
}
else {
Object bean = this.beanFactory.getBean(beanName);
ObjectName objectName = registerBeanInstance(bean, beanKey);
replaceNotificationListenerBeanNameKeysIfNecessary(beanName, objectName);
return objectName;
}
}
2. 传入的实例非String类型 主要看这部分
else {
// Plain bean instance -> register it directly.
if (this.beanFactory != null) {
3. 根据当前对象类型重新从bean工厂拉取bean
Map<String, ?> beansOfSameType =
this.beanFactory.getBeansOfType(mapValue.getClass(), false, this.allowEagerInit);
for (Map.Entry<String, ?> entry : beansOfSameType.entrySet()) {
if (entry.getValue() == mapValue) {
String beanName = entry.getKey();
4. 注册
ObjectName objectName = registerBeanInstance(mapValue, beanKey);
replaceNotificationListenerBeanNameKeysIfNecessary(beanName, objectName);
return objectName;
}
}
}
return registerBeanInstance(mapValue, beanKey);
}
}
catch (Throwable ex) {
throw new UnableToRegisterMBeanException(
"Unable to register MBean [" + mapValue + "] with key '" + beanKey + "'", ex);
}
}
通过以上注册方法进来的时候mapValue是实际的Bean对象不是String(在bean初始化之前已经进行了属性填充,将xml中的String转化为了对应的bean实例对象),所以以上方法走的是下面的逻辑,当然即使是String,在这里通过BeanFactory也完全可以解析的(方法的上半段逻辑)
另外在这里,还会再次通过beanFactory根据指定类型去查找一次(注意MBeanExporter
中的allowEagerInit
这个属性默认是false的,如果与用户注册的对应的bean类型不一致,就没法注册到MBeanServer当中去了,如果要注册懒加载模式的Bean,那就需要将allowEagerInit
这个属性配置为true了)。现在就差最后一步org.springframework.jmx.export.MBeanExporter#registerBeanInstance了。
private ObjectName registerBeanInstance(Object bean, String beanKey) throws JMException {
a. 解析ObjectName
ObjectName objectName = getObjectName(bean, beanKey);
Object mbeanToExpose = null;
b. 判断是否为MBean或是MXBean
if (isMBean(bean.getClass())) {
mbeanToExpose = bean;
}
else {
c. 非MBean适配为DynamicMBean
DynamicMBean adaptedBean = adaptMBeanIfPossible(bean);
if (adaptedBean != null) {
mbeanToExpose = adaptedBean;
}
}
if (mbeanToExpose != null) {
if (logger.isDebugEnabled()) {
logger.debug("Located MBean '" + beanKey + "': registering with JMX server as MBean [" +
objectName + "]");
}
d. 适配成功 则进行注册
doRegister(mbeanToExpose, objectName);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Located managed bean '" + beanKey + "': registering with JMX server as MBean [" +
objectName + "]");
}
d. 以上适配不成功 则适配为ModelMBean
ModelMBean mbean = createAndConfigureMBean(bean, beanKey);
e. 注册MBean到Server
doRegister(mbean, objectName);
f. 注册后事件通知
injectNotificationPublisherIfNecessary(bean, mbean, objectName);
}
return objectName;
}
a. 获取ObjectName
这里应该有写熟悉了,首先是获取ObjectName对象,这是注册必须要的对象。在这里,Spring还是做了扩展,如果对应的bean没有实现SelfNaming接口,就会通过namingStrategy策略接口获取名称(策略模式的应用)
/** The strategy to use for creating ObjectNames for an object. */
private ObjectNamingStrategy namingStrategy = new KeyNamingStrategy();
protected ObjectName getObjectName(Object bean, @Nullable String beanKey) throws MalformedObjectNameException {
if (bean instanceof SelfNaming) {
return ((SelfNaming) bean).getObjectName();
}
else {
return this.namingStrategy.getObjectName(bean, beanKey);
}
}
b. 判断是否为MBean和MXBean
protected boolean isMBean(@Nullable Class<?> beanClass) {
return JmxUtils.isMBean(beanClass);
}
org.springframework.jmx.support.JmxUtils#isMBean
public static boolean isMBean(@Nullable Class<?> clazz) {
return (clazz != null &&
(DynamicMBean.class.isAssignableFrom(clazz) ||
(getMBeanInterface(clazz) != null || getMXBeanInterface(clazz) != null)));
}
Bean的类型是DynamicMBean类型或者属于标准的Mean或者MXBean.
org.springframework.jmx.support.JmxUtils#getMBeanInterface和org.springframework.jmx.support.JmxUtils#getMXBeanInterface大体的逻辑是相同的,这里只分析前者
public static Class<?> getMBeanInterface(@Nullable Class<?> clazz) {
if (clazz == null || clazz.getSuperclass() == null) {
return null;
}
String mbeanInterfaceName = clazz.getName() + MBEAN_SUFFIX;
Class<?>[] implementedInterfaces = clazz.getInterfaces();
for (Class<?> iface : implementedInterfaces) {
if (iface.getName().equals(mbeanInterfaceName)) {
return iface;
}
}
return getMBeanInterface(clazz.getSuperclass());
}
遍历自己和父类的接口,看是否存在名称为目标类名+MBean
,如果存在,则是属于MBean,否则则不是,这个其实就是MBean的一个标准,需要继承一个以MBean结尾的接口,比如我们当前的类名为com.example.spring.jmx.JmxTestBean
,如果它继承了com.example.spring.jmx.JmxTestBeanMean
这样一个接口,那么它就是标准
的MBean了。这里并不是,然后判断是否为MXBean,在这里的逻辑与上面差不多,也是遍历所有的接口(包括父类的)判断是否为公开类(当然可以配置)和是否包含MXBean
注解或是接口名称以MXBean
结尾。这里也不满足条件。
c. 适配为DynamicMBean
/**
* Build an adapted MBean for the given bean instance, if possible.
* <p>The default implementation builds a JMX 1.2 StandardMBean
* for the target's MBean/MXBean interface in case of an AOP proxy,
* delegating the interface's management operations to the proxy.
* @param bean the original bean instance
* @return the adapted MBean, or {@code null} if not possible
*/
@SuppressWarnings("unchecked")
@Nullable
protected DynamicMBean adaptMBeanIfPossible(Object bean) throws JMException {
Class<?> targetClass = AopUtils.getTargetClass(bean);
if (targetClass != bean.getClass()) {
Class<?> ifc = JmxUtils.getMXBeanInterface(targetClass);
if (ifc != null) {
if (!ifc.isInstance(bean)) {
throw new NotCompliantMBeanException("Managed bean [" + bean +
"] has a target class with an MXBean interface but does not expose it in the proxy");
}
return new StandardMBean(bean, ((Class<Object>) ifc), true);
}
else {
ifc = JmxUtils.getMBeanInterface(targetClass);
if (ifc != null) {
if (!ifc.isInstance(bean)) {
throw new NotCompliantMBeanException("Managed bean [" + bean +
"] has a target class with an MBean interface but does not expose it in the proxy");
}
return new StandardMBean(bean, ((Class<Object>) ifc));
}
}
}
return null;
}
首先查找到目标类类型(通过AopUtils工具类,在Spring当中bean可能被代理了(比如事务)),如果被代理了,就会适配为StandardMBean类型,这里并没有被代理,所以返回一个null,尝试适配失败。本来如果适配成功了,就可以直接注册了,这里在注册之前还需要继续处理
d. 适配为RequiredModelMBean
其实这里适配为SpringModelMBean类型(修改属性配置exposeManagedResourceClassLoader为false则直接适配为RequiredModelMBean类型),该类的继承关系如下所示
以下方法包括三个步骤
- 适配目标资源为ModelMBean类型
- 通过默认的MBeanInfoAssembler获取ModelMBeanInfo信息,主要还是属性和操作那一些信息,并设置到ModelMBean中
- 设置原资源对象到适配对象当中
/** Indicates whether Spring should expose the managed resource ClassLoader in the MBean. */
private boolean exposeManagedResourceClassLoader = true;
protected ModelMBean createAndConfigureMBean(Object managedResource, String beanKey)
throws MBeanExportException {
try {
// 适配为ModelMBean对象
ModelMBean mbean = createModelMBean();
// 通过默认的MBeanInfoAssembler获取ModelMBeanInfo信息
mbean.setModelMBeanInfo(getMBeanInfo(managedResource, beanKey));
mbean.setManagedResource(managedResource, MR_TYPE_OBJECT_REFERENCE);
return mbean;
}
catch (Throwable ex) {
throw new MBeanExportException("Could not create ModelMBean for managed resource [" +
managedResource + "] with key '" + beanKey + "'", ex);
}
}
/**
* Create an instance of a class that implements {@code ModelMBean}.
* <p>This method is called to obtain a {@code ModelMBean} instance to
* use when registering a bean. This method is called once per bean during the
* registration phase and must return a new instance of {@code ModelMBean}
* @return a new instance of a class that implements {@code ModelMBean}
* @throws javax.management.MBeanException if creation of the ModelMBean failed
*/
protected ModelMBean createModelMBean() throws MBeanException {
// 默认为true
return (this.exposeManagedResourceClassLoader ? new SpringModelMBean() : new RequiredModelMBean());
}
/** Stores the MBeanInfoAssembler to use for this exporter. */
private MBeanInfoAssembler assembler = new SimpleReflectiveMBeanInfoAssembler();
/**
* Gets the {@code ModelMBeanInfo} for the bean with the supplied key
* and of the supplied type.
*/
private ModelMBeanInfo getMBeanInfo(Object managedBean, String beanKey) throws JMException {
ModelMBeanInfo info = this.assembler.getMBeanInfo(managedBean, beanKey);
if (logger.isInfoEnabled() && ObjectUtils.isEmpty(info.getAttributes()) &&
ObjectUtils.isEmpty(info.getOperations())) {
logger.info("Bean with key '" + beanKey +
"' has been registered as an MBean but has no exposed attributes or operations");
}
return info;
}
e. 进行注册
完成以上步骤之后,只剩下注册到MBeanServer当中了。源码简化如下,除了注册到server当中,还会将actualObjectName添加到org.springframework.jmx.support.MBeanRegistrationSupport#registeredBeans集合属性当中。
/**
* The beans that have been registered by this exporter.
*/
private final Set<ObjectName> registeredBeans = new LinkedHashSet<>();
protected void doRegister(Object mbean, ObjectName objectName) throws JMException {
Assert.state(this.server != null, "No MBeanServer set");
ObjectName actualObjectName;
synchronized (this.registeredBeans) {
ObjectInstance registeredBean = null;
try {
registeredBean = this.server.registerMBean(mbean, objectName);
}
catch (InstanceAlreadyExistsException ex) {
// 省略各种异常处理
}
// Track registration and notify listeners.
actualObjectName = (registeredBean != null ? registeredBean.getObjectName() : null);
if (actualObjectName == null) {
actualObjectName = objectName;
}
this.registeredBeans.add(actualObjectName);
}
// 目前在org.springframework.jmx.support.MBeanRegistrationSupport这一块实现为空,但是在Spring中扩展
onRegister(actualObjectName, mbean);
}
f. 注册后事件通知
MBeanExporter类扩展了MBeanRegistrationSupport的onRegister方法用于事件通知(目前listeners这个数组为空)。
/** The MBeanExporterListeners registered with this exporter. */
@Nullable
private MBeanExporterListener[] listeners;
protected void onRegister(ObjectName objectName) {
notifyListenersOfRegistration(objectName);
}
/**
* Notifies all registered {@link MBeanExporterListener MBeanExporterListeners} of the
* registration of the MBean identified by the supplied {@link ObjectName}.
*/
private void notifyListenersOfRegistration(ObjectName objectName) {
if (this.listeners != null) {
for (MBeanExporterListener listener : this.listeners) {
listener.mbeanRegistered(objectName);
}
}
}
通过以上步骤,Spring完成了MBean的注册,额外还会有一些通知操作,但是默认情况下因为没有配置registeredNotificationListeners属性是没有的。此处暂时也不分析了。
总结
本章主要讲解了通过Spring配置MBean的方式,还分析了源码。Spring针对原生java做了一些封装,但是基本步骤没有变,首先需要创建一个MBeanServer(在MBeanExporter类型Bean初始化时完成的),然后创建ObjectName对象,注册MBean到MBeanServer当中(在所有非懒加载的Bean初始化完成之后进行的)。