当今技术潮流中,事件驱动的微服务成为了一种转型的力量,其中的微服务模块通过事件实现无缝通信,提高系统的可扩展性、可适应性和敏捷性。消息队列在事件驱动架构中起着重要作用,Apache Kafka 作为高性能、可扩展和可靠的消息队列系统,被广泛应用于实时数据流处理和事件驱动架构中,因此成为了事件驱动架构中的核心技术之一。
本文介绍如何使用 Apache Kafka 构建事件驱动的微服务架构。
1 事件驱动架构简介
事件驱动架构(EDA)是一种软件设计模式,它使系统内部的组件通过生成和消费事件来相互通信。在这种架构中,事件表示系统内发生的重要事件,并可以在其他组件中触发相应的操作。这种方法可以实现松散耦合的系统,提高系统的可扩展性,并能够快速响应实时变化。
2 Apache Kafka 简介
Apache Kafka 是一个分布式、容错的消息系统,可以处理大量数据和实时流。它采用发布-订阅模型,生产者将消息发布到主题中,消费者通过订阅这些主题来接收消息。Kafka 的持久存储和副本机制确保了数据的可靠性和容错能力,是构建实时数据流处理和事件驱动架构的理想选择。
3 设置环境
在深入研究构建微服务之前,先确保已经设置了有效的工作环境。需要:
- Java 开发工具包(JDK)
- Gradle(如果您使用 gradle-wrapper,则无需安装系统级 gradle)
- Docker
- 运行实例的 Apache Kafka
- 您喜欢的代码编辑器(例如 IntelliJ IDEA、Eclipse、VSCode)
4 具体步骤
4.1 步骤 1:设置 Spring Boot 项目
使用 Spring Initializer (https://start.spring.io/)创建一个新的 Spring Boot 项目,确保包含必要的依赖项。在项目中集成 Spring Web 以管理 Web 功能,并集成 Spring Boot DevTools 以提高开发效率。选择 Java 21 作为开发环境,以兼容新的字符串模板和 Java 虚拟线程。
创建和保存项目文件,然后使用集成开发环境(IDE)打开它们。
在开始项目实现之前,为了保证最佳性能,可以在 application.properties 文件中启用 Spring Boot 虚拟线程。通过这个配置,在处理 HTTP 连接时,可以在默认线程执行器中使用虚拟线程。这样可以提高应用程序的响应速度和处理能力,特别是在高并发场景下。
spring.threads.virtual.enabled=true
4.2 步骤 2:实现文本生产者微服务
在此微服务中,将处理文本数据的上传,并将其发布到名为 TEXT_DATA 的 Apache Kafka 主题中。
在 application.properties 中添加所需的 Apache Kafka 属性
# src/main/resources/application.properties
spring.kafka.bootstrap-servers=localhost:9092
创建所需的组件:
TextDataProducer 类:
package com.example.kafkaapp;
import org.apache.kafka.clients.admin.NewTopic;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaAdmin;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
import java.util.stream.Stream;
import static java.lang.StringTemplate.STR;
@Component
public class TextDataProducer {
Logger logger = Logger.getLogger(getClass().getName());
// 主题配置常量
private final static int PARTITION_COUNT = 8;
private final static String TOPIC = "TEXT-DATA";
private final static short REPLICATION_FACTOR = 1;
private final KafkaTemplate kafkaTemplate;
public TextDataProducer(KafkaTemplate kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
@Autowired
public void configureTopic(KafkaAdmin kafkaAdmin) {
kafkaAdmin.createOrModifyTopics(new NewTopic(TOPIC, PARTITION_COUNT, REPLICATION_FACTOR));
}
private void sendTextMessage(String text, int lineIndex) {
if (text == null || text.isEmpty()) {
return;
}
// 将 Link 消息发送到主题,根据行索引在分区上分发
kafkaTemplate.send(TOPIC, "KEY-" + (lineIndex % PARTITION_COUNT), text);
}
public void sendContentOf(File file) {
Instant before = Instant.now();
try (Stream lines = Files.lines(file.toPath())) {
AtomicInteger counter = new AtomicInteger(0);
lines.forEach(line -> sendTextMessage(line, counter.getAndIncrement()));
Instant after = Instant.now();
Duration duration = Duration.between(before, after);
logger.info(STR."Streamed \{counter.get()} lines in \{duration.toMillis()} millisecond");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
- 该类负责生产文本数据并将其发送到名为 TEXT-DATA 的 Kafka 主题。
- 它使用 @Component 进行注解,使其成为 Spring 管理的组件。
- 它具有主题配置的常量,例如 PARTITION_COUNT、TOPIC 和 REPLICATION_FACTOR。
- configureTopic 方法使用 KafkaAdmin 配置 Kafka 主题。它使用指定的设置创建或修改主题。
- sendTextMessage 方法将文本消息发送到 Kafka 主题,并根据行索引在分区上分布消息。
TextDataProducer 类的最重要部分是 sendContentOf(File file)方法。此方法负责逐行读取给定文件的内容,并将每行发送到名为 TEXT-DATA 的 Kafka 主题。
TextDataController 类
package com.example.kafkaapp;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
@RestController
public class TextDataController {
private final TextDataProducer producer;
public TextDataController(TextDataProducer producer) {
this.producer = producer;
}
@PostMapping("/upload")
public Optional uploadTextFile(@RequestParam("file") MultipartFile file) throws IOException {
Path tempFile = Files.createTempFile(file.getOriginalFilename(), null);
file.transferTo(tempFile);
Thread.ofVirtual().start(() -> producer.sendContentOf(tempFile.toFile()));
return Optional.of(tempFile.toString());
}
}
- 该类是 Spring REST 控制器,用于处理 HTTP 请求。
- 它通过构造函数将 TextDataProducer bean 注入进来。
- uploadTextFile 方法被映射用于处理 /upload 的 POST 请求。它接受一个 multipart 文件,并上传该文件。
- 它创建一个临时文件,将上传的文件内容转移到该文件中,使用 TextDataProducer 将临时文件的内容发送到 Kafka,并返回临时文件的路径。
TextProducerApplication 类
package com.example.kafkaapp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TextProducerApplication {
public static void main(String[] args) {
SpringApplication.run(TextProducerApplication.class, args);
}
}
- 是 Spring Boot 应用程序的入口点。
- 它使用 @SpringBootApplication 注解,该注解组合了 @Configuration、@EnableAutoConfiguration 和 @ComponentScan。
- main 方法启动 Spring Boot 应用程序。
实现了文本生产者后,可以使用以下命令运行项目:
./gradlew bootRun
使用 Rest 客户端(如 Postman 或 SwaggerUI)将示例文本文件发布到 http://localhost:8080/upload 端点。此外,也可以在 IntelliJ IDEA 中使用以下原始 HTTP 命令发布文件:
### 发送包含文本和文件字段的表单
POST http://localhost:8080/upload HTTP/1.1
Content-Type: multipart/form-data; boundary=boundary
--boundary
Content-Disposition: form-data; name="file"; filename="shakespeares.txt"
// 将上传 "input.txt "文件
< /Users/mustafaguc/Desktop/kafka-demo-content/shakespeares.txt
--boundary
4.3 步骤 3:docker化 TextData 生产者
创建 Dockerfile,将微服务打包成 Docker 镜像
FROM openjdk:21-slim
WORKDIR /app
COPY build/libs/text-producer-1.0.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
在构建 Docker 镜像之前,首先需要使用 Gradle 构建微服务:
./gradlew assemble
成功构建单个微服务 jar 文件后,就可以在项目根目录中构建 Docker 镜像:
docker build . -t text-producer
构建成功后,运行以下命令启动 Docker 镜像:
docker run -it -p 8080:8080 text-producer
运行上述命令后,应该会看到以下日志:
c.e.kafkaapp.TextProducerApplication : Starting TextProducerApplication using Java 21.0.1 with PID 4871 (/Users/mustafaguc/projects/java/text-producer/build/classes/java/main started by mustafaguc in /Users/mustafaguc/projects/java/text-producer)
c.e.kafkaapp.TextProducerApplication : No active profile set, falling back to 1 default profile: "default"
o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
o.apache.catalina.core.StandardService : Starting service [Tomcat]
o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.17]
o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 307 ms
o.a.k.clients.admin.AdminClientConfig : AdminClientConfig values:
........
o.a.k.clients.admin.AdminClientConfig : These configurations '[sasl.jaas.config, idompotence.enabled]' were supplied but are not used yet.
o.a.kafka.common.utils.AppInfoParser : Kafka version: 3.6.1
o.a.kafka.common.utils.AppInfoParser : Kafka commitId: 5e3c2b738d253ff5
o.a.kafka.common.utils.AppInfoParser : Kafka startTimeMs: 1708262321726
o.a.kafka.common.utils.AppInfoParser : App info kafka.admin.client for adminclient-1 unregistered
o.apache.kafka.common.metrics.Metrics : Metrics scheduler closed
o.apache.kafka.common.metrics.Metrics : Closing reporter org.apache.kafka.common.metrics.JmxReporter
o.apache.kafka.common.metrics.Metrics : Metrics reporters closed
o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path ''
c.e.kafkaapp.TextProducerApplication : Started TextProducerApplication in 0.764 seconds (process running for 0.976)
目前,已经实现了生产者部分。我们将在下篇文章中介绍消费者和聚合器微服务。