Java tutorial
/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ package amqp.spring.camel.component; import org.aopalliance.aop.Advice; import org.apache.camel.Exchange; import org.apache.camel.ExchangePattern; import org.apache.camel.Processor; import org.apache.camel.impl.DefaultConsumer; import org.apache.camel.impl.DefaultExchange; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.AmqpConnectException; import org.springframework.amqp.AmqpIOException; import org.springframework.amqp.AmqpRejectAndDontRequeueException; import org.springframework.amqp.core.*; import org.springframework.amqp.rabbit.config.StatefulRetryOperationsInterceptorFactoryBean; import org.springframework.amqp.rabbit.connection.Connection; import org.springframework.amqp.rabbit.connection.ConnectionListener; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.amqp.rabbit.retry.MessageKeyGenerator; import org.springframework.amqp.rabbit.retry.MessageRecoverer; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.ErrorHandler; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.Map; import java.util.StringTokenizer; public class SpringAMQPConsumer extends DefaultConsumer implements ConnectionListener { private static transient final Logger LOG = LoggerFactory.getLogger(SpringAMQPConsumer.class); private static final String TTL_QUEUE_ARGUMENT = "x-message-ttl"; private static final String HA_POLICY_ARGUMENT = "x-ha-policy"; private static final String DEAD_LETTER_EXCHANGE_ARGUMENT = "x-dead-letter-exchange"; private static final String DEAD_LETTER_ROUTING_KEY_ARGUMENT = "x-dead-letter-routing-key"; protected SpringAMQPEndpoint endpoint; private RabbitMQMessageListener messageListener; public SpringAMQPConsumer(SpringAMQPEndpoint endpoint, Processor processor) { super(endpoint, processor); this.endpoint = endpoint; this.messageListener = new RabbitMQMessageListener(endpoint); } @Override public void doStart() throws Exception { super.doStart(); if (!this.messageListener.listenerContainer.isActive()) this.messageListener.start(); } @Override public void doShutdown() throws Exception { this.messageListener.shutdown(); super.shutdown(); } protected static Map<String, Object> parseKeyValues(String routingKey) { StringTokenizer tokenizer = new StringTokenizer(routingKey, "&|"); Map<String, Object> pairs = new HashMap<String, Object>(); while (tokenizer.hasMoreTokens()) { String token = tokenizer.nextToken(); String[] keyValue = token.split("="); if (keyValue.length != 2) throw new IllegalArgumentException( "Couldn't parse key/value pair [" + token + "] out of string: " + routingKey); pairs.put(keyValue[0], keyValue[1]); } return pairs; } @Override public void onCreate(Connection connection) { LOG.info("Network connection created to broker for endpoint {}", this.getEndpoint()); } @Override public void onClose(Connection connection) { // This event is received when the consumer initiates a close, // but this event is _not_ received when RabbitMQ is the one that breaks the connection. LOG.info("Network connection closed to broker for endpoint {}", this.getEndpoint()); } //We have to ask the RabbitMQ Template for converters, the interface doesn't have a way to get MessageConverter class RabbitMQMessageListener implements MessageListener { private MessageConverter msgConverter; private SimpleMessageListenerContainer listenerContainer; private static final long DEFAULT_TIMEOUT_MILLIS = 1000; public RabbitMQMessageListener(SpringAMQPEndpoint endpoint) { this.listenerContainer = new SimpleMessageListenerContainer(); this.listenerContainer.setTaskExecutor(new SpringAMQPExecutor(endpoint)); RabbitTemplate template = (RabbitTemplate) endpoint.getAmqpTemplate(); if (template != null) { this.msgConverter = template.getMessageConverter(); this.listenerContainer.setConnectionFactory(template.getConnectionFactory()); } else { LOG.error("No AMQP Template found! Cannot initialize message conversion or connections!"); } this.listenerContainer.setQueueNames(endpoint.getQueueName()); this.listenerContainer.setConcurrentConsumers(endpoint.getConcurrentConsumers()); this.listenerContainer.setPrefetchCount(endpoint.getPrefetchCount()); //Set error handling (send it to Camel) this.listenerContainer.setErrorHandler(getErrorHandler()); this.listenerContainer.setAdviceChain(getAdviceChain()); //Set timeouts this.listenerContainer.setShutdownTimeout(DEFAULT_TIMEOUT_MILLIS); this.listenerContainer.setReceiveTimeout(DEFAULT_TIMEOUT_MILLIS); this.listenerContainer.setRecoveryInterval(DEFAULT_TIMEOUT_MILLIS / 2); //Transactions are currently not supported this.listenerContainer.setChannelTransacted(false); if (endpoint.isDLQEnabled()) { this.listenerContainer.setAcknowledgeMode(AcknowledgeMode.AUTO); } else { this.listenerContainer.setAcknowledgeMode(AcknowledgeMode.NONE); } } public void start() { this.listenerContainer.setMessageListener(this); this.listenerContainer.start(); LOG.info("Started AMQP Async Listeners for {}", endpoint.getEndpointUri()); } public void stop() { this.listenerContainer.setConcurrentConsumers(0); this.listenerContainer.setPrefetchCount(0); this.listenerContainer.stop(); } public void shutdown() { this.listenerContainer.shutdown(); this.listenerContainer.destroy(); } public final ErrorHandler getErrorHandler() { return new ErrorHandler() { @Override public void handleError(Throwable t) { if (t instanceof AmqpConnectException) { LOG.error("AMQP Connection error, marking this connection as failed"); onClose(null); } getExceptionHandler().handleException(t); } }; } /** * Do not have Spring AMQP re-try messages upon failure, leave it to Camel * @return An advice chain populated with a NeverRetryPolicy */ public final Advice[] getAdviceChain() { RetryTemplate retryRule = new RetryTemplate(); SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(); retryPolicy.setMaxAttempts(1); retryRule.setRetryPolicy(retryPolicy); StatefulRetryOperationsInterceptorFactoryBean retryOperation = new StatefulRetryOperationsInterceptorFactoryBean(); retryOperation.setRetryOperations(retryRule); retryOperation.setMessageKeyGeneretor(new DefaultKeyGenerator()); if (endpoint.isDLQEnabled()) { // will trigger sending the message to the DL exchange retryOperation.setMessageRecoverer(new MessageRecoverer() { @Override public void recover(Message message, Throwable cause) { throw new AmqpRejectAndDontRequeueException(cause == null ? "" : cause.getMessage()); } }); } return new Advice[] { retryOperation.getObject() }; } @Override public void onMessage(Message amqpMessage) { if (this.msgConverter == null) throw new IllegalStateException("No message converter present - cannot processs messages!"); LOG.debug("Received message for routing key {}", amqpMessage.getMessageProperties().getReceivedRoutingKey()); ExchangePattern exchangePattern = SpringAMQPMessage.getExchangePattern(amqpMessage); Exchange exchange = new DefaultExchange(endpoint, exchangePattern); SpringAMQPMessage camelMessage = SpringAMQPMessage.fromAMQPMessage(msgConverter, amqpMessage); exchange.setIn(camelMessage); try { getProcessor().process(exchange); } catch (Throwable t) { exchange.setException(t); } //Send a reply if one was requested Address replyToAddress = amqpMessage.getMessageProperties().getReplyToAddress(); if (replyToAddress != null) { org.apache.camel.Message outMessage = exchange.getOut(); SpringAMQPMessage replyMessage = new SpringAMQPMessage(outMessage); // Camel exchange will contain a non-null exception if an unhandled exception has occurred, // such as when using the DefaultErrorHandler with default configuration, or when // using the DeadLetterChannel error handler with an OnException handled=false override. // Exchange will not contain an exception (via getException()) if the exception has been handled, // such as when using the DeadLetterChannel error handler with default configuration, but // the Exchange property EXCEPTION_CAUGHT will contain the handled exception. if (exchange.getException() != null) { replyMessage.setHeader(SpringAMQPMessage.IS_EXCEPTION_CAUGHT, true); replyMessage.setBody(exchange.getException()); } else if (exchange.getProperty(Exchange.EXCEPTION_CAUGHT) != null) { replyMessage.setHeader(SpringAMQPMessage.IS_EXCEPTION_CAUGHT, true); replyMessage.setBody(exchange.getProperty(Exchange.EXCEPTION_CAUGHT)); } exchange.setOut(replyMessage); //Swap out the outbound message try { endpoint.getAmqpTemplate().send(replyToAddress.getExchangeName(), replyToAddress.getRoutingKey(), replyMessage.toAMQPMessage(msgConverter)); } catch (AmqpConnectException e) { LOG.error("AMQP Connection error, marking this connection as failed"); onClose(null); } } } } //If the producer does not generate an ID, let's do so now static class DefaultKeyGenerator implements MessageKeyGenerator { public static final String ALGORITHM = "MD5"; @Override public Object getKey(Message message) { try { MessageDigest digest = MessageDigest.getInstance(ALGORITHM); digest.update(message.getBody()); return String.valueOf(digest.digest()); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } } /** * Declaration of AMQP exchange, queue, and binding as well as re-try logic * all injected into SimpleMessageListenerContainer via its taskExecutor, * due to the following three problems: * 1.) Broker restarts with non-durable queues (where the new broker instance will not * contain the non-durable queues that existed in the old broker instance) * will create a failure of the BlockingQueueConsumer due to its unfortunate reliance * on passive declaration (which fails if the queue does not exist). * 2.) Load balancer failover of one broker to another will maintain the client's network * connection so we cannot rely on network connection events. * 3.) Both SimpleMessageListenerContainer and BlockingQueueConsumer have important * fields that are private without any getters, so subclassing either class fails to * provide access to needed functionality. */ class SpringAMQPExecutor extends SimpleAsyncTaskExecutor { private SpringAMQPEndpoint endpoint; SpringAMQPExecutor(SpringAMQPEndpoint endpoint) { this.endpoint = endpoint; } @Override public void execute(final Runnable task) { Runnable enrichedTask = new SpringAMQPExecutorTask(endpoint, task); super.execute(enrichedTask); } @Override public void execute(final Runnable task, long startTimeout) { Runnable enrichedTask = new SpringAMQPExecutorTask(endpoint, task); super.execute(enrichedTask, startTimeout); } } class SpringAMQPExecutorTask implements Runnable { private SpringAMQPEndpoint endpoint; private Runnable delegateTask; // Retry every 30 seconds upon error public static final long RECOVERY_INTERVAL_MILLISECONDS = 30000L; public SpringAMQPExecutorTask(SpringAMQPEndpoint endpoint, Runnable delegateTask) { this.endpoint = endpoint; this.delegateTask = delegateTask; } @Override public void run() { boolean error; do { try { error = false; declareAMQPEntities(); delegateTask.run(); } catch (Exception e) { error = true; LOG.error("Error consuming endpoint " + endpoint + ". " + e.getMessage(), e); try { Thread.sleep(RECOVERY_INTERVAL_MILLISECONDS); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new IllegalStateException("Unrecoverable interruption on consumer restart"); } } } while (error); } protected void declareAMQPEntities() { org.springframework.amqp.core.Exchange exchange = declareExchange(); Queue queue = declareQueue(); declareBinding(exchange, queue); } protected org.springframework.amqp.core.Exchange declareExchange() { org.springframework.amqp.core.Exchange exchange = this.endpoint.createAMQPExchange(); if (this.endpoint.isUsingDefaultExchange()) { LOG.info("Using default exchange; will not declare one for endpoint {}.", endpoint); } else { try { this.endpoint.amqpAdministration.declareExchange(exchange); LOG.info("Declared exchange {} for endpoint {}.", exchange.getName(), endpoint); } catch (AmqpIOException e) { LOG.warn(String.format( "Could not declare exchange %s for endpoint %s; possible re-declaration of a different type?", exchange.getName(), endpoint.toString()), e); // Be lenient: Do not re-throw Exception because the exchange may already exist but just declared // with different attributes, so let's go ahead and declare the queue and binding anyway. } catch (AmqpConnectException e) { LOG.error(String.format("Consumer cannot connect to broker for endpoint %s", this.endpoint.toString()), e); throw e; } } return exchange; } protected Queue declareQueue() { //Determine queue arguments, including vendor extensions Map<String, Object> queueArguments = new HashMap<String, Object>(); if (endpoint.getTimeToLive() != null) queueArguments.put(TTL_QUEUE_ARGUMENT, endpoint.getTimeToLive()); if (endpoint.isHa()) queueArguments.put(HA_POLICY_ARGUMENT, "all"); if (endpoint.getDeadLetterExchangeName() != null) queueArguments.put(DEAD_LETTER_EXCHANGE_ARGUMENT, endpoint.getDeadLetterExchangeName()); if (endpoint.getDeadLetterRoutingKey() != null) queueArguments.put(DEAD_LETTER_ROUTING_KEY_ARGUMENT, endpoint.getDeadLetterRoutingKey()); //Declare queue Queue queue = new Queue(this.endpoint.queueName, this.endpoint.durable, this.endpoint.exclusive, this.endpoint.autodelete, queueArguments); this.endpoint.getAmqpAdministration().declareQueue(queue); LOG.info("Declared queue {} for endpoint {}.", queue.getName(), endpoint); return queue; } protected Binding declareBinding(org.springframework.amqp.core.Exchange exchange, Queue queue) { Binding binding = null; //Is this a header exchange? Bind the key/value pair(s) if (exchange instanceof HeadersExchange) { if (this.endpoint.routingKey == null) throw new IllegalStateException("Specified a header exchange without a key/value match"); if (this.endpoint.routingKey.contains("|") && this.endpoint.routingKey.contains("&")) throw new IllegalArgumentException( "You cannot mix AND and OR expressions within a header binding"); Map<String, Object> keyValues = parseKeyValues(this.endpoint.routingKey); BindingBuilder.HeadersExchangeMapConfigurer mapConfig = BindingBuilder.bind(queue) .to((HeadersExchange) exchange); if (this.endpoint.routingKey.contains("|")) binding = mapConfig.whereAny(keyValues).match(); else binding = mapConfig.whereAll(keyValues).match(); //Is this a fanout exchange? Just bind the queue and exchange directly } else if (exchange instanceof FanoutExchange) { binding = BindingBuilder.bind(queue).to((FanoutExchange) exchange); //Perform routing key binding for direct or topic exchanges } else { binding = BindingBuilder.bind(queue).to(exchange).with(this.endpoint.routingKey).noargs(); } if (this.endpoint.isUsingDefaultExchange()) { LOG.info( "Using default exchange for endpoint {}. Default exchange is implicitly bound to every queue, with a routing key equal to the queue name.", endpoint); } else if (binding != null) { LOG.info("Declaring binding {} for endpoint {}.", binding.getRoutingKey(), endpoint); this.endpoint.getAmqpAdministration().declareBinding(binding); } return binding; } } }