Java tutorial
/* * Copyright 2002-2019 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 * * https://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.inbound; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.Address; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.batch.BatchingStrategy; import org.springframework.amqp.rabbit.batch.SimpleBatchingStrategy; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.support.converter.SimpleMessageConverter; import org.springframework.core.AttributeAccessor; import org.springframework.integration.IntegrationMessageHeaderAccessor; import org.springframework.integration.StaticMessageHeaderAccessor; import org.springframework.integration.amqp.support.AmqpHeaderMapper; import org.springframework.integration.amqp.support.AmqpMessageHeaderErrorMessageStrategy; import org.springframework.integration.amqp.support.DefaultAmqpHeaderMapper; import org.springframework.integration.amqp.support.EndpointUtils; import org.springframework.integration.gateway.MessagingGatewaySupport; import org.springframework.integration.support.ErrorMessageUtils; import org.springframework.messaging.MessageChannel; import org.springframework.retry.RecoveryCallback; import org.springframework.retry.support.RetrySynchronizationManager; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import com.rabbitmq.client.Channel; /** * Adapter that receives Messages from an AMQP Queue, converts them into * Spring Integration Messages, and sends the results to a Message Channel. * If a reply Message is received, it will be converted and sent back to * the AMQP 'replyTo'. * * @author Mark Fisher * @author Artem Bilan * @author Gary Russell * * @since 2.1 */ public class AmqpInboundGateway extends MessagingGatewaySupport { private static final ThreadLocal<AttributeAccessor> ATTRIBUTES_HOLDER = new ThreadLocal<>(); private final AbstractMessageListenerContainer messageListenerContainer; private final AmqpTemplate amqpTemplate; private final boolean amqpTemplateExplicitlySet; private MessageConverter amqpMessageConverter = new SimpleMessageConverter(); private AmqpHeaderMapper headerMapper = DefaultAmqpHeaderMapper.inboundMapper(); private Address defaultReplyTo; private RetryTemplate retryTemplate; private RecoveryCallback<? extends Object> recoveryCallback; private BatchingStrategy batchingStrategy = new SimpleBatchingStrategy(0, 0, 0L); private boolean bindSourceMessage; public AmqpInboundGateway(AbstractMessageListenerContainer listenerContainer) { this(listenerContainer, new RabbitTemplate(listenerContainer.getConnectionFactory()), false); } /** * Construct {@link AmqpInboundGateway} based on the provided {@link AbstractMessageListenerContainer} * to receive request messages and {@link AmqpTemplate} to send replies. * @param listenerContainer the {@link AbstractMessageListenerContainer} to receive AMQP messages. * @param amqpTemplate the {@link AmqpTemplate} to send reply messages. * @since 4.2 */ public AmqpInboundGateway(AbstractMessageListenerContainer listenerContainer, AmqpTemplate amqpTemplate) { this(listenerContainer, amqpTemplate, true); } private AmqpInboundGateway(AbstractMessageListenerContainer listenerContainer, AmqpTemplate amqpTemplate, boolean amqpTemplateExplicitlySet) { Assert.notNull(listenerContainer, "listenerContainer must not be null"); Assert.notNull(amqpTemplate, "'amqpTemplate' must not be null"); Assert.isNull(listenerContainer.getMessageListener(), "The listenerContainer provided to an AMQP inbound Gateway " + "must not have a MessageListener configured since " + "the adapter needs to configure its own listener implementation."); this.messageListenerContainer = listenerContainer; this.messageListenerContainer.setAutoStartup(false); this.amqpTemplate = amqpTemplate; this.amqpTemplateExplicitlySet = amqpTemplateExplicitlySet; setErrorMessageStrategy(new AmqpMessageHeaderErrorMessageStrategy()); } /** * Specify the {@link MessageConverter} to convert request and reply to/from {@link Message}. * If the {@link #amqpTemplate} is explicitly set, this {@link MessageConverter} * isn't populated there. You must configure that external {@link #amqpTemplate}. * @param messageConverter the {@link MessageConverter} to use. */ public void setMessageConverter(MessageConverter messageConverter) { Assert.notNull(messageConverter, "MessageConverter must not be null"); this.amqpMessageConverter = messageConverter; if (!this.amqpTemplateExplicitlySet) { ((RabbitTemplate) this.amqpTemplate).setMessageConverter(messageConverter); } } public void setHeaderMapper(AmqpHeaderMapper headerMapper) { Assert.notNull(headerMapper, "headerMapper must not be null"); this.headerMapper = headerMapper; } /** * The {@code defaultReplyTo} address with the form * <pre class="code"> * (exchange)/(routingKey) * </pre> * or * <pre class="code"> * (queueName) * </pre> * if the request message doesn't have a {@code replyTo} property. * The second form uses the default exchange ("") and the queue name as * the routing key. * @param defaultReplyTo the default {@code replyTo} address to use. * @since 4.2 * @see Address */ public void setDefaultReplyTo(String defaultReplyTo) { this.defaultReplyTo = new Address(defaultReplyTo); } /** * Set a {@link RetryTemplate} to use for retrying a message delivery within the * gateway. Unlike adding retry at the container level, this can be used with an * {@code ErrorMessageSendingRecoverer} {@link RecoveryCallback} to publish to the * error channel after retries are exhausted. You generally should not configure an * error channel when using retry here, use a {@link RecoveryCallback} instead. * @param retryTemplate the template. * @since 4.3.10. * @see #setRecoveryCallback(RecoveryCallback) */ public void setRetryTemplate(RetryTemplate retryTemplate) { this.retryTemplate = retryTemplate; } /** * Set a {@link RecoveryCallback} when using retry within the gateway. * @param recoveryCallback the callback. * @since 4.3.10 * @see #setRetryTemplate(RetryTemplate) */ public void setRecoveryCallback(RecoveryCallback<? extends Object> recoveryCallback) { this.recoveryCallback = recoveryCallback; } /** * Set a batching strategy to use when de-batching messages. * Default is {@link SimpleBatchingStrategy}. * @param batchingStrategy the strategy. * @since 5.2 */ public void setBatchingStrategy(BatchingStrategy batchingStrategy) { Assert.notNull(batchingStrategy, "'batchingStrategy' cannot be null"); this.batchingStrategy = batchingStrategy; } /** * Set to true to bind the source message in the header named * {@link IntegrationMessageHeaderAccessor#SOURCE_DATA}. * @param bindSourceMessage true to bind. * @since 5.1.6 */ public void setBindSourceMessage(boolean bindSourceMessage) { this.bindSourceMessage = bindSourceMessage; } @Override public String getComponentType() { return "amqp:inbound-gateway"; } @Override protected void onInit() { if (this.retryTemplate != null) { Assert.state(getErrorChannel() == null, "Cannot have an 'errorChannel' property when a 'RetryTemplate' is " + "provided; use an 'ErrorMessageSendingRecoverer' in the 'recoveryCallback' property to " + "send an error message when retries are exhausted"); } Listener messageListener = new Listener(); this.messageListenerContainer.setMessageListener(messageListener); this.messageListenerContainer.afterPropertiesSet(); if (!this.amqpTemplateExplicitlySet) { ((RabbitTemplate) this.amqpTemplate).afterPropertiesSet(); } super.onInit(); if (this.retryTemplate != null && getErrorChannel() != null) { logger.warn( "Usually, when using a RetryTemplate you should use an ErrorMessageSendingRecoverer and not " + "provide an errorChannel. Using an errorChannel could defeat retry and will receive an error " + "message for each delivery attempt."); } } @Override protected void doStart() { super.doStart(); this.messageListenerContainer.start(); } @Override protected void doStop() { super.doStop(); this.messageListenerContainer.stop(); } /** * If there's a retry template, it will set the attributes holder via the listener. If * there's no retry template, but there's an error channel, we create a new attributes * holder here. If an attributes holder exists (by either method), we set the * attributes for use by the * {@link org.springframework.integration.support.ErrorMessageStrategy}. * @param amqpMessage the AMQP message to use. * @param message the Spring Messaging message to use. * @since 4.3.10 */ private void setAttributesIfNecessary(Message amqpMessage, org.springframework.messaging.Message<?> message) { boolean needHolder = getErrorChannel() != null && this.retryTemplate == null; boolean needAttributes = needHolder || this.retryTemplate != null; if (needHolder) { ATTRIBUTES_HOLDER.set(ErrorMessageUtils.getAttributeAccessor(null, null)); } if (needAttributes) { AttributeAccessor attributes = this.retryTemplate != null ? RetrySynchronizationManager.getContext() : ATTRIBUTES_HOLDER.get(); if (attributes != null) { attributes.setAttribute(ErrorMessageUtils.INPUT_MESSAGE_CONTEXT_KEY, message); attributes.setAttribute(AmqpMessageHeaderErrorMessageStrategy.AMQP_RAW_MESSAGE, amqpMessage); } } } @Override protected AttributeAccessor getErrorMessageAttributes(org.springframework.messaging.Message<?> message) { AttributeAccessor attributes = ATTRIBUTES_HOLDER.get(); if (attributes == null) { return super.getErrorMessageAttributes(message); } else { return attributes; } } protected class Listener implements ChannelAwareMessageListener { @SuppressWarnings("unchecked") @Override public void onMessage(final Message message, final Channel channel) { if (AmqpInboundGateway.this.retryTemplate == null) { try { org.springframework.messaging.Message<Object> converted = convert(message, channel); if (converted != null) { process(message, converted); } } finally { ATTRIBUTES_HOLDER.remove(); } } else { org.springframework.messaging.Message<Object> converted = convert(message, channel); if (converted != null) { AmqpInboundGateway.this.retryTemplate.execute(context -> { StaticMessageHeaderAccessor.getDeliveryAttempt(converted).incrementAndGet(); process(message, converted); return null; }, (RecoveryCallback<Object>) AmqpInboundGateway.this.recoveryCallback); } } } private org.springframework.messaging.Message<Object> convert(Message message, Channel channel) { Map<String, Object> headers; Object payload; boolean isManualAck = AmqpInboundGateway.this.messageListenerContainer .getAcknowledgeMode() == AcknowledgeMode.MANUAL; try { if (AmqpInboundGateway.this.batchingStrategy.canDebatch(message.getMessageProperties())) { List<Object> payloads = new ArrayList<>(); AmqpInboundGateway.this.batchingStrategy.deBatch(message, fragment -> payloads .add(AmqpInboundGateway.this.amqpMessageConverter.fromMessage(fragment))); payload = payloads; } else { payload = AmqpInboundGateway.this.amqpMessageConverter.fromMessage(message); } headers = AmqpInboundGateway.this.headerMapper.toHeadersFromRequest(message.getMessageProperties()); if (isManualAck) { headers.put(AmqpHeaders.DELIVERY_TAG, message.getMessageProperties().getDeliveryTag()); headers.put(AmqpHeaders.CHANNEL, channel); } if (AmqpInboundGateway.this.retryTemplate != null) { headers.put(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT, new AtomicInteger()); } if (AmqpInboundGateway.this.bindSourceMessage) { headers.put(IntegrationMessageHeaderAccessor.SOURCE_DATA, message); } } catch (RuntimeException e) { MessageChannel errorChannel = getErrorChannel(); if (errorChannel != null) { setAttributesIfNecessary(message, null); AmqpInboundGateway.this.messagingTemplate.send(errorChannel, buildErrorMessage(null, EndpointUtils.errorMessagePayload(message, channel, isManualAck, e))); } else { throw e; } return null; } return getMessageBuilderFactory().withPayload(payload).copyHeaders(headers).build(); } private void process(Message message, org.springframework.messaging.Message<Object> messagingMessage) { setAttributesIfNecessary(message, messagingMessage); final org.springframework.messaging.Message<?> reply = sendAndReceiveMessage(messagingMessage); if (reply != null) { Address replyTo; String replyToProperty = message.getMessageProperties().getReplyTo(); if (replyToProperty != null) { replyTo = new Address(replyToProperty); } else { replyTo = AmqpInboundGateway.this.defaultReplyTo; } MessagePostProcessor messagePostProcessor = message1 -> { MessageProperties messageProperties = message1.getMessageProperties(); String contentEncoding = messageProperties.getContentEncoding(); long contentLength = messageProperties.getContentLength(); String contentType = messageProperties.getContentType(); AmqpInboundGateway.this.headerMapper.fromHeadersToReply(reply.getHeaders(), messageProperties); // clear the replyTo from the original message since we are using it now messageProperties.setReplyTo(null); // reset the content-* properties as determined by the MessageConverter if (StringUtils.hasText(contentEncoding)) { messageProperties.setContentEncoding(contentEncoding); } messageProperties.setContentLength(contentLength); if (contentType != null) { messageProperties.setContentType(contentType); } return message1; }; if (replyTo != null) { AmqpInboundGateway.this.amqpTemplate.convertAndSend(replyTo.getExchangeName(), replyTo.getRoutingKey(), reply.getPayload(), messagePostProcessor); } else { if (!AmqpInboundGateway.this.amqpTemplateExplicitlySet) { throw new IllegalStateException("There is no 'replyTo' message property " + "and the `defaultReplyTo` hasn't been configured."); } else { AmqpInboundGateway.this.amqpTemplate.convertAndSend(reply.getPayload(), messagePostProcessor); } } } } } }