背景
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。
上述是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。中間省略註冊的一部分過程,只看最終部分代碼,最終會走到父類MBeanRegistrationSupport的doRegister方法:
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異常。
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>
如果是通過註解的形式注入的,也可以手動調用MBeanExporter的setRegistrationPolicy方法。
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來解決錯誤。