InstanceAlreadyExistsException的解決方案

背景

JMX

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。 MBeanjava

上述是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。中間省略註冊的一部分過程,只看最終部分代碼,最終會走到父類MBeanRegistrationSupportdoRegister方法: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的地方是MBeanServerregisterMBean方法,這裏不展開細說,最終MBean會放在一個Map裏面,當要註冊的MBean的key已經存在的時候,會拋出InstanceAlreadyExistsException異常。app

MBeanRegistrationSupport中有一個重要參數registrationPolicy,有三個值分別是FAIL_ON_EXISTING(出異常時註冊失敗),IGNORE_EXISTING(忽略異常)和REPLACE_EXISTING(出異常時替換原有的),而默認值是FAIL_ON_EXISTING,也就是說,當出現MBean重複註冊的時候,會將異常InstanceAlreadyExistsException直接拋出去。dom

確實,因爲項目須要,咱們的Tomcat裏面配置了兩個工程實例,致使了MBean註冊衝突。ide

問題解決

1. 確認重複註冊的MBean

找到重複註冊的MBean,確認是否是真的有必要存在。若是不是,能夠經過修改配置或者刪除多餘的MBean實例。

2. 修改registrationPolicy

對於經過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>

若是是經過註解的形式注入的,也能夠手動調用MBeanExportersetRegistrationPolicy方法。

3. 關閉Jmx功能

在Java6以後,Jmx是默認打開的。若是你確實不須要這個功能,name能夠將它關閉。如Spring boot工程能夠在application.properties中添加如下配置來關閉: > spring.jmx.enabled = false

或者參考這篇文檔。

4. 將MBean註冊到不一樣的domain name

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,>

相關文章
相關標籤/搜索