From 18f9fe6810a01ad00f4ee55fe4db3851f785f501 Mon Sep 17 00:00:00 2001 From: Mark Fisher Date: Thu, 6 Oct 2011 13:14:06 -0400 Subject: [PATCH] initial implementation of AMQP channels --- docs/src/reference/docbook/amqp.xml | 63 ++-- .../amqp/channel/AbstractAmqpChannel.java | 49 +++ .../amqp/channel/PollableAmqpChannel.java | 85 +++++ .../amqp/channel/SubscribableAmqpChannel.java | 196 ++++++++++ .../amqp/config/AmqpChannelFactoryBean.java | 350 ++++++++++++++++++ .../amqp/config/AmqpChannelParser.java | 76 ++++ .../amqp/config/AmqpNamespaceHandler.java | 2 + .../config/spring-integration-amqp-2.1.xsd | 290 ++++++++++----- .../amqp/config/ChannelSample-context.xml | 28 ++ .../amqp/config/ChannelSample.java | 31 ++ .../config/PubSubChannelSample-context.xml | 28 ++ .../amqp/config/PubSubChannelSample.java | 31 ++ .../dispatcher/AbstractDispatcher.java | 5 +- 13 files changed, 1125 insertions(+), 109 deletions(-) create mode 100644 spring-integration-amqp/src/main/java/org/springframework/integration/amqp/channel/AbstractAmqpChannel.java create mode 100644 spring-integration-amqp/src/main/java/org/springframework/integration/amqp/channel/PollableAmqpChannel.java create mode 100644 spring-integration-amqp/src/main/java/org/springframework/integration/amqp/channel/SubscribableAmqpChannel.java create mode 100644 spring-integration-amqp/src/main/java/org/springframework/integration/amqp/config/AmqpChannelFactoryBean.java create mode 100644 spring-integration-amqp/src/main/java/org/springframework/integration/amqp/config/AmqpChannelParser.java create mode 100644 spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/ChannelSample-context.xml create mode 100644 spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/ChannelSample.java create mode 100644 spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/PubSubChannelSample-context.xml create mode 100644 spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/PubSubChannelSample.java diff --git a/docs/src/reference/docbook/amqp.xml b/docs/src/reference/docbook/amqp.xml index ec7a58dac8..58fe3f44d9 100644 --- a/docs/src/reference/docbook/amqp.xml +++ b/docs/src/reference/docbook/amqp.xml @@ -18,6 +18,10 @@ Outbound Gateway + + Spring Integration also provides a point-to-point Message Channel as well as a + publish/subscribe Message Channel backed by AMQP Exchanges and Queues. + In order to provide AMQP support, Spring Integration relies on Spring AMQP (http://www.springsource.org/spring-amqp) @@ -27,7 +31,7 @@ Whereas the provided AMQP Channel Adapters are intended for unidirectional - Messaging (send or receive), only, Spring Integration also provides inbound + Messaging (send or receive) only, Spring Integration also provides inbound and outbound AMQP Gateways for request/reply operations. @@ -250,48 +254,48 @@ A configuration sample for an AMQP Outbound Channel Adapter is shown below with all available parameters. - ]]> + ]]> - + Unique ID for this adapter. Optional. - + Message Channel to which Messages should be sent in order to have them converted and published to an AMQP Exchange. Required. - + Bean Reference to the configured AMQP Template Optional (Defaults to "amqpTemplate"). - + The name of the AMQP Exchange to which Messages should be sent. If not provided, Messages will be sent to the default, no-name Exchange. Optional. - + The order for this consumer when multiple consumers are registered thereby enabling load- balancing and/or failover. Optional (Defaults to Ordered.LOWEST_PRECEDENCE [=Integer.MAX_VALUE]). - + The fixed routing-key to use when sending Messages. By default, this will be an empty String. Optional. - + The routing-key to use when sending Messages evaluated as an expression on the message (e.g. 'payload.key'). By default, this will be an empty String. @@ -419,15 +423,30 @@ AMQP Backed Message Channels - This feature is not currently available, yet, but it is planned for the - Spring Integration 2.1 M2 release. In order to follow progress, please - visit the Spring Integration issue tracker at - - https://jira.springsource.org/browse/INT-1878 - + There are two Message Channel implementations available. One is point-to-point, and the other is publish/subscribe. + Both of these channels provide a wide range of configuration attributes for the underlying AmqpTemplate and + SimpleMessageListenerContainer as you have seen on the Channel Adapters and Gateways. However, the examples we'll + show here are going to have minimal configuration. Explore the XML schema to view the available attributes. - + + A point-to-point channel would look like this: + ]]> + Under the covers a Queue named "si.p2pChannel" would be declared, and this channel will send to + that Queue (technically by sending to the no-name Direct Exchange with a routing key that matches this Queue's name). + This channel will also register a consumer on that Queue. If for some reason, you want the Queue to be "pollable" + instead of message-driven, then simply provide the "message-driven" flag with a value of false: + ]]> + + + A publish/subscribe channel would look like this: + ]]> + Under the covers a Fanout Exchange named "si.fanout.pubSubChannel" would be declared, and this channel will send + to that Fanout Exchange. This channel will also declare a server-named exclusive, autodelete, non-durable Queue + and bind that to the Fanout Exchange while registering a consumer on that Queue to receive Messages. There is no + "pollable" option for a publish-subscribe-channel; it must be message-driven. + +
AMQP Samples diff --git a/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/channel/AbstractAmqpChannel.java b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/channel/AbstractAmqpChannel.java new file mode 100644 index 0000000000..dab69d5eb9 --- /dev/null +++ b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/channel/AbstractAmqpChannel.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.amqp.channel; + +import org.springframework.amqp.core.AmqpTemplate; +import org.springframework.integration.Message; +import org.springframework.integration.channel.AbstractMessageChannel; +import org.springframework.util.Assert; + +/** + * @author Mark Fisher + * @since 2.1 + */ +public abstract class AbstractAmqpChannel extends AbstractMessageChannel { + + private final AmqpTemplate amqpTemplate; + + + AbstractAmqpChannel(AmqpTemplate amqpTemplate) { + Assert.notNull(amqpTemplate, "amqpTemplate must not be null"); + this.amqpTemplate = amqpTemplate; + } + + + AmqpTemplate getAmqpTemplate() { + return this.amqpTemplate; + } + + @Override + protected boolean doSend(Message message, long timeout) { + this.amqpTemplate.convertAndSend(message); + return true; + } + +} diff --git a/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/channel/PollableAmqpChannel.java b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/channel/PollableAmqpChannel.java new file mode 100644 index 0000000000..0965976b29 --- /dev/null +++ b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/channel/PollableAmqpChannel.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.amqp.channel; + +import org.springframework.amqp.core.AmqpTemplate; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.integration.Message; +import org.springframework.integration.core.PollableChannel; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.util.Assert; + +/** + * @author Mark Fisher + * @since 2.1 + */ +public class PollableAmqpChannel extends AbstractAmqpChannel implements PollableChannel { + + private final String channelName; + + + public PollableAmqpChannel(String channelName, AmqpTemplate amqpTemplate) { + super(amqpTemplate); + Assert.hasText(channelName, "channel name must not be empty"); + this.channelName = channelName; + } + + + @Override + protected void onInit() throws Exception { + AmqpTemplate amqpTemplate = this.getAmqpTemplate(); + if (!(amqpTemplate instanceof RabbitTemplate)) { + throw new IllegalArgumentException("AmqpTemplate must be a RabbitTemplate"); + } + RabbitTemplate rabbitTemplate = (RabbitTemplate) amqpTemplate; + RabbitAdmin admin = new RabbitAdmin(rabbitTemplate.getConnectionFactory()); + String queueName = "si." + this.channelName; + Queue queue = new Queue(queueName); + admin.declareQueue(queue); + rabbitTemplate.setRoutingKey(queueName); + rabbitTemplate.setQueue(queueName); + } + + public Message receive() { + if (!this.getInterceptors().preReceive(this)) { + return null; + } + Object object = this.getAmqpTemplate().receiveAndConvert(); + if (object == null) { + return null; + } + Message replyMessage = null; + if (object instanceof Message) { + replyMessage = (Message) object; + } + else { + replyMessage = MessageBuilder.withPayload(object).build(); + } + return this.getInterceptors().postReceive(replyMessage, this) ; + } + + public Message receive(long timeout) { + if (logger.isInfoEnabled()) { + logger.info("Calling receive with a timeout value on PollableAmqpChannel. " + + "The timeout will be ignored since no receive timeout is supported."); + } + return this.receive(); + } + +} diff --git a/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/channel/SubscribableAmqpChannel.java b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/channel/SubscribableAmqpChannel.java new file mode 100644 index 0000000000..16bc2ad2dd --- /dev/null +++ b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/channel/SubscribableAmqpChannel.java @@ -0,0 +1,196 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.amqp.channel; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.amqp.core.AmqpTemplate; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.FanoutExchange; +import org.springframework.amqp.core.MessageListener; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.SmartLifecycle; +import org.springframework.integration.Message; +import org.springframework.integration.MessagingException; +import org.springframework.integration.core.MessageHandler; +import org.springframework.integration.core.SubscribableChannel; +import org.springframework.integration.dispatcher.BroadcastingDispatcher; +import org.springframework.integration.dispatcher.MessageDispatcher; +import org.springframework.integration.dispatcher.RoundRobinLoadBalancingStrategy; +import org.springframework.integration.dispatcher.UnicastingDispatcher; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.util.Assert; + +/** + * @author Mark Fisher + * @since 2.1 + */ +public class SubscribableAmqpChannel extends AbstractAmqpChannel implements SubscribableChannel, SmartLifecycle, DisposableBean { + + private final String channelName; + + private final SimpleMessageListenerContainer container; + + private final boolean isPubSub; + + private volatile MessageDispatcher dispatcher; + + + public SubscribableAmqpChannel(String channelName, SimpleMessageListenerContainer container, AmqpTemplate amqpTemplate, boolean isPubSub) { + super(amqpTemplate); + Assert.notNull(container, "container must not be null"); + Assert.hasText(channelName, "channel name must not be empty"); + this.channelName = channelName; + this.container = container; + this.isPubSub = isPubSub; + } + + + public boolean subscribe(MessageHandler handler) { + return this.dispatcher.addHandler(handler); + } + + public boolean unsubscribe(MessageHandler handler) { + return this.dispatcher.removeHandler(handler); + } + + @Override + public void onInit() throws Exception { + super.onInit(); + this.configureDispatcher(); + AmqpTemplate amqpTemplate = this.getAmqpTemplate(); + if (!(amqpTemplate instanceof RabbitTemplate)) { + throw new IllegalArgumentException("AmqpTemplate must be a RabbitTemplate"); + } + RabbitTemplate rabbitTemplate = (RabbitTemplate) amqpTemplate; + RabbitAdmin admin = new RabbitAdmin(rabbitTemplate.getConnectionFactory()); + if (this.isPubSub) { + FanoutExchange exchange = new FanoutExchange("si.fanout." + this.channelName); + admin.declareExchange(exchange); + Queue queue = admin.declareQueue(); + Binding binding = BindingBuilder.bind(queue).to(exchange); + admin.declareBinding(binding); + this.container.setQueues(queue); + rabbitTemplate.setExchange(exchange.getName()); + } + else { + String queueName = "si." + this.channelName; + Queue queue = new Queue(queueName); + admin.declareQueue(queue); + this.container.setQueues(queue); + rabbitTemplate.setRoutingKey(queueName); + } + MessageListener listener = new DispatchingMessageListener(rabbitTemplate, this.dispatcher); + this.container.setMessageListener(listener); + if (!this.container.isActive()) { + this.container.afterPropertiesSet(); + } + rabbitTemplate.afterPropertiesSet(); + } + + private void configureDispatcher() { + if (this.isPubSub) { + this.dispatcher = new BroadcastingDispatcher(); + } + else { + UnicastingDispatcher unicastingDispatcher = new UnicastingDispatcher(); + unicastingDispatcher.setLoadBalancingStrategy(new RoundRobinLoadBalancingStrategy()); + this.dispatcher = unicastingDispatcher; + } + } + + + private static class DispatchingMessageListener implements MessageListener { + + private final Log logger = LogFactory.getLog(this.getClass()); + + private final RabbitTemplate rabbitTemplate; + + private final MessageDispatcher dispatcher; + + + private DispatchingMessageListener(RabbitTemplate rabbitTemplate, MessageDispatcher dispatcher) { + this.rabbitTemplate = rabbitTemplate; + this.dispatcher = dispatcher; + } + + + public void onMessage(org.springframework.amqp.core.Message message) { + try { + Object converted = this.rabbitTemplate.getMessageConverter().fromMessage(message); + if (converted != null) { + Message messageToSend = (converted instanceof Message) ? (Message) converted + : MessageBuilder.withPayload(converted).build(); + this.dispatcher.dispatch(messageToSend); + } + else if (this.logger.isWarnEnabled()) { + logger.warn("MessageConverter returned null, no Message to dispatch"); + } + } + catch (Exception e) { + throw new MessagingException("Failure occured in AMQP listener while attempting to convert and dispatch Message.", e); + } + } + } + + + /* + * SmartLifecycle implementation (delegates to the MessageListener container) + */ + + public boolean isAutoStartup() { + return (this.container != null) ? this.container.isAutoStartup() : false; + } + + public int getPhase() { + return (this.container != null) ? this.container.getPhase() : 0; + } + + public boolean isRunning() { + return (this.container != null) ? this.container.isRunning() : false; + } + + public void start() { + if (this.container != null) { + this.container.start(); + } + } + + public void stop() { + if (this.container != null) { + this.container.stop(); + } + } + + public void stop(Runnable callback) { + if (this.container != null) { + this.container.stop(callback); + } + } + + public void destroy() throws Exception { + if (this.container != null) { + this.container.destroy(); + } + } + +} diff --git a/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/config/AmqpChannelFactoryBean.java b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/config/AmqpChannelFactoryBean.java new file mode 100644 index 0000000000..c7a47f4c2c --- /dev/null +++ b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/config/AmqpChannelFactoryBean.java @@ -0,0 +1,350 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.amqp.config; + +import java.util.List; +import java.util.concurrent.Executor; + +import org.aopalliance.aop.Advice; + +import org.springframework.amqp.core.AcknowledgeMode; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; +import org.springframework.amqp.rabbit.support.MessagePropertiesConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.context.Lifecycle; +import org.springframework.context.SmartLifecycle; +import org.springframework.integration.amqp.channel.AbstractAmqpChannel; +import org.springframework.integration.amqp.channel.PollableAmqpChannel; +import org.springframework.integration.amqp.channel.SubscribableAmqpChannel; +import org.springframework.integration.channel.ChannelInterceptor; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.interceptor.TransactionAttribute; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ErrorHandler; +import org.springframework.util.ObjectUtils; + +/** + * If point-to-point, we send to the default exchange with the routing key + * equal to "si.[beanName]" and we declare that same Queue and register a listener + * if message-driven or poll explicitly otherwise. If publish-subscribe, we declare + * a FanoutExchange named "si.fanout.[beanName]" and we send to that without any + * routing key, and on the receiving side, we create an anonymous Queue that is + * bound to that exchange. + * + * @author Mark Fisher + * @since 2.1 + */ +public class AmqpChannelFactoryBean extends AbstractFactoryBean implements SmartLifecycle, DisposableBean, BeanNameAware { + + private volatile AbstractAmqpChannel channel; + + private volatile List interceptors; + + private final boolean messageDriven; + + private final RabbitTemplate rabbitTemplate = new RabbitTemplate(); + + private volatile SimpleMessageListenerContainer container; + + private volatile boolean autoStartup = true; + + private volatile Advice[] adviceChain; + + private volatile Integer concurrentConsumers; + + private volatile ConnectionFactory connectionFactory; + + private volatile MessagePropertiesConverter messagePropertiesConverter; + + private volatile ErrorHandler errorHandler; + + private volatile Boolean exposeListenerChannel; + + private volatile Integer phase; + + private volatile Integer prefetchCount; + + private volatile Boolean isPubSub; + + private volatile Long receiveTimeout; + + private volatile Long recoveryInterval; + + private volatile Long shutdownTimeout; + + private volatile String beanName; + + private volatile AcknowledgeMode acknowledgeMode; + + /** + * This value differs from the container implementations' default (which is false). + */ + private volatile boolean channelTransacted = true; + + private volatile Executor taskExecutor; + + private volatile PlatformTransactionManager transactionManager; + + private volatile TransactionAttribute transactionAttribute; + + private volatile Integer txSize; + + + public AmqpChannelFactoryBean() { + this(true); + } + + public AmqpChannelFactoryBean(boolean messageDriven) { + this.messageDriven = messageDriven; + } + + + public void setBeanName(String name) { + this.beanName = name; + } + + public void setInterceptors(List interceptors) { + this.interceptors = interceptors; + } + + /* + * Template-only properties + */ + + public void setEncoding(String encoding) { + this.rabbitTemplate.setEncoding(encoding); + } + + public void setMessageConverter(MessageConverter messageConverter) { + this.rabbitTemplate.setMessageConverter(messageConverter); + } + + /* + * Template and Container properties + */ + + public void setChannelTransacted(boolean channelTransacted) { + this.channelTransacted = channelTransacted; + this.rabbitTemplate.setChannelTransacted(channelTransacted); + } + + public void setConnectionFactory(ConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + this.rabbitTemplate.setConnectionFactory(this.connectionFactory); + } + + public void setMessagePropertiesConverter(MessagePropertiesConverter messagePropertiesConverter) { + this.rabbitTemplate.setMessagePropertiesConverter(messagePropertiesConverter); + this.messagePropertiesConverter = messagePropertiesConverter; + } + + /* + * Container-only properties + */ + + public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) { + this.acknowledgeMode = acknowledgeMode; + } + + public void setAdviceChain(Advice[] adviceChain) { + this.adviceChain = adviceChain; + } + + public void setAutoStartup(boolean autoStartup) { + this.autoStartup = autoStartup; + } + + public void setConcurrentConsumers(int concurrentConsumers) { + this.concurrentConsumers = concurrentConsumers; + } + + public void setErrorHandler(ErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } + + public void setExposeListenerChannel(boolean exposeListenerChannel) { + this.exposeListenerChannel = exposeListenerChannel; + } + + public void setPhase(int phase) { + this.phase = phase; + } + + public void setPrefetchCount(int prefetchCount) { + this.prefetchCount = prefetchCount; + } + + public void setPubSub(boolean pubSub) { + this.isPubSub = pubSub; + } + + public void setReceiveTimeout(long receiveTimeout) { + this.receiveTimeout = receiveTimeout; + } + + public void setRecoveryInterval(long recoveryInterval) { + this.recoveryInterval = recoveryInterval; + } + + public void setShutdownTimeout(long shutdownTimeout) { + this.shutdownTimeout = shutdownTimeout; + } + + public void setTaskExecutor(Executor taskExecutor) { + this.taskExecutor = taskExecutor; + } + + public void setTransactionAttribute(TransactionAttribute transactionAttribute) { + this.transactionAttribute = transactionAttribute; + } + + public void setTransactionManager(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + public void setTxSize(int txSize) { + this.txSize = this.txSize; + } + + @Override + public Class getObjectType() { + return (this.channel != null) ? this.channel.getClass() : AbstractAmqpChannel.class; + } + + @Override + protected AbstractAmqpChannel createInstance() throws Exception { + if (this.messageDriven) { + this.container = this.createContainer(); + this.channel = new SubscribableAmqpChannel(this.beanName, this.container, this.rabbitTemplate, this.isPubSub); + } + else { + Assert.isTrue(!Boolean.TRUE.equals(this.isPubSub), + "An AMQP 'publish-subscribe-channel' must be message-driven."); + this.channel = new PollableAmqpChannel(this.beanName, this.rabbitTemplate); + } + if (!CollectionUtils.isEmpty(this.interceptors)) { + this.channel.setInterceptors(this.interceptors); + } + this.channel.afterPropertiesSet(); + this.channel.setBeanName(this.beanName); + return this.channel; + } + + private SimpleMessageListenerContainer createContainer() throws Exception { + //if (!messageDriven) TODO: no container attributes would apply if not message-driven + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + if (this.acknowledgeMode != null) { + container.setAcknowledgeMode(this.acknowledgeMode); + } + if (!ObjectUtils.isEmpty(this.adviceChain)) { + container.setAdviceChain(this.adviceChain); + } + container.setAutoStartup(this.autoStartup); + container.setChannelTransacted(this.channelTransacted); + if (this.concurrentConsumers != null) { + container.setConcurrentConsumers(this.concurrentConsumers); + } + container.setConnectionFactory(this.connectionFactory); + if (this.errorHandler != null) { + container.setErrorHandler(this.errorHandler); + } + if (this.exposeListenerChannel != null) { + container.setExposeListenerChannel(this.exposeListenerChannel); + } + if (this.messagePropertiesConverter != null) { + container.setMessagePropertiesConverter(this.messagePropertiesConverter); + } + if (this.phase != null) { + container.setPhase(this.phase); + } + if (this.prefetchCount != null) { + container.setPrefetchCount(this.prefetchCount); + } + if (this.receiveTimeout != null) { + container.setReceiveTimeout(this.receiveTimeout); + } + if (this.recoveryInterval != null) { + container.setRecoveryInterval(this.recoveryInterval); + } + if (this.shutdownTimeout != null) { + container.setShutdownTimeout(this.shutdownTimeout); + } + if (this.taskExecutor != null) { + container.setTaskExecutor(this.taskExecutor); + } + if (this.transactionAttribute != null) { + container.setTransactionAttribute(this.transactionAttribute); + } + if (this.transactionManager != null) { + container.setTransactionManager(this.transactionManager); + } + if (this.txSize != null) { + container.setTxSize(this.txSize); + } + return container; + } + + /* + * SmartLifecycle implementation (delegates to the created channel if message-driven) + */ + + public boolean isAutoStartup() { + return (this.channel instanceof SmartLifecycle) ? + ((SmartLifecycle) this.channel).isAutoStartup() : false; + } + + public int getPhase() { + return (this.channel instanceof SmartLifecycle) ? + ((SmartLifecycle) this.channel).getPhase() : 0; + } + + public boolean isRunning() { + return (this.channel instanceof Lifecycle) ? + ((Lifecycle) this.channel).isRunning() : false; + } + + public void start() { + if (this.channel instanceof Lifecycle) { + ((Lifecycle) this.channel).start(); + } + } + + public void stop() { + if (this.channel instanceof Lifecycle) { + ((Lifecycle) this.channel).stop(); + } + } + + public void stop(Runnable callback) { + if (this.channel instanceof SmartLifecycle) { + ((SmartLifecycle) this.channel).stop(callback); + } + } + + protected void destroyInstance(AbstractAmqpChannel instance) throws Exception { + if (instance instanceof DisposableBean) { + ((DisposableBean) this.channel).destroy(); + } + } + +} diff --git a/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/config/AmqpChannelParser.java b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/config/AmqpChannelParser.java new file mode 100644 index 0000000000..93b347b653 --- /dev/null +++ b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/config/AmqpChannelParser.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.amqp.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.config.xml.AbstractChannelParser; +import org.springframework.integration.config.xml.IntegrationNamespaceUtils; +import org.springframework.util.StringUtils; + +/** + * Parser for the 'channel' and 'publish-subscribe-channel' elements of the + * Spring Integration AMQP namespace. + * + * @author Mark Fisher + * @since 2.1 + */ +public class AmqpChannelParser extends AbstractChannelParser { + + @Override + protected BeanDefinitionBuilder buildBeanDefinition(Element element, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(AmqpChannelFactoryBean.class); + String messageDriven = element.getAttribute("message-driven"); + if (StringUtils.hasText(messageDriven)) { + builder.addConstructorArgValue(messageDriven); + } + String connectionFactory = element.getAttribute("connection-factory"); + if (!StringUtils.hasText(connectionFactory)) { + connectionFactory = "rabbitConnectionFactory"; + } + builder.addPropertyReference("connectionFactory", connectionFactory); + if ("channel".equals(element.getLocalName())) { + builder.addPropertyValue("pubSub", false); + } + else if ("publish-subscribe-channel".equals(element.getLocalName())) { + builder.addPropertyValue("pubSub", true); + } + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "acknowledge-mode"); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "advice-chain"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "auto-startup"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "channel-transacted"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "concurrent-consumers"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "encoding"); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "error-handler"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "expose-listener-channel"); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "message-converter"); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "message-properties-converter"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "phase"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "prefetch-count"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "receive-timeout"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "recovery-interval"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "shutdown-timeout"); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "task-executor"); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "transaction-attribute"); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "transaction-manager"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "tx-size"); + return builder; + } + +} diff --git a/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/config/AmqpNamespaceHandler.java b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/config/AmqpNamespaceHandler.java index 876566bc40..a5f854f725 100644 --- a/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/config/AmqpNamespaceHandler.java +++ b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/config/AmqpNamespaceHandler.java @@ -27,6 +27,8 @@ import org.springframework.integration.config.xml.AbstractIntegrationNamespaceHa public class AmqpNamespaceHandler extends AbstractIntegrationNamespaceHandler { public void init() { + this.registerBeanDefinitionParser("channel", new AmqpChannelParser()); + this.registerBeanDefinitionParser("publish-subscribe-channel", new AmqpChannelParser()); this.registerBeanDefinitionParser("inbound-channel-adapter", new AmqpInboundChannelAdapterParser()); this.registerBeanDefinitionParser("inbound-gateway", new AmqpInboundGatewayParser()); this.registerBeanDefinitionParser("outbound-channel-adapter", new AmqpOutboundChannelAdapterParser()); diff --git a/spring-integration-amqp/src/main/resources/org/springframework/integration/amqp/config/spring-integration-amqp-2.1.xsd b/spring-integration-amqp/src/main/resources/org/springframework/integration/amqp/config/spring-integration-amqp-2.1.xsd index 3525f85962..c6726ff496 100644 --- a/spring-integration-amqp/src/main/resources/org/springframework/integration/amqp/config/spring-integration-amqp-2.1.xsd +++ b/spring-integration-amqp/src/main/resources/org/springframework/integration/amqp/config/spring-integration-amqp-2.1.xsd @@ -242,6 +242,60 @@ + + + + Creates a point-to-point channel that is backed by an AMQP Queue. + + + + + + + + + Indicate whether this channel should be message-driven (subscribable) or not (pollable). + + + + + + + + + + + + Creates a publish-subscribe-channel that is backed by an AMQP FanoutExchange. + Always message-driven (subscribable). + + + + + + + + Base type for 'channel' and 'publish-subscribe-channel'. + + + + + + Unique ID for this Message Channel. + + + + + + + Flag to indicate whether this Message Channel should start automatically. + This only applies to a message-driven channel. Default is true. + + + + + + @@ -255,58 +309,7 @@ - - - - Acknowledge Mode for the MessageListenerContainer. - - - - - - - - - - - - - - - - - - Array of AOP Advice instances to be applied to the MessageListener. - - - - - - - - Flag to indicate that AMQP channels created by this component will be transactional. - - - - - - - Specify the number of concurrent consumers to create. Default is 1. - Raising the number of concurrent consumers is recommended in order to scale the consumption of messages coming in - from a queue. However, note that any ordering guarantees are lost once multiple consumers are registered. In - general, stick with 1 consumer for low-volume queues. - - - - - - - - - - - - + @@ -319,28 +322,18 @@ - + - ErrorHandler to be configured on the underlying MessageListener container. + MessageConverter to use when receiving AMQP Messages. - + - - - - - Set whether to expose the listener Rabbit Channel to a registered ChannelAwareMessageListener as well as - to RabbitTemplate calls. - - - - @@ -369,14 +362,47 @@ - + - MessageConverter to use when receiving AMQP Messages. + Names of the AMQP Queues from which Messages should be consumed (comma-separated list). + + + + + + + + + + + + + + + + + + + + + + + + + + Flag to indicate that channels created by this component will be transactional. + + + + + + + Reference to the Rabbit ConnectionFactory to be used by this component. - + @@ -393,6 +419,105 @@ + + + + + + Attributes for a RabbitTemplate. This does not include the exchange, queue, or routingKey properties + since those may or may not be exposed for configuration depending on what type of component uses this + attribute group. This group also does not include any of the properties that are shared with the + SimpleMessageListenerContainer, such as channelTransacted, connectionFactory, and messagePropertiesConverter. + + + + + + The encoding to use when converting between byte arrays and Strings in message properties. + + + + + + + Reference to a MessageConverter to be used by this RabbitTemplate. + + + + + + + + + + + + + + Attributes for a SimpleMesssageListenerContainer's properties other than queues, queueNames, messageListener, and + autoStartup which may or may not be exposed for configuration depending on what type of component uses this attribute group. + This group also does not include any of the properties that are shared with RabbitTemplate, such as channelTransacted, + connectionFactory, and messsagePropertiesConverter. + + + + + + Acknowledge Mode for the MessageListenerContainer. + + + + + + + + + + + + + + + + + + Array of AOP Advice instances to be applied to the MessageListener. + + + + + + + + Specify the number of concurrent consumers to create. Default is 1. + Raising the number of concurrent consumers is recommended in order to scale the consumption of messages coming in + from a queue. However, note that any ordering guarantees are lost once multiple consumers are registered. In + general, stick with 1 consumer for low-volume queues. + + + + + + + ErrorHandler to be configured on the underlying MessageListener container. + + + + + + + + + + + + + Set whether to expose the listener Rabbit Channel to a registered ChannelAwareMessageListener as well as + to RabbitTemplate calls. + + + + @@ -412,13 +537,6 @@ - - - - Names of the AMQP Queues from which Messages should be consumed (comma-separated list). - - - @@ -446,16 +564,6 @@ - - - - - How many messages to process in a single transaction (if the channel is transactional). For best results it should be - less than or equal to the prefetch count. - - - - @@ -493,6 +601,16 @@ - + + + + + How many messages to process in a single transaction (if the channel is transactional). For best results it should be + less than or equal to the prefetch count. + + + + + diff --git a/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/ChannelSample-context.xml b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/ChannelSample-context.xml new file mode 100644 index 0000000000..41101975c5 --- /dev/null +++ b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/ChannelSample-context.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/ChannelSample.java b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/ChannelSample.java new file mode 100644 index 0000000000..af63d8dca1 --- /dev/null +++ b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/ChannelSample.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.amqp.config; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +/** + * @author Mark Fisher + * @since 2.1 + */ +public class ChannelSample { + + public static void main(String[] args) { + new ClassPathXmlApplicationContext("ChannelSample-context.xml", ChannelSample.class); + } + +} diff --git a/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/PubSubChannelSample-context.xml b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/PubSubChannelSample-context.xml new file mode 100644 index 0000000000..b6f398009a --- /dev/null +++ b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/PubSubChannelSample-context.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/PubSubChannelSample.java b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/PubSubChannelSample.java new file mode 100644 index 0000000000..afdf37d904 --- /dev/null +++ b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/PubSubChannelSample.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.amqp.config; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +/** + * @author Mark Fisher + * @since 2.1 + */ +public class PubSubChannelSample { + + public static void main(String[] args) { + new ClassPathXmlApplicationContext("PubSubChannelSample-context.xml", PubSubChannelSample.class); + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/dispatcher/AbstractDispatcher.java b/spring-integration-core/src/main/java/org/springframework/integration/dispatcher/AbstractDispatcher.java index d57ffec6b2..70e711d9b9 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/dispatcher/AbstractDispatcher.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/dispatcher/AbstractDispatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2011 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.integration.core.MessageHandler; +import org.springframework.util.Assert; /** * Base class for {@link MessageDispatcher} implementations. @@ -61,6 +62,7 @@ public abstract class AbstractDispatcher implements MessageDispatcher { * @return the result of {@link Set#add(Object)} */ public boolean addHandler(MessageHandler handler) { + Assert.notNull(handler, "handler must not be null"); return this.handlers.add(handler); } @@ -70,6 +72,7 @@ public abstract class AbstractDispatcher implements MessageDispatcher { * @return the result of {@link Set#remove(Object)} */ public boolean removeHandler(MessageHandler handler) { + Assert.notNull(handler, "handler must not be null"); return this.handlers.remove(handler); }