GH-1118: Add RecordInterceptor

Resolves https://github.com/spring-projects/spring-kafka/issues/1118

**cherry-pick to 2.2.x**

# Conflicts:
#	spring-kafka/src/main/java/org/springframework/kafka/config/AbstractKafkaListenerContainerFactory.java
#	spring-kafka/src/main/java/org/springframework/kafka/listener/AbstractMessageListenerContainer.java
#	spring-kafka/src/main/java/org/springframework/kafka/listener/KafkaMessageListenerContainer.java
#	src/reference/asciidoc/whats-new.adoc
This commit is contained in:
Gary Russell
2019-06-11 15:36:11 -04:00
committed by Artem Bilan
parent d80ade57f7
commit e487ece915
8 changed files with 134 additions and 24 deletions

View File

@@ -36,6 +36,7 @@ import org.springframework.kafka.listener.BatchErrorHandler;
import org.springframework.kafka.listener.ContainerProperties;
import org.springframework.kafka.listener.ErrorHandler;
import org.springframework.kafka.listener.GenericErrorHandler;
import org.springframework.kafka.listener.RecordInterceptor;
import org.springframework.kafka.listener.adapter.RecordFilterStrategy;
import org.springframework.kafka.listener.adapter.ReplyHeadersConfigurer;
import org.springframework.kafka.requestreply.ReplyingKafkaOperations;
@@ -95,6 +96,8 @@ public abstract class AbstractKafkaListenerContainerFactory<C extends AbstractMe
private ReplyHeadersConfigurer replyHeadersConfigurer;
private RecordInterceptor<K, V> recordInterceptor;
/**
* Specify a {@link ConsumerFactory} to use.
* @param consumerFactory The consumer factory.
@@ -266,6 +269,16 @@ public abstract class AbstractKafkaListenerContainerFactory<C extends AbstractMe
return this.containerProperties;
}
/**
* Set an interceptor to be called before calling the listener.
* Does not apply to batch listeners.
* @param recordInterceptor the interceptor.
* @since 2.2.7
*/
public void setRecordInterceptor(RecordInterceptor<K, V> recordInterceptor) {
this.recordInterceptor = recordInterceptor;
}
@Override
public void afterPropertiesSet() {
if (this.errorHandler != null) {
@@ -363,6 +376,7 @@ public abstract class AbstractKafkaListenerContainerFactory<C extends AbstractMe
else if (this.autoStartup != null) {
instance.setAutoStartup(this.autoStartup);
}
instance.setRecordInterceptor(this.recordInterceptor);
if (this.phase != null) {
instance.setPhase(this.phase);
}

View File

@@ -81,6 +81,8 @@ public abstract class AbstractMessageListenerContainer<K, V>
private AfterRollbackProcessor<? super K, ? super V> afterRollbackProcessor =
new DefaultAfterRollbackProcessor<>();
private RecordInterceptor<K, V> recordInterceptor;
private volatile boolean running = false;
private volatile boolean paused;
@@ -261,6 +263,20 @@ public abstract class AbstractMessageListenerContainer<K, V>
return this.beanName; // the container factory sets the bean name to the id attribute
}
protected RecordInterceptor<K, V> getRecordInterceptor() {
return this.recordInterceptor;
}
/**
* Set an interceptor to be called before calling the listener.
* Does not apply to batch listeners.
* @param recordInterceptor the interceptor.
* @since 2.2.7
*/
public void setRecordInterceptor(RecordInterceptor<K, V> recordInterceptor) {
this.recordInterceptor = recordInterceptor;
}
@Override
public void setupMessageListener(Object messageListener) {
this.containerProperties.setMessageListener(messageListener);

View File

@@ -161,6 +161,7 @@ public class ConcurrentMessageListenerContainer<K, V> extends AbstractMessageLis
container.setClientIdSuffix("-" + i);
container.setGenericErrorHandler(getGenericErrorHandler());
container.setAfterRollbackProcessor(getAfterRollbackProcessor());
container.setRecordInterceptor(getRecordInterceptor());
container.setEmergencyStop(() -> {
stop(() -> {
// NOSONAR

View File

@@ -465,6 +465,8 @@ public class KafkaMessageListenerContainer<K, V> // NOSONAR comment density
private final boolean checkNullValueForExceptions;
private final RecordInterceptor<K, V> recordInterceptor = getRecordInterceptor();
private Map<TopicPartition, OffsetMetadata> definedPartitions;
private volatile Collection<TopicPartition> assignedPartitions;
@@ -1257,26 +1259,37 @@ public class KafkaMessageListenerContainer<K, V> // NOSONAR comment density
ackCurrent(record, producer);
}
private void doInvokeOnMessage(final ConsumerRecord<K, V> record) {
switch (this.listenerType) {
case ACKNOWLEDGING_CONSUMER_AWARE:
this.listener.onMessage(record,
this.isAnyManualAck
? new ConsumerAcknowledgment(record)
: null, this.consumer);
break;
case CONSUMER_AWARE:
this.listener.onMessage(record, this.consumer);
break;
case ACKNOWLEDGING:
this.listener.onMessage(record,
this.isAnyManualAck
? new ConsumerAcknowledgment(record)
: null);
break;
case SIMPLE:
this.listener.onMessage(record);
break;
private void doInvokeOnMessage(final ConsumerRecord<K, V> recordArg) {
ConsumerRecord<K, V> record = recordArg;
if (this.recordInterceptor != null) {
record = this.recordInterceptor.intercept(record);
}
if (record == null) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("RecordInterceptor returned null, skipping: " + recordArg);
}
}
else {
switch (this.listenerType) {
case ACKNOWLEDGING_CONSUMER_AWARE:
this.listener.onMessage(record,
this.isAnyManualAck
? new ConsumerAcknowledgment(record)
: null, this.consumer);
break;
case CONSUMER_AWARE:
this.listener.onMessage(record, this.consumer);
break;
case ACKNOWLEDGING:
this.listener.onMessage(record,
this.isAnyManualAck
? new ConsumerAcknowledgment(record)
: null);
break;
case SIMPLE:
this.listener.onMessage(record);
break;
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 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.kafka.listener;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.lang.Nullable;
/**
* An interceptor for {@link ConsumerRecord} invoked by the listener
* container before invoking the listener.
*
* @param <K> the key type.
* @param <V> the value type.
*
* @author Gary Russell
* @since 2.2.7
*
*/
@FunctionalInterface
public interface RecordInterceptor<K, V> {
/**
* Perform some action on the record or return a different one.
* If null is returned the record will be skipped.
* @param record the record.
* @return the record or null.
*/
@Nullable
ConsumerRecord<K, V> intercept(ConsumerRecord<K, V> record);
}

View File

@@ -740,6 +740,7 @@ public class EnableKafkaIntegrationTests {
this.bytesKeyTemplate.send("annotated36", "foo".getBytes(), "bar");
assertThat(this.listener.keyLatch.await(30, TimeUnit.SECONDS)).isTrue();
assertThat(this.listener.convertedKey).isEqualTo("foo");
assertThat(this.config.intercepted).isTrue();
}
@Configuration
@@ -747,7 +748,11 @@ public class EnableKafkaIntegrationTests {
@EnableTransactionManagement(proxyTargetClass = true)
public static class Config implements KafkaListenerConfigurer {
private final CountDownLatch spyLatch = new CountDownLatch(2);
final CountDownLatch spyLatch = new CountDownLatch(2);
volatile Throwable globalErrorThrowable;
volatile boolean intercepted;
@Bean
public static PropertySourcesPlaceholderConfigurer ppc() {
@@ -770,8 +775,6 @@ public class EnableKafkaIntegrationTests {
return new ChainedKafkaTransactionManager<>(ktm(), transactionManager());
}
private Throwable globalErrorThrowable;
@Bean
public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Integer, String>>
kafkaListenerContainerFactory() {
@@ -857,6 +860,10 @@ public class EnableKafkaIntegrationTests {
ConcurrentKafkaListenerContainerFactory<byte[], String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(bytesStringConsumerFactory());
factory.setRecordInterceptor(record -> {
this.intercepted = true;
return record;
});
return factory;
}

View File

@@ -112,11 +112,13 @@ public class ConcurrentMessageListenerContainerTests {
ContainerProperties containerProps = new ContainerProperties(topic1);
containerProps.setLogContainerConfig(true);
final CountDownLatch latch = new CountDownLatch(4);
final CountDownLatch latch = new CountDownLatch(3);
final Set<String> listenerThreadNames = new ConcurrentSkipListSet<>();
final List<String> payloads = new ArrayList<>();
containerProps.setMessageListener((MessageListener<Integer, String>) message -> {
ConcurrentMessageListenerContainerTests.this.logger.info("auto: " + message);
listenerThreadNames.add(Thread.currentThread().getName());
payloads.add(message.value());
latch.countDown();
});
@@ -132,6 +134,11 @@ public class ConcurrentMessageListenerContainerTests {
stopLatch.countDown();
}
});
CountDownLatch intercepted = new CountDownLatch(4);
container.setRecordInterceptor(record -> {
intercepted.countDown();
return record.value().equals("baz") ? null : record;
});
container.start();
ContainerTestUtils.waitForAssignment(container, embeddedKafka.getPartitionsPerTopic());
@@ -146,6 +153,7 @@ public class ConcurrentMessageListenerContainerTests {
template.sendDefault(0, "baz");
template.sendDefault(2, "qux");
template.flush();
assertThat(intercepted.await(10, TimeUnit.SECONDS)).isTrue();
assertThat(latch.await(60, TimeUnit.SECONDS)).isTrue();
for (String threadName : listenerThreadNames) {
assertThat(threadName).contains("-C-");
@@ -161,6 +169,7 @@ public class ConcurrentMessageListenerContainerTests {
Set<KafkaMessageListenerContainer<Integer, String>> children = new HashSet<>(containers);
container.stop();
assertThat(stopLatch.await(10, TimeUnit.SECONDS)).isTrue();
assertThat(payloads).containsExactlyInAnyOrder("foo", "bar", "qux");
events.forEach(e -> {
assertThat(e.getContainer(MessageListenerContainer.class)).isSameAs(container);
if (e instanceof ContainerStoppedEvent) {

View File

@@ -642,6 +642,10 @@ Two `MessageListenerContainer` implementations are provided:
The `KafkaMessageListenerContainer` receives all message from all topics or partitions on a single thread.
The `ConcurrentMessageListenerContainer` delegates to one or more `KafkaMessageListenerContainer` instances to provide multi-threaded consumption.
Starting with version 2.1.7, you can add a `RecordInterceptor` to the listener container; it will be invoked before calling the listener allowing inspection or modification of the record.
If the interceptor returns null, the listener is not called.
The interceptor is not invoked when the listener is a <<batch-listners, batch listener>>.
[[kafka-container]]
====== Using `KafkaMessageListenerContainer`