Skip to content

Commit e487ece

Browse files
garyrussellartembilan
authored andcommitted
GH-1118: Add RecordInterceptor
Resolves #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
1 parent d80ade5 commit e487ece

File tree

8 files changed

+134
-24
lines changed

8 files changed

+134
-24
lines changed

spring-kafka/src/main/java/org/springframework/kafka/config/AbstractKafkaListenerContainerFactory.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.springframework.kafka.listener.ContainerProperties;
3737
import org.springframework.kafka.listener.ErrorHandler;
3838
import org.springframework.kafka.listener.GenericErrorHandler;
39+
import org.springframework.kafka.listener.RecordInterceptor;
3940
import org.springframework.kafka.listener.adapter.RecordFilterStrategy;
4041
import org.springframework.kafka.listener.adapter.ReplyHeadersConfigurer;
4142
import org.springframework.kafka.requestreply.ReplyingKafkaOperations;
@@ -95,6 +96,8 @@ public abstract class AbstractKafkaListenerContainerFactory<C extends AbstractMe
9596

9697
private ReplyHeadersConfigurer replyHeadersConfigurer;
9798

99+
private RecordInterceptor<K, V> recordInterceptor;
100+
98101
/**
99102
* Specify a {@link ConsumerFactory} to use.
100103
* @param consumerFactory The consumer factory.
@@ -266,6 +269,16 @@ public ContainerProperties getContainerProperties() {
266269
return this.containerProperties;
267270
}
268271

272+
/**
273+
* Set an interceptor to be called before calling the listener.
274+
* Does not apply to batch listeners.
275+
* @param recordInterceptor the interceptor.
276+
* @since 2.2.7
277+
*/
278+
public void setRecordInterceptor(RecordInterceptor<K, V> recordInterceptor) {
279+
this.recordInterceptor = recordInterceptor;
280+
}
281+
269282
@Override
270283
public void afterPropertiesSet() {
271284
if (this.errorHandler != null) {
@@ -363,6 +376,7 @@ protected void initializeContainer(C instance, KafkaListenerEndpoint endpoint) {
363376
else if (this.autoStartup != null) {
364377
instance.setAutoStartup(this.autoStartup);
365378
}
379+
instance.setRecordInterceptor(this.recordInterceptor);
366380
if (this.phase != null) {
367381
instance.setPhase(this.phase);
368382
}

spring-kafka/src/main/java/org/springframework/kafka/listener/AbstractMessageListenerContainer.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ public abstract class AbstractMessageListenerContainer<K, V>
8181
private AfterRollbackProcessor<? super K, ? super V> afterRollbackProcessor =
8282
new DefaultAfterRollbackProcessor<>();
8383

84+
private RecordInterceptor<K, V> recordInterceptor;
85+
8486
private volatile boolean running = false;
8587

8688
private volatile boolean paused;
@@ -261,6 +263,20 @@ public String getListenerId() {
261263
return this.beanName; // the container factory sets the bean name to the id attribute
262264
}
263265

266+
protected RecordInterceptor<K, V> getRecordInterceptor() {
267+
return this.recordInterceptor;
268+
}
269+
270+
/**
271+
* Set an interceptor to be called before calling the listener.
272+
* Does not apply to batch listeners.
273+
* @param recordInterceptor the interceptor.
274+
* @since 2.2.7
275+
*/
276+
public void setRecordInterceptor(RecordInterceptor<K, V> recordInterceptor) {
277+
this.recordInterceptor = recordInterceptor;
278+
}
279+
264280
@Override
265281
public void setupMessageListener(Object messageListener) {
266282
this.containerProperties.setMessageListener(messageListener);

spring-kafka/src/main/java/org/springframework/kafka/listener/ConcurrentMessageListenerContainer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ protected void doStart() {
161161
container.setClientIdSuffix("-" + i);
162162
container.setGenericErrorHandler(getGenericErrorHandler());
163163
container.setAfterRollbackProcessor(getAfterRollbackProcessor());
164+
container.setRecordInterceptor(getRecordInterceptor());
164165
container.setEmergencyStop(() -> {
165166
stop(() -> {
166167
// NOSONAR

spring-kafka/src/main/java/org/springframework/kafka/listener/KafkaMessageListenerContainer.java

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,8 @@ private final class ListenerConsumer implements SchedulingAwareRunnable, Consume
465465

466466
private final boolean checkNullValueForExceptions;
467467

468+
private final RecordInterceptor<K, V> recordInterceptor = getRecordInterceptor();
469+
468470
private Map<TopicPartition, OffsetMetadata> definedPartitions;
469471

470472
private volatile Collection<TopicPartition> assignedPartitions;
@@ -1257,26 +1259,37 @@ private void invokeOnMessage(final ConsumerRecord<K, V> record,
12571259
ackCurrent(record, producer);
12581260
}
12591261

1260-
private void doInvokeOnMessage(final ConsumerRecord<K, V> record) {
1261-
switch (this.listenerType) {
1262-
case ACKNOWLEDGING_CONSUMER_AWARE:
1263-
this.listener.onMessage(record,
1264-
this.isAnyManualAck
1265-
? new ConsumerAcknowledgment(record)
1266-
: null, this.consumer);
1267-
break;
1268-
case CONSUMER_AWARE:
1269-
this.listener.onMessage(record, this.consumer);
1270-
break;
1271-
case ACKNOWLEDGING:
1272-
this.listener.onMessage(record,
1273-
this.isAnyManualAck
1274-
? new ConsumerAcknowledgment(record)
1275-
: null);
1276-
break;
1277-
case SIMPLE:
1278-
this.listener.onMessage(record);
1279-
break;
1262+
private void doInvokeOnMessage(final ConsumerRecord<K, V> recordArg) {
1263+
ConsumerRecord<K, V> record = recordArg;
1264+
if (this.recordInterceptor != null) {
1265+
record = this.recordInterceptor.intercept(record);
1266+
}
1267+
if (record == null) {
1268+
if (this.logger.isDebugEnabled()) {
1269+
this.logger.debug("RecordInterceptor returned null, skipping: " + recordArg);
1270+
}
1271+
}
1272+
else {
1273+
switch (this.listenerType) {
1274+
case ACKNOWLEDGING_CONSUMER_AWARE:
1275+
this.listener.onMessage(record,
1276+
this.isAnyManualAck
1277+
? new ConsumerAcknowledgment(record)
1278+
: null, this.consumer);
1279+
break;
1280+
case CONSUMER_AWARE:
1281+
this.listener.onMessage(record, this.consumer);
1282+
break;
1283+
case ACKNOWLEDGING:
1284+
this.listener.onMessage(record,
1285+
this.isAnyManualAck
1286+
? new ConsumerAcknowledgment(record)
1287+
: null);
1288+
break;
1289+
case SIMPLE:
1290+
this.listener.onMessage(record);
1291+
break;
1292+
}
12801293
}
12811294
}
12821295

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.kafka.listener;
18+
19+
import org.apache.kafka.clients.consumer.ConsumerRecord;
20+
21+
import org.springframework.lang.Nullable;
22+
23+
/**
24+
* An interceptor for {@link ConsumerRecord} invoked by the listener
25+
* container before invoking the listener.
26+
*
27+
* @param <K> the key type.
28+
* @param <V> the value type.
29+
*
30+
* @author Gary Russell
31+
* @since 2.2.7
32+
*
33+
*/
34+
@FunctionalInterface
35+
public interface RecordInterceptor<K, V> {
36+
37+
/**
38+
* Perform some action on the record or return a different one.
39+
* If null is returned the record will be skipped.
40+
* @param record the record.
41+
* @return the record or null.
42+
*/
43+
@Nullable
44+
ConsumerRecord<K, V> intercept(ConsumerRecord<K, V> record);
45+
46+
}

spring-kafka/src/test/java/org/springframework/kafka/annotation/EnableKafkaIntegrationTests.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -740,14 +740,19 @@ public void testKeyConversion() throws Exception {
740740
this.bytesKeyTemplate.send("annotated36", "foo".getBytes(), "bar");
741741
assertThat(this.listener.keyLatch.await(30, TimeUnit.SECONDS)).isTrue();
742742
assertThat(this.listener.convertedKey).isEqualTo("foo");
743+
assertThat(this.config.intercepted).isTrue();
743744
}
744745

745746
@Configuration
746747
@EnableKafka
747748
@EnableTransactionManagement(proxyTargetClass = true)
748749
public static class Config implements KafkaListenerConfigurer {
749750

750-
private final CountDownLatch spyLatch = new CountDownLatch(2);
751+
final CountDownLatch spyLatch = new CountDownLatch(2);
752+
753+
volatile Throwable globalErrorThrowable;
754+
755+
volatile boolean intercepted;
751756

752757
@Bean
753758
public static PropertySourcesPlaceholderConfigurer ppc() {
@@ -770,8 +775,6 @@ public ChainedKafkaTransactionManager<Integer, String> cktm() {
770775
return new ChainedKafkaTransactionManager<>(ktm(), transactionManager());
771776
}
772777

773-
private Throwable globalErrorThrowable;
774-
775778
@Bean
776779
public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Integer, String>>
777780
kafkaListenerContainerFactory() {
@@ -857,6 +860,10 @@ public KafkaListenerContainerFactory<?> bytesStringListenerContainerFactory() {
857860
ConcurrentKafkaListenerContainerFactory<byte[], String> factory =
858861
new ConcurrentKafkaListenerContainerFactory<>();
859862
factory.setConsumerFactory(bytesStringConsumerFactory());
863+
factory.setRecordInterceptor(record -> {
864+
this.intercepted = true;
865+
return record;
866+
});
860867
return factory;
861868
}
862869

spring-kafka/src/test/java/org/springframework/kafka/listener/ConcurrentMessageListenerContainerTests.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,13 @@ public void testAutoCommit() throws Exception {
112112
ContainerProperties containerProps = new ContainerProperties(topic1);
113113
containerProps.setLogContainerConfig(true);
114114

115-
final CountDownLatch latch = new CountDownLatch(4);
115+
final CountDownLatch latch = new CountDownLatch(3);
116116
final Set<String> listenerThreadNames = new ConcurrentSkipListSet<>();
117+
final List<String> payloads = new ArrayList<>();
117118
containerProps.setMessageListener((MessageListener<Integer, String>) message -> {
118119
ConcurrentMessageListenerContainerTests.this.logger.info("auto: " + message);
119120
listenerThreadNames.add(Thread.currentThread().getName());
121+
payloads.add(message.value());
120122
latch.countDown();
121123
});
122124

@@ -132,6 +134,11 @@ public void testAutoCommit() throws Exception {
132134
stopLatch.countDown();
133135
}
134136
});
137+
CountDownLatch intercepted = new CountDownLatch(4);
138+
container.setRecordInterceptor(record -> {
139+
intercepted.countDown();
140+
return record.value().equals("baz") ? null : record;
141+
});
135142
container.start();
136143

137144
ContainerTestUtils.waitForAssignment(container, embeddedKafka.getPartitionsPerTopic());
@@ -146,6 +153,7 @@ public void testAutoCommit() throws Exception {
146153
template.sendDefault(0, "baz");
147154
template.sendDefault(2, "qux");
148155
template.flush();
156+
assertThat(intercepted.await(10, TimeUnit.SECONDS)).isTrue();
149157
assertThat(latch.await(60, TimeUnit.SECONDS)).isTrue();
150158
for (String threadName : listenerThreadNames) {
151159
assertThat(threadName).contains("-C-");
@@ -161,6 +169,7 @@ public void testAutoCommit() throws Exception {
161169
Set<KafkaMessageListenerContainer<Integer, String>> children = new HashSet<>(containers);
162170
container.stop();
163171
assertThat(stopLatch.await(10, TimeUnit.SECONDS)).isTrue();
172+
assertThat(payloads).containsExactlyInAnyOrder("foo", "bar", "qux");
164173
events.forEach(e -> {
165174
assertThat(e.getContainer(MessageListenerContainer.class)).isSameAs(container);
166175
if (e instanceof ContainerStoppedEvent) {

src/reference/asciidoc/kafka.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,10 @@ Two `MessageListenerContainer` implementations are provided:
642642
The `KafkaMessageListenerContainer` receives all message from all topics or partitions on a single thread.
643643
The `ConcurrentMessageListenerContainer` delegates to one or more `KafkaMessageListenerContainer` instances to provide multi-threaded consumption.
644644

645+
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.
646+
If the interceptor returns null, the listener is not called.
647+
The interceptor is not invoked when the listener is a <<batch-listners, batch listener>>.
648+
645649
[[kafka-container]]
646650
====== Using `KafkaMessageListenerContainer`
647651

0 commit comments

Comments
 (0)