Introdução ao Apache Kafka com Spring
1. Visão geral
Apache Kafka é um sistema de processamento de fluxo distribuído e tolerante a falhas.
Neste artigo, vamos cobrir o suporte Spring para Kafka e o nível de abstrações que ele fornece sobre APIs de cliente Java Kafka nativas.
Spring Kafka traz o modelo de programação de template simples e típico do Spring com umKafkaTemplatee POJOs orientados por mensagem via anotação@KafkaListener.
Leitura adicional:
Construindo um pipeline de dados com Flink e Kafka
Aprenda a processar dados de fluxo com Flink e Kafka
Exemplo do Kafka Connect com MQTT e MongoDB
Veja um exemplo prático usando os conectores Kafka.
2. Instalação e Configuração
Para baixar e instalar o Kafka, consulte o guia oficialhere.
Também precisamos adicionar a dependênciaspring-kafka ao nossopom.xml:
org.springframework.kafka
spring-kafka
2.2.7.RELEASE
A versão mais recente deste artefato pode ser encontradahere.
Nosso exemplo de aplicativo será um aplicativo Spring Boot.
Este artigo pressupõe que o servidor seja iniciado usando a configuração padrão e nenhuma porta do servidor seja alterada.
3. Configurando tópicos
Anteriormente, costumávamos executar ferramentas de linha de comando para criar tópicos no Kafka, como:
$ bin/kafka-topics.sh --create \
--zookeeper localhost:2181 \
--replication-factor 1 --partitions 1 \
--topic mytopic
Mas com a introdução deAdminClient no Kafka, agora podemos criar tópicos de maneira programática.
Precisamos adicionar o beanKafkaAdmin Spring, que adicionará tópicos automaticamente para todos os beans do tipoNewTopic:
@Configuration
public class KafkaTopicConfig {
@Value(value = "${kafka.bootstrapAddress}")
private String bootstrapAddress;
@Bean
public KafkaAdmin kafkaAdmin() {
Map configs = new HashMap<>();
configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
return new KafkaAdmin(configs);
}
@Bean
public NewTopic topic1() {
return new NewTopic("example", 1, (short) 1);
}
}
4. Produzindo Mensagens
Para criar mensagens, primeiro, precisamos configurar umProducerFactory que define a estratégia para criar instâncias de KafkaProducer.
Então, precisamos de umKafkaTemplate que envolve uma instânciaProducer e fornece métodos convenientes para enviar mensagens para tópicos do Kafka.
As instâncias deProducer são thread-safe e, portanto, o uso de uma única instância em todo o contexto do aplicativo proporcionará melhor desempenho. Consequentemente, as instânciasKakfaTemplate também são thread-safe e o uso de uma instância é recomendado.
4.1. Configuração do Produtor
@Configuration
public class KafkaProducerConfig {
@Bean
public ProducerFactory producerFactory() {
Map 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);
return new DefaultKafkaProducerFactory<>(configProps);
}
@Bean
public KafkaTemplate kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
}
4.2. Publicação de mensagens
Podemos enviar mensagens usando a classeKafkaTemplate:
@Autowired
private KafkaTemplate kafkaTemplate;
public void sendMessage(String msg) {
kafkaTemplate.send(topicName, msg);
}
The send API returns a ListenableFuture object. Se quisermos bloquear o thread de envio e obter o resultado sobre a mensagem enviada, podemos chamar oget API do objetoListenableFuture. O encadeamento aguardará o resultado, mas diminuirá a velocidade do produtor.
Kafka é uma plataforma de processamento de fluxo rápido. Portanto, é uma ideia melhor lidar com os resultados de forma assíncrona para que as mensagens subsequentes não esperem pelo resultado da mensagem anterior. Podemos fazer isso por meio de um retorno de chamada:
public void sendMessage(String message) {
ListenableFuture> future =
kafkaTemplate.send(topicName, message);
future.addCallback(new ListenableFutureCallback>() {
@Override
public void onSuccess(SendResult result) {
System.out.println("Sent message=[" + message +
"] with offset=[" + result.getRecordMetadata().offset() + "]");
}
@Override
public void onFailure(Throwable ex) {
System.out.println("Unable to send message=["
+ message + "] due to : " + ex.getMessage());
}
});
}
5. Consumindo mensagens
5.1. Configuração do Consumidor
Para consumir mensagens, precisamos configurar umConsumerFactorye umKafkaListenerContainerFactory. Uma vez que esses beans estão disponíveis na fábrica de bean Spring, os consumidores baseados em POJO podem ser configurados usando a anotação@KafkaListener.
A anotação@EnableKafka é necessária na classe de configuração para permitir a detecção da anotação@KafkaListener nos beans gerenciados do Spring:
@EnableKafka
@Configuration
public class KafkaConsumerConfig {
@Bean
public ConsumerFactory consumerFactory() {
Map props = new HashMap<>();
props.put(
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
bootstrapAddress);
props.put(
ConsumerConfig.GROUP_ID_CONFIG,
groupId);
props.put(
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
StringDeserializer.class);
props.put(
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
StringDeserializer.class);
return new DefaultKafkaConsumerFactory<>(props);
}
@Bean
public ConcurrentKafkaListenerContainerFactory
kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
return factory;
}
}
5.2. Consumindo mensagens
@KafkaListener(topics = "topicName", groupId = "foo")
public void listen(String message) {
System.out.println("Received Messasge in group foo: " + message);
}
Multiple listeners can be implemented for a topic, cada um com um Id de grupo diferente. Além disso, um consumidor pode ouvir mensagens de vários tópicos:
@KafkaListener(topics = "topic1, topic2", groupId = "foo")
Spring também suporta a recuperação de um ou mais cabeçalhos de mensagem usando a anotação@Header no listener:
@KafkaListener(topics = "topicName")
public void listenWithHeaders(
@Payload String message,
@Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition) {
System.out.println(
"Received Message: " + message"
+ "from partition: " + partition);
}
5.3. Consumindo mensagens de uma partição específica
Como você deve ter notado, criamos o tópicoexample com apenas uma partição. No entanto, para um tópico com várias partições, a@KafkaListener pode se inscrever explicitamente em uma partição específica de um tópico com um deslocamento inicial:
@KafkaListener(
topicPartitions = @TopicPartition(topic = "topicName",
partitionOffsets = {
@PartitionOffset(partition = "0", initialOffset = "0"),
@PartitionOffset(partition = "3", initialOffset = "0")
}))
public void listenToParition(
@Payload String message,
@Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition) {
System.out.println(
"Received Messasge: " + message"
+ "from partition: " + partition);
}
Visto queinitialOffset foi enviado para 0 neste ouvinte, todas as mensagens anteriormente consumidas das partições 0 e três serão consumidas novamente sempre que este ouvinte for inicializado. Se definir o deslocamento não for necessário, podemos usar a propriedadepartitions da anotação@TopicPartition para definir apenas as partições sem o deslocamento:
@KafkaListener(topicPartitions
= @TopicPartition(topic = "topicName", partitions = { "0", "1" }))
5.4. Adicionar filtro de mensagens para ouvintes
Os ouvintes podem ser configurados para consumir tipos específicos de mensagens adicionando um filtro personalizado. Isso pode ser feito definindo umRecordFilterStrategy paraKafkaListenerContainerFactory:
@Bean
public ConcurrentKafkaListenerContainerFactory
filterKafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.setRecordFilterStrategy(
record -> record.value().contains("World"));
return factory;
}
Um ouvinte pode ser configurado para usar esta fábrica de contêineres:
@KafkaListener(
topics = "topicName",
containerFactory = "filterKafkaListenerContainerFactory")
public void listen(String message) {
// handle message
}
Neste ouvinte, todos osmessages matching the filter will be discarded.
6. Conversores de mensagens personalizados
Até agora, abordamos apenas o envio e o recebimento de Strings como mensagens. No entanto, também podemos enviar e receber objetos Java personalizados. Isso requer a configuração do serializador apropriado emProducerFactorye desserializador emConsumerFactory.
Vejamos uma classe de bean simples, que enviaremos como mensagens:
public class Greeting {
private String msg;
private String name;
// standard getters, setters and constructor
}
6.1. Produção de mensagens personalizadas
Neste exemplo, usaremosJsonSerializer. Vejamos o código deProducerFactory eKafkaTemplate:
@Bean
public ProducerFactory greetingProducerFactory() {
// ...
configProps.put(
ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
JsonSerializer.class);
return new DefaultKafkaProducerFactory<>(configProps);
}
@Bean
public KafkaTemplate greetingKafkaTemplate() {
return new KafkaTemplate<>(greetingProducerFactory());
}
Este novoKafkaTemplate pode ser usado para enviar a mensagemGreeting:
kafkaTemplate.send(topicName, new Greeting("Hello", "World"));
6.2. Consumindo mensagens personalizadas
Da mesma forma, vamos modificarConsumerFactory eKafkaListenerContainerFactory para desserializar a mensagem de saudação corretamente:
@Bean
public ConsumerFactory greetingConsumerFactory() {
// ...
return new DefaultKafkaConsumerFactory<>(
props,
new StringDeserializer(),
new JsonDeserializer<>(Greeting.class));
}
@Bean
public ConcurrentKafkaListenerContainerFactory
greetingKafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(greetingConsumerFactory());
return factory;
}
O serializador e desserializador JSON spring-kafka usa a bibliotecaJackson, que também é uma dependência maven opcional para o projeto spring-kafka. Então, vamos adicioná-lo ao nossopom.xml:
com.fasterxml.jackson.core
jackson-databind
2.9.7
Em vez de usar a versão mais recente de Jackson, é recomendado usar a versão que é adicionada aopom.xml de spring-kafka.
Finalmente, precisamos escrever um ouvinte para consumirGreeting mensagens:
@KafkaListener(
topics = "topicName",
containerFactory = "greetingKafkaListenerContainerFactory")
public void greetingListener(Greeting greeting) {
// process greeting message
}
7. Conclusão
Neste artigo, abordamos os conceitos básicos do suporte do Spring ao Apache Kafka. Analisamos brevemente as classes usadas para enviar e receber mensagens.
O código-fonte completo para este artigo pode ser encontradoover on GitHub. Antes de executar o código, verifique se o servidor Kafka está sendo executado e se os tópicos foram criados manualmente.