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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容