Configure multiple listeners on same topic (#13572)
* moved Spring retryable article to own package * moved Spring retryable article to own package * implement multiple listeners kafka app demo project * fix PR comments
This commit is contained in:
parent
a68763ba79
commit
f40d10a9e1
|
@ -0,0 +1,26 @@
|
|||
package com.baeldung.spring.kafka.multiplelisteners;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.kafka.annotation.KafkaListener;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class BookConsumer {
|
||||
private static final Logger logger = LoggerFactory.getLogger(BookConsumer.class);
|
||||
|
||||
@KafkaListener(topics = "books", groupId = "books-content-search")
|
||||
public void bookContentSearchConsumer(BookEvent event) {
|
||||
logger.info("Books event received for full-text search indexing => {}", event);
|
||||
}
|
||||
|
||||
@KafkaListener(topics = "books", groupId = "books-price-index")
|
||||
public void bookPriceIndexerConsumer(BookEvent event) {
|
||||
logger.info("Books event received for price indexing => {}", event);
|
||||
}
|
||||
|
||||
@KafkaListener(topics = "books", groupId = "book-notification-consumer", concurrency = "2")
|
||||
public void bookNotificationConsumer(BookEvent event) {
|
||||
logger.info("Books event received for notification => {}", event);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package com.baeldung.spring.kafka.multiplelisteners;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class BookEvent {
|
||||
|
||||
private String title;
|
||||
private String description;
|
||||
private Double price;
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package com.baeldung.spring.kafka.multiplelisteners;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.kafka.clients.consumer.ConsumerConfig;
|
||||
import org.apache.kafka.common.serialization.StringDeserializer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.kafka.annotation.EnableKafka;
|
||||
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
|
||||
import org.springframework.kafka.core.ConsumerFactory;
|
||||
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
|
||||
import org.springframework.kafka.listener.DefaultErrorHandler;
|
||||
import org.springframework.kafka.support.serializer.JsonDeserializer;
|
||||
import org.springframework.util.backoff.BackOff;
|
||||
import org.springframework.util.backoff.FixedBackOff;
|
||||
|
||||
@EnableKafka
|
||||
@Configuration
|
||||
public class KafkaConsumerConfig {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(KafkaConsumerConfig.class);
|
||||
|
||||
@Value(value = "${spring.kafka.bootstrap-servers}")
|
||||
private String bootstrapAddress;
|
||||
|
||||
@Value(value = "${kafka.backoff.interval}")
|
||||
private Long interval;
|
||||
|
||||
@Value(value = "${kafka.backoff.max_failure}")
|
||||
private Long maxAttempts;
|
||||
|
||||
public ConsumerFactory<String, BookEvent> consumerFactory() {
|
||||
Map<String, Object> props = new HashMap<>();
|
||||
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
|
||||
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
|
||||
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
|
||||
props.put(ConsumerConfig.MAX_PARTITION_FETCH_BYTES_CONFIG, "20971520");
|
||||
props.put(ConsumerConfig.FETCH_MAX_BYTES_CONFIG, "20971520");
|
||||
props.put(JsonDeserializer.TRUSTED_PACKAGES, "*");
|
||||
props.put(JsonDeserializer.TYPE_MAPPINGS, "bookEvent:com.baeldung.spring.kafka.multiplelisteners.BookEvent");
|
||||
|
||||
return new DefaultKafkaConsumerFactory<>(props);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ConcurrentKafkaListenerContainerFactory<String, BookEvent> kafkaListenerContainerFactory() {
|
||||
ConcurrentKafkaListenerContainerFactory<String, BookEvent> factory = new ConcurrentKafkaListenerContainerFactory<>();
|
||||
factory.setConsumerFactory(consumerFactory());
|
||||
factory.setCommonErrorHandler(errorHandler());
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DefaultErrorHandler errorHandler() {
|
||||
BackOff fixedBackOff = new FixedBackOff(interval, maxAttempts);
|
||||
return new DefaultErrorHandler((consumerRecord, e) -> LOGGER.error(String.format("consumed record %s because this exception was thrown", consumerRecord.toString())), fixedBackOff);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package com.baeldung.spring.kafka.multiplelisteners;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.kafka.clients.producer.ProducerConfig;
|
||||
import org.apache.kafka.common.serialization.StringSerializer;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
import org.springframework.kafka.core.ProducerFactory;
|
||||
import org.springframework.kafka.support.serializer.JsonSerializer;
|
||||
|
||||
@Configuration
|
||||
public class KafkaProducerConfig {
|
||||
|
||||
@Value(value = "${spring.kafka.bootstrap-servers}")
|
||||
private String bootstrapAddress;
|
||||
|
||||
@Bean
|
||||
public ProducerFactory<String, String> producerFactory() {
|
||||
Map<String, Object> configProps = new HashMap<>();
|
||||
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
|
||||
configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
|
||||
configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
|
||||
configProps.put(ProducerConfig.MAX_REQUEST_SIZE_CONFIG, "20971520");
|
||||
|
||||
return new DefaultKafkaProducerFactory<>(configProps);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public KafkaTemplate<String, String> kafkaTemplate() {
|
||||
return new KafkaTemplate<>(producerFactory());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ProducerFactory<String, BookEvent> bookProducerFactory() {
|
||||
Map<String, Object> configProps = new HashMap<>();
|
||||
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
|
||||
configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
|
||||
configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
|
||||
configProps.put(JsonSerializer.TYPE_MAPPINGS, "bookEvent:com.baeldung.spring.kafka.multiplelisteners.BookEvent");
|
||||
|
||||
return new DefaultKafkaProducerFactory<>(configProps);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public KafkaTemplate<String, BookEvent> bookKafkaTemplate() {
|
||||
return new KafkaTemplate<>(bookProducerFactory());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package com.baeldung.spring.kafka.multiplelisteners;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.kafka.clients.admin.AdminClientConfig;
|
||||
import org.apache.kafka.clients.admin.NewTopic;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.kafka.core.KafkaAdmin;
|
||||
|
||||
@Configuration
|
||||
public class KafkaTopicConfig {
|
||||
|
||||
@Value(value = "${spring.kafka.bootstrap-servers}")
|
||||
private String bootstrapAddress;
|
||||
|
||||
@Value(value = "${multiple-listeners.books.topic.name}")
|
||||
private String booksTopicName;
|
||||
|
||||
@Bean
|
||||
public KafkaAdmin kafkaAdmin() {
|
||||
Map<String, Object> configs = new HashMap<>();
|
||||
configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
|
||||
return new KafkaAdmin(configs);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public NewTopic booksTopic() {
|
||||
return new NewTopic(booksTopicName, 1, (short) 1);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package com.baeldung.spring.kafka.multiplelisteners;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
@SpringBootApplication
|
||||
@Import(value = { KafkaTopicConfig.class, KafkaConsumerConfig.class, KafkaProducerConfig.class })
|
||||
public class MultipleListenersApplicationKafkaApp {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MultipleListenersApplicationKafkaApp.class, args);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.baeldung.spring.kafka;
|
||||
package com.baeldung.spring.kafka.retryable;
|
||||
|
||||
public class Farewell {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.baeldung.spring.kafka;
|
||||
package com.baeldung.spring.kafka.retryable;
|
||||
|
||||
public class Greeting {
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.baeldung.spring.kafka;
|
||||
package com.baeldung.spring.kafka.retryable;
|
||||
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.baeldung.spring.kafka;
|
||||
package com.baeldung.spring.kafka.retryable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
@ -55,7 +55,7 @@ public class KafkaProducerConfig {
|
|||
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
|
||||
configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
|
||||
configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
|
||||
configProps.put(JsonSerializer.TYPE_MAPPINGS, "greeting:com.baeldung.spring.kafka.Greeting, farewell:com.baeldung.spring.kafka.Farewell");
|
||||
configProps.put(JsonSerializer.TYPE_MAPPINGS, "greeting:com.baeldung.spring.kafka.retrayable.Greeting, farewell:com.baeldung.spring.kafka.retrayable.Farewell");
|
||||
return new DefaultKafkaProducerFactory<>(configProps);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.baeldung.spring.kafka;
|
||||
package com.baeldung.spring.kafka.retryable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
|
@ -1,4 +1,4 @@
|
|||
package com.baeldung.spring.kafka;
|
||||
package com.baeldung.spring.kafka.retryable;
|
||||
|
||||
import org.springframework.kafka.annotation.KafkaHandler;
|
||||
import org.springframework.kafka.annotation.KafkaListener;
|
|
@ -1,4 +1,4 @@
|
|||
package com.baeldung.spring.kafka;
|
||||
package com.baeldung.spring.kafka.retryable;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|
@ -16,5 +16,7 @@ monitor.kafka.consumer.groupid.simulate=baeldungGrpSimulate
|
|||
test.topic=testtopic1
|
||||
kafka.backoff.interval=9000
|
||||
kafka.backoff.max_failure=5
|
||||
# multiple listeners properties
|
||||
multiple-listeners.books.topic.name=books
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
package com.baeldung.spring.kafka.multiplelisteners;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.kafka.config.KafkaListenerEndpointRegistry;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
import org.springframework.kafka.listener.AcknowledgingConsumerAwareMessageListener;
|
||||
import org.springframework.kafka.listener.ConcurrentMessageListenerContainer;
|
||||
import org.springframework.kafka.test.context.EmbeddedKafka;
|
||||
|
||||
@SpringBootTest(classes = MultipleListenersApplicationKafkaApp.class)
|
||||
@EmbeddedKafka(partitions = 1, brokerProperties = { "listeners=PLAINTEXT://localhost:9092", "port=9092" })
|
||||
class KafkaMultipleListenersIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private KafkaListenerEndpointRegistry registry;
|
||||
@Autowired
|
||||
private KafkaTemplate<String, BookEvent> bookEventKafkaTemplate;
|
||||
|
||||
private static final String TOPIC = "books";
|
||||
|
||||
@Test
|
||||
void givenEmbeddedKafkaBroker_whenSendingAMessage_thenMessageIsConsumedByAll3Listeners() throws Exception {
|
||||
BookEvent bookEvent = new BookEvent("test-book-title-1", "test-book-desc-1", 2.0);
|
||||
CountDownLatch latch = new CountDownLatch(3);
|
||||
|
||||
List<? extends ConcurrentMessageListenerContainer<?, ?>> bookListeners = registry.getAllListenerContainers()
|
||||
.stream()
|
||||
.map(c -> (ConcurrentMessageListenerContainer<?, ?>) c)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
bookListeners.forEach(listener -> {
|
||||
listener.stop();
|
||||
listener.getContainerProperties()
|
||||
.setMessageListener((AcknowledgingConsumerAwareMessageListener<String, BookEvent>) (data, acknowledgment, consumer) -> {
|
||||
assertThat(data.value()).isEqualTo(bookEvent);
|
||||
latch.countDown();
|
||||
});
|
||||
listener.start();
|
||||
});
|
||||
|
||||
bookEventKafkaTemplate.send(TOPIC, UUID.randomUUID()
|
||||
.toString(), bookEvent);
|
||||
|
||||
assertThat(bookListeners.size()).isEqualTo(3);
|
||||
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenEmbeddedKafkaBroker_whenSendingThreeMessage_thenListenerPrintLogs() throws Exception {
|
||||
CountDownLatch latch = new CountDownLatch(3);
|
||||
Arrays.stream(new int[] { 1, 2, 3 })
|
||||
.mapToObj(i -> new BookEvent(String.format("book %s", i), String.format("description %s", i), (double) i))
|
||||
.forEach(bookEvent -> {
|
||||
bookEventKafkaTemplate.send(TOPIC, UUID.randomUUID()
|
||||
.toString(), bookEvent);
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
// wait for messages to be printed
|
||||
Thread.sleep(1000);
|
||||
|
||||
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.baeldung.spring.kafka;
|
||||
package com.baeldung.spring.kafka.retryable;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
|
@ -17,6 +17,8 @@ import org.springframework.kafka.listener.ConcurrentMessageListenerContainer;
|
|||
import org.springframework.kafka.test.EmbeddedKafkaBroker;
|
||||
import org.springframework.kafka.test.context.EmbeddedKafka;
|
||||
|
||||
import com.baeldung.spring.kafka.retryable.Greeting;
|
||||
import com.baeldung.spring.kafka.retryable.RetryableApplicationKafkaApp;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
@SpringBootTest(classes = RetryableApplicationKafkaApp.class)
|
Loading…
Reference in New Issue