起底SpringAmqp
1.背景
Spring-amqp是Spring为了方便开发RabbitMQ程序而做的一个第三方类库。Amqp全名Advanced Message Queuing Protocol,即高级消息队列协议。旨在提供一种跨平台跨语言的消息服务,AMQP是一种应用层协议,不同于JMS(API接口),目前实现AMQP的开源项目包括RabbitMQ, Aman Gupta等,RabbitMQ是应用最广泛的AMQP的实现,使用erlang开发使其具有了较高的读写性能。RabbitMQ官方提供了Java,.NET,erlang版本的客户端,提供了基本的消息操作实现。
2.相关概念
AMQP中的基本概念:
Queue:队列,可以看做消息发送的目的地,接受消息的必备参数。若无consumer接受,将会一直存在服务器内存中。
Exchange:交换机,发送消息必须指定的参数,一般需要和routingkey组合路由到指定的队列中去。
ExchangeType:交换机类型,有四种:direct(严格按照key寻找queue),fanout(不需要key只要是绑定到该交换机的queue都能收到消息),topic(正则匹配key寻找queue),system(不常用),自定义(不常用)
RoutingKey:路由键,配合Exchange确定发送消息的目标队列,在Fanout类型的Exchange中忽略此参数。
Binding:绑定关系,将交换机与队列绑定,依据交换机类型确定不同参数。
Virtual hosts:虚拟机,感觉像是命名空间的概念,为了区分不同类别的资源
关系如下图所示:
3.Srping的封装
Spring对RabbitMQ的封装分成两个部分,一部分专门做接口,对应AMQP,项目名称也是spring-amqp,另一部分针对RabbitMQ做了具体的实现,项目名称是spring-rabbit。这一点也是我们要在项目规划中要注意和学习的,把变的部分(具体实现)和不变的部分(接口、协议、规范)区分开来,分别发展,提高隔离性。
Spring的封装增强了以下几点能力:
1) channel做了缓存
这部分主要依赖CachingConnectionFactory实现。其中的private Channel getChannel(boolean transactional)方法,主要是首先判断是否是事务的,这里Spring把事务的和非事务的channel分别存在不同的LinkedList中,然后判断是否有缓存的channel,有则取之,无则创建新的。
Spring在创建的时候运用了代理,生成一个Channel的代理,然后在这个代理中规范了几个方法执行时的特殊处理方法,这一点非常赞的,在不影响原来的标准Channel类的情况下,接管了其部分方法,具体如何实现缓存的呢?仔细看源码,发现在执行close方法时,会判断一下当前缓存的个数是否超出设置的最大值,如果没有,则执行一个logicalClose方法,这个方法就是实现缓存的关键了。源码摘录如下:
获取channel方法:
private Channel getChannel(boolean transactional) { LinkedList<ChannelProxy> channelList = transactional ? this.cachedChannelsTransactional : this.cachedChannelsNonTransactional; Channel channel = null; synchronized (channelList) { if (!channelList.isEmpty()) { channel = channelList.removeFirst(); } } if (channel != null) { if (logger.isTraceEnabled()) { logger.trace("Found cached Rabbit Channel"); } } else { channel = getCachedChannelProxy(channelList, transactional);//发现没有缓存,直接新建一个channel } return channel; }
private ChannelProxy getCachedChannelProxy(LinkedList<ChannelProxy> channelList, boolean transactional) { Channel targetChannel = createBareChannel(transactional); if (logger.isDebugEnabled()) { logger.debug("Creating cached Rabbit Channel from " + targetChannel); } getChannelListener().onCreate(targetChannel, transactional); return (ChannelProxy) Proxy.newProxyInstance(ChannelProxy.class.getClassLoader(), new Class[] { ChannelProxy.class }, new CachedChannelInvocationHandler(targetChannel, channelList, transactional));//精彩之笔,代理扩展channel行为 }
代理中的handle方法:
if (methodName.equals("close")) { // Handle close method: don't pass the call on. if (active) { synchronized (this.channelList) { if (this.channelList.size() < getChannelCacheSize()) { logicalClose((ChannelProxy) proxy);//逻辑上关闭channel,实际上可以理解为归还channel // Remain open in the channel list. return null; } } } private void logicalClose(ChannelProxy proxy) throws Exception { if (this.target != null && !this.target.isOpen()) { synchronized (targetMonitor) { if (this.target != null && !this.target.isOpen()) { this.target = null; return; } } } // Allow for multiple close calls... if (!this.channelList.contains(proxy)) { if (logger.isTraceEnabled()) { logger.trace("Returning cached Channel: " + this.target); } this.channelList.addLast(proxy);//归还Channel } }
总结:Java的代理用对地方将会提高Java的动态性,使程序的扩展性大大提高,缓存可以有效解决创建耗时耗资源对象的性能问题。
2) 可以添加Connection和Channel的监听器
AbstractConnectionFactory中加入了如下方法:
public void setConnectionListeners(List<? extends ConnectionListener> listeners) {
this.connectionListener.setDelegates(listeners);
}
public void addConnectionListener(ConnectionListener listener) {
this.connectionListener.addDelegate(listener);
}
public void setChannelListeners(List<? extends ChannelListener> listeners) {
this.channelListener.setDelegates(listeners);
}
public void addChannelListener(ChannelListener listener) {
this.channelListener.addDelegate(listener);
}
分别可以设置Connection和Channel的打开、关闭、创建的监听器,通过设置监听器,可以在适当的时机加入一些自己想做的事情。
3) 自动接收消息,接收消息模仿了JMS的事件的方式
Spring提供了MessageListener接口来定义了收到消息执行的方法onMessage,这点非常像JMS,可能是为了讨好JMS程序员吧^-^。我本人非常喜欢这种事件驱动的设计方式,感觉非常符合事物发展的规律,凡是有因果。下面来看具体的执行原理,最终实现类SimpleMessageListenerContainer,抽象类AbstractMessageListenerContainer,实现了SmartLifecycle接口,而SmartLifecycle继承了Spring标准的Lifecycle接口,即Spring容器启动的时候会自动执行SimpleMessageListenerContainer的start方法,类间的关系如图:
下面看SimpleMessageListenerContainer中的start方法,执行doStart方法:
protected void doStart() throws Exception { super.doStart(); synchronized (this.consumersMonitor) { initializeConsumers(); if (this.consumers == null) { logger.info("Consumers were initialized and then cleared (presumably the container was stopped concurrently)"); return; } Set<AsyncMessageProcessingConsumer> processors = new HashSet<AsyncMessageProcessingConsumer>(); for (BlockingQueueConsumer consumer : this.consumers) { AsyncMessageProcessingConsumer processor = new AsyncMessageProcessingConsumer(consumer);//最终执行类 processors.add(processor); this.taskExecutor.execute(processor); } for (AsyncMessageProcessingConsumer processor : processors) { FatalListenerStartupException startupException = processor.getStartupException(); if (startupException != null) { throw new AmqpIllegalStateException("Fatal exception on listener startup", startupException); } } } }
AsyncMessageProcessingConsumer中的run方法:
while (isActive() || continuable) {//实现无限接受消息 try { // Will come back false when the queue is drained continuable = receiveAndExecute(consumer) && !isChannelTransacted(); } catch (ListenerExecutionFailedException ex) { // Continue to process, otherwise re-throw } } private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Throwable { Channel channel = consumer.getChannel(); for (int i = 0; i < txSize; i++) { logger.trace("Waiting for message from consumer."); Message message = consumer.nextMessage(receiveTimeout);//如果没有消息,会在这里阻塞,否则接受新消息继续进行 if (message == null) { break; } try { executeListener(channel, message);//执行MessageListener的onMessage方法。 } catch (ImmediateAcknowledgeAmqpException e) { break; } catch (Throwable ex) { consumer.rollbackOnExceptionIfNecessary(ex); throw ex; } }
此外,SimpleMessageListenerContainer还提供了shutdown方法,如果需要停止接收信息,即可调用此方法。
这里面还有一个比较有意思的地方SimpleMessageListenerContainer中的私有类AsyncMessageProcessingConsumer中有一个容量为1的CountDownLatch锁,这主要是为了实现一个AsyncMessageProcessingConsumer实例仅执行一次
时序图如下:
总结:用一个阻塞队列来存储接受的信息可以保证读取消息的一致性,巧妙利用了LifeCycle接口的start,实现自动开始监听消息。
4) 最大的亮点,增加了转换器的概念,提供了内置的JSON,Object,Simple转换器
这点是Spring创新的最大亮点,可以自由转换消息的格式,通过RabbitTemplate的convertAndSend方法来实现消息的封装发送,receiveAndConvert来实现消息的接受和封装,所有的转换器都实现MessageConverter接口,如下方法:
Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException;
Object fromMessage(Message message) throws MessageConversionException;
toMessage是在发送消息前封装消息的方法,fromMessage是在接受消息后封装消息的方法。
已有的实现JsonMessageConverter是专门封装Json对象的,Spring用了jackson来实现解析json。SimpleMessageConverter是封装普通文本或者java对象的实现。SerializerMessageConverter是专门实现Java对象的封装。
最关键的是你可以实现自己的messageConver,根据自己的需求来from和to。这一点是非常值得推荐的。
5) 实现了队列、交换机和绑定的自动创建。
一般来说在配置文件中如果定义了Queue,Exchange,Binding对象的实例,Spring会自动创建这些定义,但前提是一定要定义一个RabbitAdmin实例。这里面有什么猫腻呢?通过源码可以看到RabbitAdmin实现了InitializingBean接口,这是Spring的标准接口,意义是在该bean的属性设置完以后立即执行,继续看源码,发现这个方法里添加了一个ConnectionListener,在Connetion创建完成后,寻找Spring中定义的所有Queue,Exchange,Binding类型的Bean,并利用rabbitTemplate的execute回调方法创建Queue,Exchange,Binding。
时序图如下:
6) 提供了一个log4J版本的Appender,可以实现分布式日志服务
如果我们的应用实现了集群,要查某条日志的时候不知道这条日志会输出在哪台机器上,如果集群的数量非常大的话这将是一场灾难,所以实现日志统一输出成为了必要,利用Springamqp提供的Appender可以轻松实现所有集群节点的日志有序的输出在同一目的地。
实现类是org.springframework.amqp.rabbit.log4j. AmqpAppender,主要原理就是执行日志记录时会实现public void append(LoggingEvent event)方法,然后会启动多个线程专门扫描接受消息的queue的内容,有则取出,封装并发送至目标队列。具体地,首先执行startSenders方法,创建一个缓冲式线程池,根据设置的缓冲大小提交一系列EventSender任务,EventSender的run方法中会取出events的元素,events是一个阻塞队列当为空时会发生阻塞,然后封装这个取出的event对象,并发送。
总结:良好的运用了线程池的概念,多线程监控同一阻塞队列,阻塞队列能保证数据的一致性,多线程能提高吞吐效率。
总结
Spring提供了操作RabbitMQ的一种解决方案,不但兼容了RabbitMQ的标准操作,还创造性的增加了几个新特性,其中包含了很多在软件开发过程中可以借鉴的编程思想,设计方法。