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); } }