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 { | public class Farewell { | ||||||
| 
 | 
 | ||||||
| @ -1,4 +1,4 @@ | |||||||
| package com.baeldung.spring.kafka; | package com.baeldung.spring.kafka.retryable; | ||||||
| 
 | 
 | ||||||
| public class Greeting { | 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.HashMap; | ||||||
| import java.util.Map; | 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.HashMap; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
| @ -55,7 +55,7 @@ public class KafkaProducerConfig { | |||||||
|         configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress); |         configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress); | ||||||
|         configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); |         configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); | ||||||
|         configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.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); |         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.HashMap; | ||||||
| import java.util.Map; | 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.KafkaHandler; | ||||||
| import org.springframework.kafka.annotation.KafkaListener; | 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.SpringApplication; | ||||||
| import org.springframework.boot.autoconfigure.SpringBootApplication; | import org.springframework.boot.autoconfigure.SpringBootApplication; | ||||||
| @ -16,5 +16,7 @@ monitor.kafka.consumer.groupid.simulate=baeldungGrpSimulate | |||||||
| test.topic=testtopic1 | test.topic=testtopic1 | ||||||
| kafka.backoff.interval=9000 | kafka.backoff.interval=9000 | ||||||
| kafka.backoff.max_failure=5 | 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; | 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.EmbeddedKafkaBroker; | ||||||
| import org.springframework.kafka.test.context.EmbeddedKafka; | 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; | import com.fasterxml.jackson.databind.ObjectMapper; | ||||||
| 
 | 
 | ||||||
| @SpringBootTest(classes = RetryableApplicationKafkaApp.class) | @SpringBootTest(classes = RetryableApplicationKafkaApp.class) | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user