InstanceAlreadyExistsException的解決方案

背景

JMX

Java Coder們都知道,Java提供了JMX(Java Management Extensions) attach的機制(如JConsole),可以動態獲取JVM運行時的一些信息。我們可以自定義MBean,來暴露指定的一些參數值,如DB連接數等。爲方便故障排查,我們添加了一些DB相關的metrics,於是在Spring配置文件裏面添加了如下代碼

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

上述是JConsole的一個截圖,最後一個Tab就是由JDK默認暴露出來的一些MBean的信息。

問題描述

通過Spring的MBeanExporter註冊自定義的MBean到JVM,結果工程啓動報錯,堆棧如下:

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的源碼:

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實例化好並且設置好屬性之後,會調用這個方法:

@Override
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方法:

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異常。

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

確實,由於項目需要,我們的Tomcat裏面配置了兩個工程實例,導致了MBean註冊衝突。

問題解決

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來解決錯誤。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章