Spring Kafka: @KafkaListener
Spring Kafka wraps the Kafka client in a Spring-friendly programming model. The @KafkaListener annotation turns any Spring bean method into a Kafka consumer: no poll loop, no manual lifecycle management. Spring manages the consumer thread, offset commits, and reconnection automatically.
Contents
Dependency & Auto-configuration
Add spring-kafkaKafkaListenerContainerFactoryapplication.properties
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=my-app-group
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
Basic @KafkaListener
Annotate any Spring-managed method with @KafkaListener
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;
@Service
public class OrderConsumer {
@KafkaListener(topics = "orders", groupId = "order-service")
public void consume(String message) {
System.out.println("Received: " + message);
}
// Listen to multiple topics
@KafkaListener(topics = {"orders", "returns"}, groupId = "order-service")
public void consumeMultiple(String message) {
System.out.println("Received: " + message);
}
}
@EnableKafka@Configurationspring-kafkaAccessing ConsumerRecord Metadata
Accept a ConsumerRecord
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;
@Service
public class OrderConsumer {
@KafkaListener(topics = "orders")
public void consume(ConsumerRecord<String, String> record) {
System.out.printf("topic=%s partition=%d offset=%d key=%s value=%s%n",
record.topic(), record.partition(), record.offset(),
record.key(), record.value());
}
}
Concurrency: Multiple Consumer Threads
Set concurrency
@KafkaListener(topics = "orders", concurrency = "3") // 3 threads, each consumes assigned partitions
public void consume(String message) {
System.out.println(Thread.currentThread().getName() + " received: " + message);
}
# Or configure the default factory concurrency globally
spring.kafka.listener.concurrency=3
Manual Acknowledgement
By default Spring Kafka uses auto-acknowledgement after each record is processed. For precise control, switch to MANUALMANUAL_IMMEDIATEack.acknowledge()
spring.kafka.listener.ack-mode=MANUAL_IMMEDIATE
import org.springframework.kafka.support.Acknowledgment;
@KafkaListener(topics = "orders")
public void consume(String message, Acknowledgment ack) {
try {
processOrder(message);
ack.acknowledge(); // commit offset only after successful processing
} catch (Exception e) {
// Do NOT ack — message will be redelivered
log.error("Processing failed, will retry", e);
}
}
Batch Listener
Consume a whole batch of records in one method call by accepting a List
spring.kafka.listener.type=batch
import java.util.List;
@KafkaListener(topics = "orders")
public void consumeBatch(List<String> messages) {
System.out.println("Batch of " + messages.size() + " messages received");
messages.forEach(this::processOrder);
}
// Or with full ConsumerRecord access
@KafkaListener(topics = "orders")
public void consumeBatch(List<ConsumerRecord<String, String>> records) {
records.forEach(r -> processOrder(r.value()));
}
Message Filtering
Use a RecordFilterStrategy
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
@Configuration
public class KafkaConsumerConfig {
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> filteringFactory(
ConsumerFactory<String, String> consumerFactory) {
ConcurrentKafkaListenerContainerFactory<String, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory);
// Skip messages where the value starts with "CANCELLED"
factory.setRecordFilterStrategy(record ->
record.value().startsWith("CANCELLED"));
return factory;
}
}
// Reference the custom factory in the listener
@KafkaListener(topics = "orders", containerFactory = "filteringFactory")
public void consume(String message) {
// Only receives non-CANCELLED orders
}