解决ActiveMQ的“Invalid broker URI”异常的历程

000

最近碰到一个问题,把解决的过程记录下来。

故障原因

同事的应用上线,Tomcat无法正常启动。抛出这样的异常:

org.springframework.beans.PropertyBatchUpdateException; nested PropertyAccessExceptions (1) are:|PropertyAccessException 1: org.springframework.beans.MethodInvocationException: Property 'brokerURL' threw exception; nested exception is java.lang.IllegalArgumentException: Invalid broker URI: nio://10.0.0.0:91616 
        at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:121)
        at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:75)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1504)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1216)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:538)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:476)
        at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:302)
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:229)
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:298)
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:193)

据同事说,在线下环境,可以正常运行,在线上环境就出错了,非常的诡异。

程序有一个env.properties的配置文件:

broker.producer  = failover://(nio://10.0.0.3:91615?connectionTimeout=3000,nio://10.0.0.4:91615?connectionTimeout=3000)

线上和线下加载的是不同的env.properties。

在Spring xml配置文件里有这样的配置:

	<!-- ActiveMQ 连接工厂 -->
	<amq:connectionFactory id="jmsConnectionFactory" brokerURL="${broker.producer}" />

Spring在运行时,会自动替换${}表达式里的值。

首先检查网络的连通性:

在线上机器上,执行

telnet 10.0.0.3 91616

可以正常连接,再到ActiveMQ的Web Console上查看Connection,发现的确没有这个IP的连接。

检查配置是否正常:

线上的配置的brokerURI是这个:

failover://(nio://10.0.0.3:91616?connectionTimeout=3000,nio://10.0.0.4:91616?connectionTimeout=3000)

直接先改为最简单的,在vim下yy复制了一行,删除多余的,剩下:

nio://10.0.0.3:91616

发现还是报异常。

利用ssh做端口转发,本地测试

在本地跑了个简单程序,用XShell的端口转发,把本地请求转发到线上机器上,发现可以正常发送消息。于是让同事回去检查代码里的其它问题了。

把测试程序放到线上机器运行

但是同事没有找到错误,于是把刚才的简单的程序打成一个fat jar包,放到线上机器上去跑,发现可以正常发送ActiveMQ消息。

程序打包用的是maven的one jar 插件,参考:

http://www.mkyong.com/maven/maven-create-a-fat-jar-file-one-jar-example/

把测试程序集成到同事的代码里,运行

把测试程序放到同事的War包的代码里,放到线上机器,发现可以正常发送消息。

但是同事配置的ActiveMQ还是不能发送消息,还是报“Invalid broker URI”异常。

增加变量,反复测试,对比配置

没办法了,把同事的环境变量${broker.producer},设置到测试代码里,发现测试代码抛异常了。

于是确认是${broker.producer} 这个变量有问题。

但是env.properties文件里的配置看起来是对的。于是怀疑是配置文件格式有问题。

备份旧文件,建了个新配置文件,配置上

broker.producer =nio://10.0.0.3:91615

发现,居然正常了。

于是对比两个配置文件,发现旧的配置文件上,最后多了一个空格。。就是91615后面多了一个空格。

蛋疼无比,ActiveMQ居然不能识别处理配置值后面多出来的一个空格。而且Spring抛出来的异常里也没有这个信息。

搜索关键字”Invalid broker URI”,查看ActiveMQ代码,找到原始异常

开始调试时,找不到原始的异常信息在哪里,Spring的函数调用层次太多了。于是采用代码搜索。

在 https://searchcode.com 搜索”Invalid broker URI”,终于找到原始的异常信息是下面的代码抛出来的:

URI org.apache.activemq.ActiveMQConnectionFactory.createURI(String brokerURL)
    private static URI createURI(String brokerURL) {
        try {
            return new URI(brokerURL);
        } catch (URISyntaxException e) {
            throw (IllegalArgumentException)new IllegalArgumentException("Invalid broker URI: " + brokerURL).initCause(e);
        }
    }

很奇怪的是,IllegalArgumentException异常里是正确地把URISyntaxException设置到cause里了,后面的Spring却没有把这个信息给打印出来。。

Spring打印异常的工作原理

为什么Spring能把前面的异常信息都打印出来,而原始的异常信息却不能打印出来?比如某个Spring异常信息是这样的:

Caused by: org.springframework.beans.PropertyBatchUpdateException; nested PropertyAccessExceptions (1) are:

Srping会用Caused by,nested exception is,这样的字符把所有的异常都串起来。到底这里面是怎么工作的?

再次搜索”nested exception is”

查找到Spring相关的代码。

原来所有的Spring异常类都继承自NestedRuntimeException,而NestedRuntimeException重写了getMessage()函数,在getMessage()函数里,会把异常的信息全都串起来。

而IllegalArgumentException继承自Throwable类,Throwable类的getMessage()函数只是简单打印了message,并没有把cause也输出。

org.springframework.core.NestedRuntimeException

	/**
	 * Return the detail message, including the message from the nested exception
	 * if there is one.
	 */
	@Override
	public String getMessage() {
		return NestedExceptionUtils.buildMessage(super.getMessage(), getCause());
	}


org.springframework.core.NestedExceptionUtils
	/**
	 * Build a message for the given base message and root cause.
	 * @param message the base message
	 * @param cause the root cause
	 * @return the full exception message
	 */
	public static String buildMessage(String message, Throwable cause) {
		if (cause != null) {
			StringBuilder sb = new StringBuilder();
			if (message != null) {
				sb.append(message).append("; ");
			}
			sb.append("nested exception is ").append(cause);
			return sb.toString();
		}
		else {
			return message;
		}
	}

其它的一些东东:

NestedExceptionUtils这个类是abstract,这样可以防止使用者得到实例,这样使用者不能用错。这个也是一个常见的util类的技巧了。

public abstract class NestedExceptionUtils {

总结:

这个代码搜索网站比较好用:https://searchcode.com

很多流行网站的数据都有,比github上要全。

后来在网上搜索了下“Invalid broker URI”,有10万多条结果。。估计有不少就是因为一些空格而造成的。。

ActiveMQ的开发者只需要加上一点点的trim()的判断处理代码,就可以减少很多人的痛苦了。

所以防御性编程还是有必要的,有的时候并不真的是使用者不会用,而是错误来自想像不到的地方。

URI org.apache.activemq.ActiveMQConnectionFactory.createURI(String brokerURL)
    private static URI createURI(String brokerURL) {
        try {
            if(brokerURL != null)
                brokerURL = brokerURL.trim();
            return new URI(brokerURL);
        } catch (URISyntaxException e) {
            throw (IllegalArgumentException)new IllegalArgumentException("Invalid broker URI: " + brokerURL).initCause(e);
        }
    }

点赞