springboot情操陶冶-jmx解析


承接前文springboot情操陶冶-@Configuration注解解析,近期筆者接觸的項目中有使用到了jmx的協議框架,遂在前文的基礎上講解下springboot中是如何整合jmx的

知識儲備

JMX:Java Management Extension(Java管理應用擴展),這種機制可以方便的管理、監控正在運行的Java程序。常用於監控管理線程、內存、日志Level、服務重啟、系統環境等等。
更多的知識點參考此篇文獻:https://blog.csdn.net/u013256816/article/details/52800742。筆者此處引用其中的框架圖方便理解
jmx_frame

JmxAutoConfiguration

springboot通過在META-INF\spring.factories文件指定EnableAutoConfiguration屬性值為JmxAutoConfiguration,便基本搭建了jmx的框架模子。聽起來挺神奇的,筆者這就分析源碼來一窺究竟

注解

首先看下JmxAutoConfiguration頭上的注解

@Configuration
@ConditionalOnClass({ MBeanExporter.class })
@ConditionalOnProperty(prefix = "spring.jmx", name = "enabled", havingValue = "true", matchIfMissing = true)
public class JmxAutoConfiguration implements EnvironmentAware, BeanFactoryAware {
}

由上可知,要想使jmx環境生效,前提為

  • classpath環境得存在org.springframework.jmx.export.MBeanExporter

  • 環境變量spring.jmx.enabled設置為true,默認為true

一般引入springboot上述條件均是滿足的,只是用戶可通過spring.jmx.enabled屬性來開關啟jmx環境

@Bean方法

其下有三個方法,分別被@Bean@Conditional注解所修飾。筆者依次來進行解讀


JmxAutoConfiguration#objectNamingStrategy()-獲取ObjectName的生成策略

	@Bean
	@ConditionalOnMissingBean(value = ObjectNamingStrategy.class, search = SearchStrategy.CURRENT)
	public ParentAwareNamingStrategy objectNamingStrategy() {
		// create namingStrategy
		ParentAwareNamingStrategy namingStrategy = new ParentAwareNamingStrategy(
				new AnnotationJmxAttributeSource());
		// have a try to read environment property 'spring.jmx.default-domain'
		String defaultDomain = this.environment.getProperty("spring.jmx.default-domain");
		if (StringUtils.hasLength(defaultDomain)) {
			namingStrategy.setDefaultDomain(defaultDomain);
		}
		return namingStrategy;
	}

上述代碼也很簡單,其中環境變量spring.jmx.default-domain代表jmx默認的域掛載。

  • 如果@ManagedResource沒有指定objectName屬性或者beanName不符合jmx語法,則默認選取當前類的包名作為objectName

JmxAutoConfiguration#mbeanServer()-創建MBeanServer

	@Bean
	@ConditionalOnMissingBean
	public MBeanServer mbeanServer() {
		// 1.first to search classpath exsit 'weblogic.management.Helper'/'com.ibm.websphere.management.AdminServiceFactory' class if or not  
		SpecificPlatform platform = SpecificPlatform.get();
		if (platform != null) {
			return platform.getMBeanServer();
		}
		// 2.via MBeanServerFactoryBean to create MBeanServer
		MBeanServerFactoryBean factory = new MBeanServerFactoryBean();
		factory.setLocateExistingServerIfPossible(true);
		factory.afterPropertiesSet();
		return factory.getObject();
	}

筆者此處只關注MBeanServerFactoryBean是如何創建mbeanserver的,直接去看下其實現的afterPropertiesSet()方法

	@Override
	public void afterPropertiesSet() throws MBeanServerNotFoundException {
		// 1.嘗試去找尋已存在的mbeanserver
		if (this.locateExistingServerIfPossible || this.agentId != null) {
			try {
				this.server = locateMBeanServer(this.agentId);
			}
			catch (MBeanServerNotFoundException ex) {

				if (this.agentId != null) {
					throw ex;
				}
				logger.info("No existing MBeanServer found - creating new one");
			}
		}

		// 2.如果上述不存在mbeanserver,則調用jmx api生成mbeanserver
		if (this.server == null) {
			this.server = createMBeanServer(this.defaultDomain, this.registerWithFactory);
			this.newlyRegistered = this.registerWithFactory;
		}
	}

主要調用jmx api的MBeanServerFactory.createMBeanServer()方法創建mbeanserver,具體的創建過程筆者就不深究了,感興趣的讀者可自行分析


JmxAutoConfiguration#mbeanExporter()-創建mbeanExporter
源碼如下

	@Bean
	@Primary
	@ConditionalOnMissingBean(value = MBeanExporter.class, search = SearchStrategy.CURRENT)
	public AnnotationMBeanExporter mbeanExporter(ObjectNamingStrategy namingStrategy) {
		// 1.創建注解類型的AnnotationMBeanExporter,表明采取注解方式加載mbean
		AnnotationMBeanExporter exporter = new AnnotationMBeanExporter();
		exporter.setRegistrationPolicy(RegistrationPolicy.FAIL_ON_EXISTING);
		// 2.set above namingStrategy
		exporter.setNamingStrategy(namingStrategy);
		// 3.set mbeanserver via spring applicationContext
		String serverBean = this.environment.getProperty("spring.jmx.server",
				"mbeanServer");
		if (StringUtils.hasLength(serverBean)) {
			exporter.setServer(this.beanFactory.getBean(serverBean, MBeanServer.class));
		}
		return exporter;
	}

創建AnnotationMBeanExporter類來讀取注解方式的mbean,並優先從spring上下文讀取mbeanserver。

  • 環境變量spring.jmx.server如果沒有指定的話則默認讀取beanName為'mbeanServer'的MBeanServer對象,這與JmxAutoConfiguration#mbeanServer()方法注冊的bean不謀而合

通過上述的分析可得,筆者發現最終暴露給外界調用jmx協議是通過AnnotationMBeanExporter來完成的,其里面也蘊含了解析mbean相關注解的玄機

AnnotationMBeanExporter

其實現的常用接口有InitializingBean/SmartInitializingSingleton/DisposableBean以及MBeanExportOperations

構造函數

	public AnnotationMBeanExporter() {
		setNamingStrategy(this.metadataNamingStrategy);
		setAssembler(this.metadataAssembler);
		setAutodetectMode(AUTODETECT_ALL);
	}

主要是設置基礎的屬性

afterPropertiesSet()

InitializingBean接口實現類如下

	@Override
	public void afterPropertiesSet() {
		// have a try to find exsiting mbeanserver
		if (this.server == null) {
			this.server = JmxUtils.locateMBeanServer();
		}
	}

afterSingletonsInstantiated()

SmartInitializingSingleton接口實現類如下

	@Override
	public void afterSingletonsInstantiated() {
		try {
			logger.info("Registering beans for JMX exposure on startup");
			registerBeans();
			registerNotificationListeners();
		}
		catch (RuntimeException ex) {
			// Unregister beans already registered by this exporter.
			unregisterNotificationListeners();
			unregisterBeans();
			throw ex;
		}
	}

此處的registerBeans()方法便是mbeanserver去注冊mbean的過程,可以繼續跟蹤下

	protected void registerBeans() {
		// The beans property may be null, for example if we are relying solely on autodetection.
		if (this.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");
			}
			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);
			}
		}

		// mbeanserver register mbeans
		if (!this.beans.isEmpty()) {
			this.beans.forEach((beanName, instance) -> registerBeanNameOrInstance(instance, beanName));
		}
	}

避免代碼過長帶來的視覺疲勞,筆者此處對關鍵方法作下總結

  1. autodetect()方法的作用是遍歷bean工廠上的所有beanDefinition,找尋符合條件的beans作為后續的mbeans注冊。找尋條件歸結如下

    • 攜帶@MBean注解的類
    • DynamicBean接口實現類
    • *MBean接口的實現類
    • 攜帶@ManagedResource注解的類
  2. registerBeanNameOrInstance()方法則會對符合條件的beans進行mbean的注冊操作,操作步驟如下
    1). 根據類上的@ManagedResource注解的屬性objectName生成ObjectName對象
    2). 如果符合條件的mbean是攜帶@ManagedResource注解的,則生成ModelBean對象並讀取@ManagedOperation@ManagedAttribute等jmx注解信息
    3). 最后注冊上述的mbean到mbeanserver上

通過上述的操作便可以將搜索到的mbean注冊至mbeanserver上了,只要用戶使用@ManagedOperation@ManagedAttribute@ManagedResource注解搭配即可

附例


pom內容

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>demo-springboot</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

mbeans創建

package com.example.demo.jmx;

import com.google.gson.Gson;
import org.springframework.context.annotation.Configuration;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;

import java.util.HashMap;
import java.util.Map;

/**
 * system common monitor
 *
 * @author nanco
 * @create 2018/8/8
 **/
@Configuration
@ManagedResource(objectName = "monitor:name=SystemCommonMonitor")
public class SystemCommonMonitorMBean {

    private String systemName;

    private Gson gsonTool = new Gson();


    @ManagedAttribute
    public String getSystemName() {
        return this.systemName;
    }

    @ManagedAttribute(description = "system_name", defaultValue = "demo")
    public void setSystemName(String name) {
        this.systemName = name;
    }

    @ManagedOperation(description = "systemInfo")
    public String systemInfo() {
        Map<String, String> system = new HashMap(8);
        system.put("cpuCoreSize", "4");
        system.put("memorySize", "8G");
        system.put("cpuRatio", "20%");
        system.put("memoryRatio", "2%");
        system.put("totalDisk", "200G");
        system.put("usedDisk", "120G");
        system.put("freeDisk", "80G");


        return gsonTool.toJson(system);
    }
}


jmx serviceUrl暴露

package com.example.demo.jmx;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.jmx.support.ConnectorServerFactoryBean;
import org.springframework.remoting.rmi.RmiRegistryFactoryBean;

/**
 * @author nanco
 * @create 2018/8/8
 **/
@Configuration
public class JmxAutoConfiguration {

    @Value("${jmx.rmi.host:localhost}")
    private String rmiHost;

    @Value("${jmx.rmi.port:7099}")
    private int rmiPort;

    @Value("${jmx.service.domain:jmxrmi}")
    private String jmxDomain;

	// 指定特定端口可以開放命名服務
    @Bean
    public RmiRegistryFactoryBean rmiRegistry() {
        RmiRegistryFactoryBean factoryBean = new RmiRegistryFactoryBean();
        factoryBean.setPort(rmiPort);
        factoryBean.setAlwaysCreate(true);

        return factoryBean;
    }

    @DependsOn("rmiRegistry")
    @Bean
    public ConnectorServerFactoryBean jmxConnector() {
        ConnectorServerFactoryBean serverFactoryBean = new ConnectorServerFactoryBean();

        serverFactoryBean.setServiceUrl(String.format("service:jmx:rmi://%s:%s/jndi/rmi://%s:%s/%s", rmiHost, rmiPort, rmiHost, rmiPort, jmxDomain));

        return serverFactoryBean;
    }
}


jconsole訪問,直接遠程連接至service:jmx:rmi://localhost:7099/jndi/rmi://localhost:7099/jmxrmi即可(默認)
jconsole_1
jconsole_2
jconsole_3
jconsole_4
jconsole_5

結束語

讀者在閱讀本博文的時候,建議首先按照筆者上述給出的文獻鏈接查閱jmx相關知識點,再結合此文便會對springboot整合jmx框架有一定的了解


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM