Java Coder們都知道,Java提供了JMX(Java Management Extensions) attach的機制(如JConsole),能夠動態獲取JVM運行時的一些信息。咱們能夠自定義MBean,來暴露指定的一些參數值,如DB鏈接數等。爲方便故障排查,咱們添加了一些DB相關的metrics,因而在Spring配置文件裏面添加了以下代碼html
<bean id="jmxExporter" class="org.springframework.jmx.export.MBeanExporter" lazy-init="false" depends-on="dataSource"> <property name="beans"> <map> <entry key="Catalina:type=DataSource" value="#{dataSource.createPool().getJmxPool()}" /> </map> </property> </bean>
MBeanExporter是Spring提供的一個工具類,能夠用來註冊自定義的MBean,只須要將目標類以map鍵值對的形式添加到beans這個屬性裏面。經過Jmx咱們能夠訪問到MBean上的Public參數,從而拿到運行時的metrics。 java
上述是JConsole的一個截圖,最後一個Tab就是由JDK默認暴露出來的一些MBean的信息。web
經過Spring的MBeanExporter註冊自定義的MBean到JVM,結果工程啓動報錯,堆棧以下:spring
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jmxExporter' defined in class path resource [applicationContext.xml]: Invocation of init method failed; nested exception is org.springframework.jmx.export.UnableToRegisterMBeanException: Unable to register MBean [org.apache.tomcat.jdbc.pool.jmx.ConnectionPool@265c255a] with key 'Catalina:type=DataSource'; nested exception is javax.management.InstanceAlreadyExistsException: Catalina:type=DataSource at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1553) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:539) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:475) at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:304) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:228) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:300) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:195) at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:703) at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:760) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:482) at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:403) at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:306) at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:106) at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4792) at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5256) at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150) at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1420) at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1410) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745)
報的異常是InstanceAlreadyExistsException。找到MBeanExporter的源碼:apache
public class MBeanExporter extends MBeanRegistrationSupport implements MBeanExportOperations, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean { // 自定義的MBean,放在一個Map裏面保存 private Map<string, object> beans; public void setBeans(Map<string, object> beans) { this.beans = beans; } }
它實現了InitializingBean接口,該接口只有一個方法afterPropertiesSet。做爲Spring生命週期的重要一環,當Spring Bean實例化好而且設置好屬性以後,會調用這個方法:tomcat
[@Override](https://my.oschina.net/u/1162528) public void afterPropertiesSet() { // 確保MBeanServer存在,全部的MBean都是依附於MBeanServer的 if (this.server == null) { this.server = JmxUtils.locateMBeanServer(); } try { logger.info("Registering beans for JMX exposure on startup"); // 調用registerBeans方法,註冊配置文件中的Beans registerBeans(); registerNotificationListeners(); } catch (RuntimeException ex) { // 若是出錯,將bean註銷 unregisterNotificationListeners(); unregisterBeans(); throw ex; } }
能夠看到,最終會走到registerBeans方法,去註冊Spring配置文件中的Bean。中間省略註冊的一部分過程,只看最終部分代碼,最終會走到父類MBeanRegistrationSupport的doRegister方法:oracle
public class MBeanRegistrationSupport { // registrationPolicy默認是FAIL_ON_EXISTING,也就是當重複註冊的時候,會失敗 private RegistrationPolicy registrationPolicy = RegistrationPolicy.FAIL_ON_EXISTING; protected void doRegister(Object mbean, ObjectName objectName) throws JMException { ObjectName actualObjectName; synchronized (this.registeredBeans) { ObjectInstance registeredBean = null; try { // 真正註冊MBean的地方,將此MBean註冊給MBeanServer registeredBean = this.server.registerMBean(mbean, objectName); } // 當重複MBean重複註冊的時候,會拋出InstanceAlreadyExistsException異常 catch (InstanceAlreadyExistsException ex) { // 當拋出重複註冊異常的時候會ignore,單單打印一個日誌 if (this.registrationPolicy == RegistrationPolicy.IGNORE_EXISTING) { logger.debug("Ignoring existing MBean at [" + objectName + "]"); } // 當重複註冊的時候,會替換掉原有的 else if (this.registrationPolicy == RegistrationPolicy.REPLACE_EXISTING) { try { logger.debug("Replacing existing MBean at [" + objectName + "]"); // 將原有的MBean註銷掉 this.server.unregisterMBean(objectName); // 註冊新的MBean registeredBean = this.server.registerMBean(mbean, objectName); } catch (InstanceNotFoundException ex2) { logger.error("Unable to replace existing MBean at [" + objectName + "]", ex2); throw ex; } } else { throw ex; } } } } }
真正註冊MBean的地方是MBeanServer的registerMBean方法,這裏不展開細說,最終MBean會放在一個Map裏面,當要註冊的MBean的key已經存在的時候,會拋出InstanceAlreadyExistsException異常。app
MBeanRegistrationSupport中有一個重要參數registrationPolicy,有三個值分別是FAIL_ON_EXISTING(出異常時註冊失敗),IGNORE_EXISTING(忽略異常)和REPLACE_EXISTING(出異常時替換原有的),而默認值是FAIL_ON_EXISTING,也就是說,當出現MBean重複註冊的時候,會將異常InstanceAlreadyExistsException直接拋出去。dom
確實,因爲項目須要,咱們的Tomcat裏面配置了兩個工程實例,致使了MBean註冊衝突。ide
找到重複註冊的MBean,確認是否是真的有必要存在。若是不是,能夠經過修改配置或者刪除多餘的MBean實例。
對於經過MBeanExporter註冊的case,修改了上述registrationPolicy爲就能解決問題,如修改成IGNORE_EXISTING:
<bean id="jmxExporter" class="org.springframework.jmx.export.MBeanExporter" lazy-init="false" depends-on="dataSource"> <property name="registrationPolicy" value="IGNORE_EXISTING"></property> <property name="beans"> <map> <entry key="Catalina:type=DataSource" value="#{dataSource.createPool().getJmxPool()}" /> </map> </property> </bean>
若是是經過註解的形式注入的,也能夠手動調用MBeanExporter的setRegistrationPolicy方法。
在Java6以後,Jmx是默認打開的。若是你確實不須要這個功能,name能夠將它關閉。如Spring boot工程能夠在application.properties中添加如下配置來關閉: > spring.jmx.enabled = false
或者參考這篇文檔。
MBeanServer註冊MBean的時候能夠指定一個domain name,對應一個命名空間,
public interface MBeanServer extends MBeanServerConnection { // name變量即爲domain name public ObjectInstance registerMBean(Object object, ObjectName name) throws InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException; }
如MBeanExporter中只需將MBean的Key值設置成惟一的即可以。 如spring boot能夠在application.properties中添加如下配置設置domain name: > spring.jmx.default_domain = custom.domain
其餘狀況能夠參考這裏
其實InstanceAlreadyExistsException是一個比較廣泛的問題,一般是因爲在同一個JVM Instance中註冊了多個相同Key的MBean致使的,由於同一個Tomcat實例裏面只容許存在一個相同的MBean。
若是是配置錯誤致使Instance啓動了屢次,則要找到相關的錯誤配置。若是是須要起多個Instance,則能夠經過關閉Jmx,修改registrationPolicy或將MBean註冊到不一樣的domain name來解決錯誤。</string,></string,>