我们现在做的Web项目遇到一个问题,项目启动时使用Failover方式连接某个ActiveMQ,brokerUrl类似
failOver:(tcp://xxx.xxx.xxx.xxx:61616),如果ActiveMQ启动正常,项目启动正常;
但如果ActiveMQ没有启动,项目将无法启动完成,一直卡在连接ActiveMQ阶段。
经过调查,发现是ActiveMQ提供的FailOver Transport方式导致的。
FailOver Transport有maxReconnectAttempts和startupMaxReconnectAttempts两个连接参数。
前者是FailOver Transport最大的重连次数,后者是启动阶段的最大重连次数,由于我们原先的连接这两个参数没有设置值,它们都沿用默认值-1,表示无限次重连,直到连接成功为止。这样,只要我们在启动项目时不启动ActiveMQ,FailOver Transport将一直尝试重连,导致后续程序加载无法进行,从而项目启动卡住。
找到了原因之后,我们在brokerUrl中设置startupMaxReconnectAttempts=2,形如:
failOver:(tcp://xxx.xxx.xxx.xxx:61616)?startupMaxReconnectAttempts=2,这样启动时FailOverTransport仅仅重试2次,如果不成功,就不再尝试,继续加载后续程序,这样就解决了项目启动卡住的问题。
然而还有一个问题没有解决,如果启动时没有连接ActiveMQ成功,在项目启动完成后,再启动ActiveMQ时,即使ActiveMQConnectionFactory一直重连ActiveMQ,直到连接成功。我们配置的DefaultMessageListenerContainer仍然会抛出以下异常:
ERROR DefaultMessageListenerContainer - Could not refresh JMS Connection for destination 'queue://xxxxxxx' - retrying in 5000 ms. Cause: The JMS connection has failed: Connection refused: connect.
在StackOverflow上有人给出了解答,是因为DefaultMessageListenerContainer引用的
SingleConnectionFactory对象的reconnectOnException属性。
(http://stackoverflow.com/questions/17729066/spring-mq-jms-reconnect-configuration)
这个属性的说明如下(基于Spring Framework 4.3.2 RELEASE版本)
Specify whether the single Connection should be reset (to be subsequently renewed) when a JMSException is reported by the underlying Connection.
这个属性指定是否在底层的连接(这里是指ActiveMQ连接)抛出JMSException的时候,对连接进行重置。
它的默认值是false,意味着即使JMS异常抛出,SingleConnectionFactory自带的connection没有重置。
在调用getConnection()方法时,由于连接没有重置,this.connection != null,仍然返回旧的connection.
从而DefaultMessageListenerContainer引用的连接仍然失败连接,不断抛出异常
protected Connection getConnection() throws JMSException {
synchronized (this.connectionMonitor) {
if (this.connection == null) {
initConnection();
}
return this.connection;
}
}
当我们设置这个属性为true后,在抛出Jms异常时,SingleConnectionFactory对象绑定的ExceptionListner代理对象会回调SingleConnectionFactory对象的onException方法,将connection属性设置为null,这样在getConnection()方法时,会调用initConnection方法创建新的连接。这样就能保证ActiveMQ启动成功后,会有对应的成功连接被创建,从而使DefaultMessageListenerContainer引用到正常连接,不再抛出异常。
public class SingleConnectionFactory ......
{
@Override
public void onException(JMSException ex) {
logger.warn("Encountered a JMSException - resetting the underlying JMS Connection", ex);
resetConnection();
}
public void resetConnection() {
synchronized (this.connectionMonitor) {
if (this.connection != null) {
closeConnection(this.connection);
}
this.connection = null;
}
}
protected Connection getConnection() throws JMSException {
synchronized (this.connectionMonitor) {
if (this.connection == null) {
initConnection();
}
return this.connection;
}
}
public void initConnection() throws JMSException {
if (getTargetConnectionFactory() == null) {
throw new IllegalStateException(
"'targetConnectionFactory' is required for lazily initializing a Connection");
}
synchronized (this.connectionMonitor) {
if (this.connection != null) {
closeConnection(this.connection);
}
this.connection = doCreateConnection();
prepareConnection(this.connection);
if (this.startedCount > 0) {
this.connection.start();
}
if (logger.isInfoEnabled()) {
logger.info("Established shared JMS Connection: " + this.connection);
}
}
}
}
private class AggregatedExceptionListener implements ExceptionListener {
final Set<ExceptionListener> delegates = new LinkedHashSet<ExceptionListener>(2);
@Override
public void onException(JMSException ex) {
synchronized (connectionMonitor) {
// Iterate over temporary copy in order to avoid ConcurrentModificationException,
// since listener invocations may in turn trigger registration of listeners...
for (ExceptionListener listener : new LinkedHashSet<ExceptionListener>(this.delegates)) {
listener.onException(ex);
}
}
}
}
上述代码基于Spring Framework 4.3.3.RELEASE版本
具体的配置信息如下:
<bean id="connectionFactory" class="org.springframework.jms.connection.SingleConnectionFactory">
...................
<property name="reconnectOnException" value="true"/>
</bean>
上面的修改方案虽然可以解决ActiveMQ服务器未启动时应用启动卡住的问题,但是每次重置JMS连接时都会输出异常信息,这是因为重置连接时会将抛出的异常信息输出到日志。
package org.springframework.jms.connection;
public class SingleConnectionFactory implements ConnectionFactory, QueueConnectionFactory,
TopicConnectionFactory, ExceptionListener, InitializingBean, DisposableBean {
.............
public void onException(JMSException ex) {
logger.warn("Encountered a JMSException - resetting the underlying JMS Connection", ex);
resetConnection();
}
.............
}
如果不想查看重置连接时抛出的异常信息,可以在日志配置文件中设置日志输出级别为ERROR(以logback为例)
<logger name="org.springframework.jms.connection" level="ERROR" />
此外在ActiveMQ没有启动的情况下,如果在项目中为ActiveMQ消息队列配置了DefaultMessageListenerContainer对象,它将会持续刷新JMS连接,输出异常信息,默认的刷新间隔是由DefaultMessageListenerContainer对象的backOff属性对象的interval属性确定的
public class DefaultMessageListenerContainer extends AbstractPollingMessageListenerContainer {
...........
/**
* The default recovery interval: 5000 ms = 5 seconds.
*/
public static final long DEFAULT_RECOVERY_INTERVAL = 5000;
private BackOff backOff = new FixedBackOff(DEFAULT_RECOVERY_INTERVAL, Long.MAX_VALUE);
public void setRecoveryInterval(long recoveryInterval) {
this.backOff = new FixedBackOff(recoveryInterval, Long.MAX_VALUE);
}
...........
}
public class FixedBackOff implements BackOff {
private long interval = 5000L;
........
public FixedBackOff(long interval, long maxAttempts) {
this.interval = interval;
this.maxAttempts = maxAttempts;
}
........
}
如果在定义DefaultMessageListenerContainer对象时没有设定recoveryInterval,那它将使用默认的5000ms初始化backoff对象。实际运行时DefaultMessageListenerContainer对象将使用5000ms(5s)这个时间间隔刷新JMS连接,输出连接异常信息。如果定义的DefaultMessageListenerContainer对象数目很多,日志中的连接异常信息量将会很大,我们需要调整这个时间间隔。
可以在定义DefaultMessageListenerContainer对象时设置recoveryInterval(下面的例子将recoveryInterval设置为30000ms,大家可以根据自己的需求进行调整。)
<bean id="xxxContainer"
class="org.springframework.jms.listener.DefaultMessageListenerContainer">
..................
<property name="recoveryInterval" value="30000" />
</bean>