Java tutorial
/* * Copyright 2016-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.kafka.test.utils; import static org.assertj.core.api.Assertions.assertThat; import java.time.Duration; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Properties; import java.util.stream.Collectors; import org.apache.commons.logging.LogFactory; import org.apache.kafka.clients.consumer.Consumer; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.common.TopicPartition; import org.apache.kafka.common.serialization.IntegerDeserializer; import org.apache.kafka.common.serialization.IntegerSerializer; import org.apache.kafka.common.serialization.StringDeserializer; import org.apache.kafka.common.serialization.StringSerializer; import org.springframework.beans.DirectFieldAccessor; import org.springframework.core.log.LogAccessor; import org.springframework.kafka.test.EmbeddedKafkaBroker; import org.springframework.util.Assert; /** * Kafka testing utilities. * * @author Gary Russell * @author Hugo Wood * @author Artem Bilan */ public final class KafkaTestUtils { private static final LogAccessor logger = new LogAccessor(LogFactory.getLog(KafkaTestUtils.class)); // NOSONAR private static Properties defaults; private KafkaTestUtils() { // private ctor } /** * Set up test properties for an {@code <Integer, String>} consumer. * @param group the group id. * @param autoCommit the auto commit. * @param embeddedKafka a {@link EmbeddedKafkaBroker} instance. * @return the properties. */ public static Map<String, Object> consumerProps(String group, String autoCommit, EmbeddedKafkaBroker embeddedKafka) { return consumerProps(embeddedKafka.getBrokersAsString(), group, autoCommit); } /** * Set up test properties for an {@code <Integer, String>} producer. * @param embeddedKafka a {@link EmbeddedKafkaBroker} instance. * @return the properties. */ public static Map<String, Object> producerProps(EmbeddedKafkaBroker embeddedKafka) { return senderProps(embeddedKafka.getBrokersAsString()); } /** * Set up test properties for an {@code <Integer, String>} consumer. * @param brokers the bootstrapServers property. * @param group the group id. * @param autoCommit the auto commit. * @return the properties. */ public static Map<String, Object> consumerProps(String brokers, String group, String autoCommit) { Map<String, Object> props = new HashMap<>(); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers); props.put(ConsumerConfig.GROUP_ID_CONFIG, group); props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, autoCommit); props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "10"); props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "60000"); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); return props; } /** * Set up test properties for an {@code <Integer, String>} producer. * @param brokers the bootstrapServers property. * @return the properties. */ public static Map<String, Object> senderProps(String brokers) { Map<String, Object> props = new HashMap<>(); props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers); props.put(ProducerConfig.RETRIES_CONFIG, 0); props.put(ProducerConfig.BATCH_SIZE_CONFIG, "16384"); props.put(ProducerConfig.LINGER_MS_CONFIG, 1); props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, "33554432"); props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class); props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); return props; } /** * Poll the consumer, expecting a single record for the specified topic. * @param consumer the consumer. * @param topic the topic. * @param <K> the key type. * @param <V> the value type. * @return the record. * @throws org.junit.ComparisonFailure if exactly one record is not received. * @see #getSingleRecord(Consumer, String, long) */ public static <K, V> ConsumerRecord<K, V> getSingleRecord(Consumer<K, V> consumer, String topic) { return getSingleRecord(consumer, topic, 60000); // NOSONAR magic # } /** * Poll the consumer, expecting a single record for the specified topic. * @param consumer the consumer. * @param topic the topic. * @param timeout max time in milliseconds to wait for records; forwarded to {@link Consumer#poll(long)}. * @param <K> the key type. * @param <V> the value type. * @return the record. * @throws org.junit.ComparisonFailure if exactly one record is not received. * @since 2.0 */ public static <K, V> ConsumerRecord<K, V> getSingleRecord(Consumer<K, V> consumer, String topic, long timeout) { ConsumerRecords<K, V> received = getRecords(consumer, timeout); Iterator<ConsumerRecord<K, V>> iterator = received.records(topic).iterator(); assertThat(iterator.hasNext()).as("No records found for topic").isTrue(); iterator.next(); assertThat(iterator.hasNext()).as("More than one record for topic found").isFalse(); if (received.count() > 1) { Map<TopicPartition, Long> reset = new HashMap<>(); received.forEach(rec -> { if (!rec.topic().equals(topic)) { reset.computeIfAbsent(new TopicPartition(rec.topic(), rec.partition()), tp -> rec.offset()); } }); reset.forEach((tp, off) -> consumer.seek(tp, off)); } return received.records(topic).iterator().next(); } /** * Poll the consumer for records. * @param consumer the consumer. * @param <K> the key type. * @param <V> the value type. * @return the records. * @see #getRecords(Consumer, long) */ public static <K, V> ConsumerRecords<K, V> getRecords(Consumer<K, V> consumer) { return getRecords(consumer, 60000); // NOSONAR magic # } /** * Poll the consumer for records. * @param consumer the consumer. * @param timeout max time in milliseconds to wait for records; forwarded to {@link Consumer#poll(long)}. * @param <K> the key type. * @param <V> the value type. * @return the records. * @since 2.0 */ public static <K, V> ConsumerRecords<K, V> getRecords(Consumer<K, V> consumer, long timeout) { logger.debug("Polling..."); ConsumerRecords<K, V> received = consumer.poll(Duration.ofMillis(timeout)); logger.debug(() -> "Received: " + received.count() + ", " + received.partitions().stream().flatMap(p -> received.records(p).stream()) // map to same format as send metadata toString() .map(r -> r.topic() + "-" + r.partition() + "@" + r.offset()).collect(Collectors.toList())); assertThat(received).as("null received from consumer.poll()").isNotNull(); return received; } /** * Uses nested {@link DirectFieldAccessor}s to obtain a property using dotted notation to traverse fields; e.g. * "foo.bar.baz" will obtain a reference to the baz field of the bar field of foo. Adopted from Spring Integration. * @param root The object. * @param propertyPath The path. * @return The field. */ public static Object getPropertyValue(Object root, String propertyPath) { Object value = null; DirectFieldAccessor accessor = new DirectFieldAccessor(root); String[] tokens = propertyPath.split("\\."); for (int i = 0; i < tokens.length; i++) { value = accessor.getPropertyValue(tokens[i]); if (value != null) { accessor = new DirectFieldAccessor(value); } else if (i == tokens.length - 1) { return null; } else { throw new IllegalArgumentException("intermediate property '" + tokens[i] + "' is null"); } } return value; } /** * A typed version of {@link #getPropertyValue(Object, String)}. * @param root the object. * @param propertyPath the path. * @param type the type to cast the object to. * @param <T> the type. * @return the field value. * @see #getPropertyValue(Object, String) */ @SuppressWarnings("unchecked") public static <T> T getPropertyValue(Object root, String propertyPath, Class<T> type) { Object value = getPropertyValue(root, propertyPath); if (value != null) { Assert.isAssignable(type, value.getClass()); } return (T) value; } /** * Return a {@link Properties} object equal to the default consumer property overrides. * Useful when matching arguments in Mockito tests. * @return the default properties. * @since 2.2.5 */ public static Properties defaultPropertyOverrides() { if (defaults == null) { Properties props = new Properties(); props.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); defaults = props; } return defaults; } }