Introdução ao Apache Kafka com Spring

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:

Aprenda a processar dados de fluxo com Flink e Kafka

Read more

Exemplo do Kafka Connect com MQTT e MongoDB

Veja um exemplo prático usando os conectores Kafka.

Read more

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.