Add the kafka-basics module
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
package com.ankurm.kafkabasics;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* An order-events pipeline, small enough to read in one sitting.
|
||||
*
|
||||
* <p>Everything the companion article claims was produced by the tests in {@code src/test},
|
||||
* against a real Kafka broker running in KRaft mode. See the module README for how to point it
|
||||
* at a Testcontainers broker instead.
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class KafkaBasicsApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(KafkaBasicsApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.ankurm.kafkabasics;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.kafka.annotation.KafkaListener;
|
||||
import org.springframework.kafka.support.KafkaHeaders;
|
||||
import org.springframework.messaging.handler.annotation.Header;
|
||||
import org.springframework.messaging.handler.annotation.Payload;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* The consuming side. Note what is NOT here: no polling loop, no offset commit, no rebalance
|
||||
* listener. The container owns all of that, which is the actual value Spring Kafka adds.
|
||||
*
|
||||
* @see <a href="../../../../../docs/05-consuming.md">docs/05-consuming.md</a>
|
||||
*/
|
||||
@Component
|
||||
public class OrderConsumer {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(OrderConsumer.class);
|
||||
|
||||
private final List<Received> received = new CopyOnWriteArrayList<>();
|
||||
|
||||
/** What arrived, with the metadata the tests assert on. */
|
||||
public record Received(OrderEvent event, int partition, long offset, String key) {
|
||||
}
|
||||
|
||||
@KafkaListener(topics = OrderProducer.TOPIC, groupId = "orders-basic")
|
||||
public void onOrder(@Payload OrderEvent event,
|
||||
@Header(KafkaHeaders.RECEIVED_PARTITION) int partition,
|
||||
@Header(KafkaHeaders.OFFSET) long offset,
|
||||
@Header(name = KafkaHeaders.RECEIVED_KEY, required = false) String key) {
|
||||
log.info("received orderId={} partition={} offset={}", event.orderId(), partition, offset);
|
||||
this.received.add(new Received(event, partition, offset, key));
|
||||
}
|
||||
|
||||
public List<Received> received() {
|
||||
return this.received;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
this.received.clear();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.ankurm.kafkabasics;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* The payload. A record works as a Kafka value with no annotations at all, because Jackson 3
|
||||
* handles records natively — but note that it needs a canonical constructor the
|
||||
* deserializer can call, which is the one thing a record gives you for free and a Lombok
|
||||
* {@code @Builder}-only class does not.
|
||||
*
|
||||
* @param orderId the partition key. Same customer, same order, same partition, same order of
|
||||
* delivery — see docs/04-keys-and-partitions.md
|
||||
*/
|
||||
public record OrderEvent(String orderId, String customerId, BigDecimal amount, Instant placedAt) {
|
||||
|
||||
public static OrderEvent of(String orderId, String customerId, String amount) {
|
||||
return new OrderEvent(orderId, customerId, new BigDecimal(amount),
|
||||
Instant.parse("2026-08-29T10:15:30Z"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.ankurm.kafkabasics;
|
||||
|
||||
import org.apache.kafka.clients.producer.ProducerRecord;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
import org.springframework.kafka.support.SendResult;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Three ways to send, and only two of them tell you when the send failed.
|
||||
*
|
||||
* @see <a href="../../../../../docs/02-producing.md">docs/02-producing.md</a>
|
||||
*/
|
||||
@Component
|
||||
public class OrderProducer {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(OrderProducer.class);
|
||||
|
||||
static final String TOPIC = "orders";
|
||||
|
||||
private final KafkaTemplate<String, OrderEvent> template;
|
||||
|
||||
public OrderProducer(KafkaTemplate<String, OrderEvent> template) {
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire and forget. {@code send} returns a {@code CompletableFuture} and this method throws
|
||||
* it away, so a broker-side rejection is invisible here: the method returns normally, the
|
||||
* message never lands, and nothing in your logs says so unless you have the producer's own
|
||||
* logger turned up. This is the single most common way to lose messages in Spring Kafka.
|
||||
*/
|
||||
public void sendAndForget(OrderEvent event) {
|
||||
this.template.send(TOPIC, event.orderId(), event);
|
||||
}
|
||||
|
||||
/** Asynchronous, but the outcome is handled. This is the shape you want by default. */
|
||||
public CompletableFuture<SendResult<String, OrderEvent>> send(OrderEvent event) {
|
||||
CompletableFuture<SendResult<String, OrderEvent>> future =
|
||||
this.template.send(TOPIC, event.orderId(), event);
|
||||
future.whenComplete((result, ex) -> {
|
||||
if (ex != null) {
|
||||
log.error("send failed for orderId={}", event.orderId(), ex);
|
||||
}
|
||||
else {
|
||||
log.info("sent orderId={} to {}-{}@{}", event.orderId(),
|
||||
result.getRecordMetadata().topic(), result.getRecordMetadata().partition(),
|
||||
result.getRecordMetadata().offset());
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous. Correct when the caller must not proceed unless the write is durable, and
|
||||
* expensive for exactly that reason: it blocks a thread for a network round trip plus the
|
||||
* replication acknowledgement.
|
||||
*/
|
||||
public SendResult<String, OrderEvent> sendAndWait(OrderEvent event) throws Exception {
|
||||
return this.template.send(new ProducerRecord<>(TOPIC, event.orderId(), event)).get();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user