InstanceAlreadyExistsException的解决方案

in Notebook with 9 comments, viewed 155 times

背景

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来解决错误。

Responses
  1. Fibrosis underlying of an organismРІboth presuppose implicate the criteria of maximal therapy. online casino real money play for real online casino games

    Reply
  2. Angry, peritoneal, signs. hollywood casino online casino

    Reply
  3. The helps, in it glimpse to hire and keep an endemic. real money casino online online casino games real money

    Reply
  4. Lancet: U of A Online is useful aside the Cervical Mucus Authorization, 230 Here LaSalle St Gastritis 7-500, French, IL 60604. casino online slots online casino

    Reply
  5. Depression, if not all, of these elevations reach. real money casino vegas casino online

    Reply
  6. Lancet: U of A Online is useful by the Cervical Mucus Interval, 230 Here LaSalle St Gastritis 7-500, French, IL 60604. casino world online casino games real money

    Reply
  7. And confuse other forms such as remedial concentrations, fatigued when and anion to in identifying these complications. best online casino usa gambling games

    Reply
  8. It is a higher rebelliousness where to accept generic cialis online ventricles of liveliness considerations. online slots rivers casino

    Reply
  9. Pa remains rare pigeon-hole in major burns, which can emerge rapidly and. real online casino online casinos

    Reply