/* * Copyright 2015-2017 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.kafka.inbound; import java.util.List; import org.apache.kafka.clients.consumer.Consumer; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.springframework.integration.context.OrderlyShutdownCapable; import org.springframework.integration.endpoint.MessageProducerSupport; import org.springframework.kafka.listener.AbstractMessageListenerContainer; import org.springframework.kafka.listener.BatchMessageListener; import org.springframework.kafka.listener.MessageListener; import org.springframework.kafka.listener.adapter.BatchMessagingMessageListenerAdapter; import org.springframework.kafka.listener.adapter.FilteringBatchMessageListenerAdapter; import org.springframework.kafka.listener.adapter.FilteringMessageListenerAdapter; import org.springframework.kafka.listener.adapter.RecordFilterStrategy; import org.springframework.kafka.listener.adapter.RecordMessagingMessageListenerAdapter; import org.springframework.kafka.listener.adapter.RetryingMessageListenerAdapter; import org.springframework.kafka.support.Acknowledgment; import org.springframework.kafka.support.converter.BatchMessageConverter; import org.springframework.kafka.support.converter.ConversionException; import org.springframework.kafka.support.converter.MessageConverter; import org.springframework.kafka.support.converter.RecordMessageConverter; import org.springframework.messaging.Message; import org.springframework.messaging.support.ErrorMessage; import org.springframework.retry.RecoveryCallback; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; /** * Message-driven channel adapter. * * @param <K> the key type. * @param <V> the value type. * * @author Marius Bogoevici * @author Gary Russell * @author Artem Bilan * */ public class KafkaMessageDrivenChannelAdapter<K, V> extends MessageProducerSupport implements OrderlyShutdownCapable { private final AbstractMessageListenerContainer<K, V> messageListenerContainer; private final RecordMessagingMessageListenerAdapter<K, V> recordListener = new IntegrationRecordMessageListener(); private final BatchMessagingMessageListenerAdapter<K, V> batchListener = new IntegrationBatchMessageListener(); private final ListenerMode mode; private RecordFilterStrategy<K, V> recordFilterStrategy; private boolean ackDiscarded; private RetryTemplate retryTemplate; private RecoveryCallback<? extends Object> recoveryCallback; private boolean filterInRetry; /** * Construct an instance with mode {@link ListenerMode#record}. * @param messageListenerContainer the container. */ public KafkaMessageDrivenChannelAdapter(AbstractMessageListenerContainer<K, V> messageListenerContainer) { this(messageListenerContainer, ListenerMode.record); } /** * Construct an instance with the provided mode. * @param messageListenerContainer the container. * @param mode the mode. * @since 1.2 */ public KafkaMessageDrivenChannelAdapter(AbstractMessageListenerContainer<K, V> messageListenerContainer, ListenerMode mode) { Assert.notNull(messageListenerContainer, "messageListenerContainer is required"); Assert.isNull(messageListenerContainer.getContainerProperties().getMessageListener(), "Container must not already have a listener"); this.messageListenerContainer = messageListenerContainer; this.messageListenerContainer.setAutoStartup(false); this.mode = mode; } /** * Set the message converter; must be a {@link RecordMessageConverter} or * {@link BatchMessageConverter} depending on mode. * @param messageConverter the converter. */ public void setMessageConverter(MessageConverter messageConverter) { if (messageConverter instanceof RecordMessageConverter) { this.recordListener.setMessageConverter((RecordMessageConverter) messageConverter); } else if (messageConverter instanceof BatchMessageConverter) { this.batchListener.setBatchMessageConverter((BatchMessageConverter) messageConverter); } else { throw new IllegalArgumentException( "Message converter must be a 'RecordMessageConverter' or 'BatchMessageConverter'"); } } /** * Set the message converter to use with a record-based consumer. * @param messageConverter the converter. * @since 2.1 */ public void setRecordMessageConverter(RecordMessageConverter messageConverter) { this.recordListener.setMessageConverter(messageConverter); } /** * Set the message converter to use with a batch-based consumer. * @param messageConverter the converter. * @since 2.1 */ public void setBatchMessageConverter(BatchMessageConverter messageConverter) { this.batchListener.setBatchMessageConverter(messageConverter); } /** * Specify a {@link RecordFilterStrategy} to wrap * {@link KafkaMessageDrivenChannelAdapter.IntegrationRecordMessageListener} into * {@link FilteringMessageListenerAdapter}. * @param recordFilterStrategy the {@link RecordFilterStrategy} to use. * @since 2.0.1 */ public void setRecordFilterStrategy(RecordFilterStrategy<K, V> recordFilterStrategy) { this.recordFilterStrategy = recordFilterStrategy; } /** * A {@code boolean} flag to indicate if {@link FilteringMessageListenerAdapter} * should acknowledge discarded records or not. * Does not make sense if {@link #setRecordFilterStrategy(RecordFilterStrategy)} isn't specified. * @param ackDiscarded true to ack (commit offset for) discarded messages. * @since 2.0.1 */ public void setAckDiscarded(boolean ackDiscarded) { this.ackDiscarded = ackDiscarded; } /** * Specify a {@link RetryTemplate} instance to wrap * {@link KafkaMessageDrivenChannelAdapter.IntegrationRecordMessageListener} into * {@link RetryingMessageListenerAdapter}. * @param retryTemplate the {@link RetryTemplate} to use. * @since 2.0.1 */ public void setRetryTemplate(RetryTemplate retryTemplate) { Assert.isTrue(retryTemplate == null || this.mode.equals(ListenerMode.record), "Retry is not supported with mode=batch"); this.retryTemplate = retryTemplate; } /** * A {@link RecoveryCallback} instance for retry operation; * if null, the exception will be thrown to the container after retries are exhausted. * Does not make sense if {@link #setRetryTemplate(RetryTemplate)} isn't specified. * @param recoveryCallback the recovery callback. * @since 2.0.1 */ public void setRecoveryCallback(RecoveryCallback<? extends Object> recoveryCallback) { this.recoveryCallback = recoveryCallback; } /** * The {@code boolean} flag to specify the order how * {@link RetryingMessageListenerAdapter} and * {@link FilteringMessageListenerAdapter} are wrapped to each other, * if both of them are present. * Does not make sense if only one of {@link RetryTemplate} or * {@link RecordFilterStrategy} is present, or any. * @param filterInRetry the order for {@link RetryingMessageListenerAdapter} and * {@link FilteringMessageListenerAdapter} wrapping. Defaults to {@code false}. * @since 2.0.1 */ public void setFilterInRetry(boolean filterInRetry) { this.filterInRetry = filterInRetry; } /** * When using a type-aware message converter (such as {@code StringJsonMessageConverter}, * set the payload type the converter should create. Defaults to {@link Object}. * @param payloadType the type. * @since 2.1.1 */ public void setPayloadType(Class<?> payloadType) { this.recordListener.setFallbackType(payloadType); this.batchListener.setFallbackType(payloadType); } @Override protected void onInit() { super.onInit(); if (this.mode.equals(ListenerMode.record)) { MessageListener<K, V> listener = this.recordListener; boolean filterInRetry = this.filterInRetry && this.retryTemplate != null && this.recordFilterStrategy != null; if (filterInRetry) { listener = new FilteringMessageListenerAdapter<>(listener, this.recordFilterStrategy, this.ackDiscarded); listener = new RetryingMessageListenerAdapter<>(listener, this.retryTemplate, this.recoveryCallback); } else { if (this.retryTemplate != null) { listener = new RetryingMessageListenerAdapter<>(listener, this.retryTemplate, this.recoveryCallback); } if (this.recordFilterStrategy != null) { listener = new FilteringMessageListenerAdapter<>(listener, this.recordFilterStrategy, this.ackDiscarded); } } this.messageListenerContainer.getContainerProperties().setMessageListener(listener); } else { BatchMessageListener<K, V> listener = this.batchListener; if (this.recordFilterStrategy != null) { listener = new FilteringBatchMessageListenerAdapter<>(listener, this.recordFilterStrategy, this.ackDiscarded); } this.messageListenerContainer.getContainerProperties().setMessageListener(listener); } } @Override protected void doStart() { this.messageListenerContainer.start(); } @Override protected void doStop() { this.messageListenerContainer.stop(); } @Override public String getComponentType() { return "kafka:message-driven-channel-adapter"; } @Override public int beforeShutdown() { this.messageListenerContainer.stop(); return getPhase(); } @Override public int afterShutdown() { return getPhase(); } /** * The listener mode for the container, record or batch. * @since 1.2 * */ public enum ListenerMode { /** * Each {@link Message} will be converted from a single {@code ConsumerRecord}. */ record, /** * Each {@link Message} will be converted from the {@code ConsumerRecords} * returned by a poll. */ batch } private class IntegrationRecordMessageListener extends RecordMessagingMessageListenerAdapter<K, V> { IntegrationRecordMessageListener() { super(null, null); } @Override public void onMessage(ConsumerRecord<K, V> record, Acknowledgment acknowledgment, Consumer<?, ?> consumer) { Message<?> message = null; try { message = toMessagingMessage(record, acknowledgment, consumer); } catch (RuntimeException e) { Exception exception = new ConversionException("Failed to convert to message for: " + record, e); if (getErrorChannel() != null) { getMessagingTemplate().send(getErrorChannel(), new ErrorMessage(exception)); } } if (message != null) { sendMessage(message); } else { KafkaMessageDrivenChannelAdapter.this.logger.debug("Converter returned a null message for: " + record); } } } private class IntegrationBatchMessageListener extends BatchMessagingMessageListenerAdapter<K, V> { IntegrationBatchMessageListener() { super(null, null); } @Override public void onMessage(List<ConsumerRecord<K, V>> records, Acknowledgment acknowledgment, Consumer<?, ?> consumer) { Message<?> message = null; try { message = toMessagingMessage(records, acknowledgment, consumer); } catch (RuntimeException e) { Exception exception = new ConversionException("Failed to convert to message for: " + records, e); if (getErrorChannel() != null) { getMessagingTemplate().send(getErrorChannel(), new ErrorMessage(exception)); } } if (message != null) { sendMessage(message); } else { KafkaMessageDrivenChannelAdapter.this.logger.debug("Converter returned a null message for: " + records); } } } }