Introduction à Apache Kafka avec Printemps

Introduction à Apache Kafka avec le printemps

1. Vue d'ensemble

Apache Kafka est un système de traitement de flux distribué et tolérant aux pannes.

Dans cet article, nous aborderons la prise en charge de Spring pour Kafka et le niveau d'abstractions qu'il fournit sur les API client Kafka Java natives.

Spring Kafka apporte le modèle de programmation de modèle Spring simple et typique avec unKafkaTemplate et des POJO pilotés par message via l'annotation@KafkaListener.

Lectures complémentaires:

Apprenez à traiter les données de flux avec Flink et Kafka

Read more

Exemple Kafka Connect avec MQTT et MongoDB

Regardez un exemple pratique utilisant les connecteurs Kafka.

Read more

2. Installation et configuration

Pour télécharger et installer Kafka, veuillez vous référer au guide officielhere.

Nous devons également ajouter la dépendancespring-kafka à nospom.xml:


    org.springframework.kafka
    spring-kafka
    2.2.7.RELEASE

La dernière version de cet artefact peut être trouvéehere.

Notre exemple d'application sera une application Spring Boot.

Cet article suppose que le serveur est démarré à l'aide de la configuration par défaut et qu'aucun port de serveur n'est modifié.

3. Configurer les sujets

Auparavant, nous utilisions des outils de ligne de commande pour créer des sujets dans Kafka, tels que:

$ bin/kafka-topics.sh --create \
  --zookeeper localhost:2181 \
  --replication-factor 1 --partitions 1 \
  --topic mytopic

Mais avec l'introduction deAdminClient dans Kafka, nous pouvons désormais créer des sujets par programmation.

Nous devons ajouter le haricot SpringKafkaAdmin, qui ajoutera automatiquement des rubriques pour tous les beans de typeNewTopic:

@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. Produire des messages

Pour créer des messages, nous devons d'abord configurer unProducerFactory qui définit la stratégie de création des instances de KafkaProducer.

Ensuite, nous avons besoin d'unKafkaTemplate qui encapsule une instance deProducer et fournit des méthodes pratiques pour envoyer des messages aux sujets Kafka.

Les instances deProducer sont thread-safe et donc l'utilisation d'une seule instance dans un contexte d'application donnera de meilleures performances. Par conséquent, les instancesKakfaTemplate sont également thread-safe et l'utilisation d'une instance est recommandée.

4.1. Configuration du producteur

@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. Publication de messages

Nous pouvons envoyer des messages en utilisant la classeKafkaTemplate:

@Autowired
private KafkaTemplate kafkaTemplate;

public void sendMessage(String msg) {
    kafkaTemplate.send(topicName, msg);
}

The send API returns a ListenableFuture object. Si nous voulons bloquer le thread d'envoi et obtenir le résultat sur le message envoyé, nous pouvons appeler leget API de l'objetListenableFuture. Le thread attendra le résultat, mais ralentira le producteur.

Kafka est une plateforme de traitement de flux rapide. Il est donc préférable de gérer les résultats de manière asynchrone afin que les messages suivants n'attendent pas le résultat du message précédent. Nous pouvons le faire via un rappel:

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. Consommer des messages

5.1. Configuration du consommateur

Pour consommer des messages, nous devons configurer unConsumerFactory et unKafkaListenerContainerFactory. Une fois que ces beans sont disponibles dans l'usine Spring Bean, les consommateurs basés sur POJO peuvent être configurés à l'aide de l'annotation@KafkaListener.

L'annotation@EnableKafka est requise sur la classe de configuration pour permettre la détection de l'annotation@KafkaListener sur les beans gérés par 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. Consommer des messages

@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, chacun avec un ID de groupe différent. En outre, un consommateur peut écouter des messages provenant de différents sujets:

@KafkaListener(topics = "topic1, topic2", groupId = "foo")

Spring prend également en charge la récupération d'un ou plusieurs en-têtes de message à l'aide de l'annotation@Header dans l'écouteur:

@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. Consommer des messages à partir d'une partition spécifique

Comme vous l'avez peut-être remarqué, nous avons créé le sujetexample avec une seule partition. Cependant, pour un sujet avec plusieurs partitions, un@KafkaListener peut s'abonner explicitement à une partition particulière d'un sujet avec un décalage initial:

@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);
}

Puisque leinitialOffset a été envoyé à 0 dans cet écouteur, tous les messages précédemment consommés des partitions 0 et 3 seront réutilisés à chaque fois que cet écouteur est initialisé. Si la définition du décalage n'est pas requise, nous pouvons utiliser la propriétépartitions de l'annotation@TopicPartition pour définir uniquement les partitions sans décalage:

@KafkaListener(topicPartitions
  = @TopicPartition(topic = "topicName", partitions = { "0", "1" }))

5.4. Ajout d'un filtre de messages pour les auditeurs

Les écouteurs peuvent être configurés pour consommer des types de messages spécifiques en ajoutant un filtre personnalisé. Cela peut être fait en définissant unRecordFilterStrategy sur leKafkaListenerContainerFactory:

@Bean
public ConcurrentKafkaListenerContainerFactory
  filterKafkaListenerContainerFactory() {

    ConcurrentKafkaListenerContainerFactory factory =
      new ConcurrentKafkaListenerContainerFactory<>();
    factory.setConsumerFactory(consumerFactory());
    factory.setRecordFilterStrategy(
      record -> record.value().contains("World"));
    return factory;
}

Un écouteur peut ensuite être configuré pour utiliser cette fabrique de conteneurs:

@KafkaListener(
  topics = "topicName",
  containerFactory = "filterKafkaListenerContainerFactory")
public void listen(String message) {
    // handle message
}

Dans cet écouteur, tous lesmessages matching the filter will be discarded.

6. Convertisseurs de messages personnalisés

Jusqu'à présent, nous avons uniquement traité l'envoi et la réception de chaînes en tant que messages. Cependant, nous pouvons également envoyer et recevoir des objets Java personnalisés. Cela nécessite de configurer le sérialiseur approprié dansProducerFactory et le désérialiseur dansConsumerFactory.

Regardons une simple classe de bean, que nous enverrons sous forme de messages:

public class Greeting {

    private String msg;
    private String name;

    // standard getters, setters and constructor
}

6.1. Produire des messages personnalisés

Dans cet exemple, nous utiliseronsJsonSerializer. Regardons le code pourProducerFactory etKafkaTemplate:

@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());
}

Ce nouveauKafkaTemplate peut être utilisé pour envoyer le messageGreeting:

kafkaTemplate.send(topicName, new Greeting("Hello", "World"));

6.2. Consommer des messages personnalisés

De même, modifions lesConsumerFactory etKafkaListenerContainerFactory pour désérialiser correctement le message d'accueil:

@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;
}

Le sérialiseur et désérialiseur JSON spring-kafka utilise la bibliothèqueJackson qui est également une dépendance maven facultative pour le projet spring-kafka. Alors ajoutons-le à nospom.xml:


    com.fasterxml.jackson.core
    jackson-databind
    2.9.7

Au lieu d'utiliser la dernière version de Jackson, il est recommandé d'utiliser la version qui est ajoutée auxpom.xml de spring-kafka.

Enfin, nous devons écrire un écouteur pour consommer les messagesGreeting:

@KafkaListener(
  topics = "topicName",
  containerFactory = "greetingKafkaListenerContainerFactory")
public void greetingListener(Greeting greeting) {
    // process greeting message
}

7. Conclusion

Dans cet article, nous avons présenté les bases du support Spring pour Apache Kafka. Nous avons brièvement examiné les classes utilisées pour envoyer et recevoir des messages.

Le code source complet de cet article peut être trouvéover on GitHub. Avant d’exécuter le code, assurez-vous que le serveur Kafka est en cours d’exécution et que les sujets sont créés manuellement.