From 37f566c086a2773d3183b806148902723097c923 Mon Sep 17 00:00:00 2001 From: Evan Hu Date: Mon, 14 Sep 2026 01:08:11 +0900 Subject: [PATCH 1/2] feat: add four dependency-based example modules (kafka, mail, elasticsearch, flyway) --- Readme.md | 12 +++- changeLog.md | 16 +++++ elasticsearch/pom.xml | 41 +++++++++++++ .../elasticsearch/ElasticsearchMain.java | 29 +++++++++ .../controller/ArticleController.java | 44 ++++++++++++++ .../xiaomo/elasticsearch/model/Article.java | 40 +++++++++++++ .../repository/ArticleRepository.java | 20 +++++++ .../elasticsearch/service/ArticleService.java | 37 ++++++++++++ .../resources/config/application.properties | 10 ++++ .../src/main/resources/config/logback-dev.xml | 17 ++++++ flyway/pom.xml | 57 ++++++++++++++++++ .../java/info/xiaomo/flyway/FlywayMain.java | 29 +++++++++ .../flyway/controller/UserController.java | 35 +++++++++++ .../java/info/xiaomo/flyway/dao/UserDao.java | 31 ++++++++++ .../resources/config/application.properties | 18 ++++++ .../src/main/resources/config/logback-dev.xml | 17 ++++++ .../db/migration/V1__create_user_table.sql | 5 ++ .../resources/db/migration/V2__seed_users.sql | 3 + .../xiaomo/flyway/FlywayMigrationTest.java | 46 +++++++++++++++ kafka/pom.xml | 47 +++++++++++++++ .../java/info/xiaomo/kafka/KafkaMain.java | 27 +++++++++ .../xiaomo/kafka/config/KafkaTopicConfig.java | 20 +++++++ .../kafka/controller/OrderController.java | 29 +++++++++ .../kafka/service/OrderEventConsumer.java | 23 ++++++++ .../kafka/service/OrderEventProducer.java | 24 ++++++++ .../resources/config/application.properties | 16 +++++ .../src/main/resources/config/logback-dev.xml | 18 ++++++ .../info/xiaomo/kafka/OrderKafkaTest.java | 47 +++++++++++++++ mail/pom.xml | 52 ++++++++++++++++ .../main/java/info/xiaomo/mail/MailMain.java | 27 +++++++++ .../mail/controller/MailController.java | 37 ++++++++++++ .../info/xiaomo/mail/service/MailService.java | 43 ++++++++++++++ .../resources/config/application.properties | 15 +++++ .../src/main/resources/config/logback-dev.xml | 17 ++++++ .../info/xiaomo/mail/MailServiceTest.java | 59 +++++++++++++++++++ pom.xml | 15 +++++ 36 files changed, 1021 insertions(+), 2 deletions(-) create mode 100644 elasticsearch/pom.xml create mode 100644 elasticsearch/src/main/java/info/xiaomo/elasticsearch/ElasticsearchMain.java create mode 100644 elasticsearch/src/main/java/info/xiaomo/elasticsearch/controller/ArticleController.java create mode 100644 elasticsearch/src/main/java/info/xiaomo/elasticsearch/model/Article.java create mode 100644 elasticsearch/src/main/java/info/xiaomo/elasticsearch/repository/ArticleRepository.java create mode 100644 elasticsearch/src/main/java/info/xiaomo/elasticsearch/service/ArticleService.java create mode 100644 elasticsearch/src/main/resources/config/application.properties create mode 100644 elasticsearch/src/main/resources/config/logback-dev.xml create mode 100644 flyway/pom.xml create mode 100644 flyway/src/main/java/info/xiaomo/flyway/FlywayMain.java create mode 100644 flyway/src/main/java/info/xiaomo/flyway/controller/UserController.java create mode 100644 flyway/src/main/java/info/xiaomo/flyway/dao/UserDao.java create mode 100644 flyway/src/main/resources/config/application.properties create mode 100644 flyway/src/main/resources/config/logback-dev.xml create mode 100644 flyway/src/main/resources/db/migration/V1__create_user_table.sql create mode 100644 flyway/src/main/resources/db/migration/V2__seed_users.sql create mode 100644 flyway/src/test/java/info/xiaomo/flyway/FlywayMigrationTest.java create mode 100644 kafka/pom.xml create mode 100644 kafka/src/main/java/info/xiaomo/kafka/KafkaMain.java create mode 100644 kafka/src/main/java/info/xiaomo/kafka/config/KafkaTopicConfig.java create mode 100644 kafka/src/main/java/info/xiaomo/kafka/controller/OrderController.java create mode 100644 kafka/src/main/java/info/xiaomo/kafka/service/OrderEventConsumer.java create mode 100644 kafka/src/main/java/info/xiaomo/kafka/service/OrderEventProducer.java create mode 100644 kafka/src/main/resources/config/application.properties create mode 100644 kafka/src/main/resources/config/logback-dev.xml create mode 100644 kafka/src/test/java/info/xiaomo/kafka/OrderKafkaTest.java create mode 100644 mail/pom.xml create mode 100644 mail/src/main/java/info/xiaomo/mail/MailMain.java create mode 100644 mail/src/main/java/info/xiaomo/mail/controller/MailController.java create mode 100644 mail/src/main/java/info/xiaomo/mail/service/MailService.java create mode 100644 mail/src/main/resources/config/application.properties create mode 100644 mail/src/main/resources/config/logback-dev.xml create mode 100644 mail/src/test/java/info/xiaomo/mail/MailServiceTest.java diff --git a/Readme.md b/Readme.md index f67ca28d..d410cece 100644 --- a/Readme.md +++ b/Readme.md @@ -18,7 +18,7 @@ ## 模块一览 -一共 26 个模块。`core` 是被其他模块共同依赖的基础包,其余每个模块各自演示一项技术。 +一共 30 个模块。`core` 是被其他模块共同依赖的基础包,其余每个模块各自演示一项技术。 | 模块 | 演示内容 | 启动类 | 需要的外部服务 | | --- | --- | --- | --- | @@ -48,10 +48,14 @@ | `i18n` | 国际化:`MessageSource` + `LocaleResolver` 多语言文案 | `I18nMain` | — | | `ratelimit` | 接口限流:`HandlerInterceptor` + 固定时间窗口 | `RateLimitMain` | — | | `graphql` | GraphQL 查询接口(`@QueryMapping` + schema) | `GraphqlMain` | — | +| `kafka` | Kafka 消息收发(`KafkaTemplate` + `@KafkaListener`) | `KafkaMain` | Kafka | +| `mail` | 邮件发送(`SimpleMailMessage` / `MimeMessageHelper`) | `MailMain` | SMTP 服务器 | +| `elasticsearch` | Spring Data Elasticsearch 文档检索 | `ElasticsearchMain` | Elasticsearch | +| `flyway` | Flyway 数据库迁移 + `JdbcTemplate` 读取 | `FlywayMain` | MySQL | 除 `socket` 使用 8081 外,其余模块都监听 **8080**,所以一次只启动一个模块。 -其中 `validation`、`fileupload`、`restclient`、`cache`、`actuator`、`async`、`aop`、`httpinterface`、`i18n`、`ratelimit`、`graphql` 十一个模块附带可直接运行的测试。它们都不依赖外部服务,`mvn test` 即可跑通,也可以当作各自技术点的可执行文档来读。 +其中 `validation`、`fileupload`、`restclient`、`cache`、`actuator`、`async`、`aop`、`httpinterface`、`i18n`、`ratelimit`、`graphql`、`kafka`、`mail`、`flyway` 十四个模块附带可直接运行的测试。它们的测试都不依赖外部服务——`kafka` 用内嵌 Kafka、`mail` 用 GreenMail 内嵌 SMTP、`flyway` 用内嵌 H2,其余用 Spring 测试上下文;`mvn test` 即可跑通,也可以当作各自技术点的可执行文档来读。 ## 技术栈 @@ -68,6 +72,10 @@ | 监控 | Micrometer + Spring Boot Actuator(`actuator` 模块) | | AOP | AspectJ(`aop` 模块) | | GraphQL | Spring GraphQL + GraphQL Java(`graphql` 模块) | +| 消息 | Spring for Apache Kafka(`kafka` 模块) | +| 邮件 | Spring Mail + Jakarta Mail(`mail` 模块) | +| 搜索引擎 | Spring Data Elasticsearch(`elasticsearch` 模块) | +| 数据库迁移 | Flyway(`flyway` 模块) | | 其他 | MyBatis、Lettuce(Redis)、jsoup、Apache POI、fastjson2、zxing | Spring、Jackson、Hibernate、JUnit 等版本统一由 `spring-boot-dependencies` BOM 管理,不在本项目中单独指定。 diff --git a/changeLog.md b/changeLog.md index eec48151..af5b8b39 100644 --- a/changeLog.md +++ b/changeLog.md @@ -108,3 +108,19 @@ LocalVariableTableParameterNameDiscoverer, 因此未显式命名的 @PathVariabl 几个 Spring Boot 4 下的注意点: aop starter 已从 spring-boot-starter-aop 改名为 spring-boot-starter-aspectj; 声明式客户端直接使用 spring 内置的 @HttpExchange, 无需再引入 OpenFeign; ratelimit 模块把 Clock 抽成 bean, 测试里用 @Primary 注入一个可拨动的时钟, 不靠 sleep 验证"窗口滚动后放行"。 + +- 2026-09-14 新增四个带外部依赖的示例模块 + +``` +1. 新增 kafka 模块: KafkaTemplate + @KafkaListener 消息收发, 测试用 @EmbeddedKafka 内嵌 broker +2. 新增 mail 模块: SimpleMailMessage / MimeMessageHelper 发邮件, 测试用 GreenMail 内嵌 SMTP +3. 新增 elasticsearch 模块: Spring Data Elasticsearch 定义索引 + 仓储检索(与 mongodb 一样不带自动化测试) +4. 新增 flyway 模块: db/migration 下的 V1/V2 迁移脚本 + JdbcTemplate 读取, 测试用内嵌 H2 +5. kafka/mail/flyway 三个模块的测试都不依赖外部服务, CI 上 mvn clean install 可稳定跑通 +``` + +几个 Spring Boot 4 下的注意点: kafka 和 flyway 的自动配置都从 spring-boot-autoconfigure 抽成了独立 +starter(spring-boot-starter-kafka / spring-boot-starter-flyway), 直接引 spring-kafka / flyway-core 拿不到自动配置; +flyway 模块保留了 DataSource, 此时必须再把 JPA 相关自动配置(HibernateJpaAutoConfiguration + DataJpaRepositoriesAutoConfiguration) +排除掉, 否则 core 传递引入的 spring-data-jpa 会尝试创建 entityManagerFactory; mail 测试要把 +spring.mail.properties.mail.smtp.auth / starttls 关掉并清空用户名密码, 否则 JavaMailSender 会在 GreenMail 上做 AUTH。 diff --git a/elasticsearch/pom.xml b/elasticsearch/pom.xml new file mode 100644 index 00000000..2366f03e --- /dev/null +++ b/elasticsearch/pom.xml @@ -0,0 +1,41 @@ + + + + xiaomo + info.xiaomo + 2020.1 + + 4.0.0 + + elasticsearch + + + + info.xiaomo + core + 2020.1 + + + org.springframework.boot + spring-boot-starter-data-elasticsearch + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + \ No newline at end of file diff --git a/elasticsearch/src/main/java/info/xiaomo/elasticsearch/ElasticsearchMain.java b/elasticsearch/src/main/java/info/xiaomo/elasticsearch/ElasticsearchMain.java new file mode 100644 index 00000000..1fbe8ba1 --- /dev/null +++ b/elasticsearch/src/main/java/info/xiaomo/elasticsearch/ElasticsearchMain.java @@ -0,0 +1,29 @@ +package info.xiaomo.elasticsearch; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration; +import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; + +/** + * Elasticsearch 启动器。 + * + *

和 Mongo 一样走 Spring Data 家族: 定义 @Document 实体 + ElasticsearchRepository 接口, + * 框架自动生成实现。服务地址在 application.properties 里配置(spring.elasticsearch.uris)。 + * + * @author : xiaomo + */ +@Configuration +@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class}) +@EnableElasticsearchRepositories +@ComponentScan("info.xiaomo.elasticsearch") +public class ElasticsearchMain { + + public static void main(String[] args) { + SpringApplication.run(ElasticsearchMain.class, args); + } + +} \ No newline at end of file diff --git a/elasticsearch/src/main/java/info/xiaomo/elasticsearch/controller/ArticleController.java b/elasticsearch/src/main/java/info/xiaomo/elasticsearch/controller/ArticleController.java new file mode 100644 index 00000000..c125b9ea --- /dev/null +++ b/elasticsearch/src/main/java/info/xiaomo/elasticsearch/controller/ArticleController.java @@ -0,0 +1,44 @@ +package info.xiaomo.elasticsearch.controller; + +import info.xiaomo.core.base.Result; +import info.xiaomo.elasticsearch.model.Article; +import info.xiaomo.elasticsearch.service.ArticleService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * @author : xiaomo + */ +@RestController +@RequestMapping("/api/articles") +public class ArticleController { + + private final ArticleService service; + + public ArticleController(ArticleService service) { + this.service = service; + } + + @PostMapping + public Result

index(@RequestBody Article article) { + return new Result<>(service.index(article)); + } + + @GetMapping("/search") + public Result> search(@RequestParam String title) { + return new Result<>(service.searchByTitle(title)); + } + + @GetMapping("/author/{author}") + public Result> byAuthor(@PathVariable String author) { + return new Result<>(service.findByAuthor(author)); + } + +} \ No newline at end of file diff --git a/elasticsearch/src/main/java/info/xiaomo/elasticsearch/model/Article.java b/elasticsearch/src/main/java/info/xiaomo/elasticsearch/model/Article.java new file mode 100644 index 00000000..d7247025 --- /dev/null +++ b/elasticsearch/src/main/java/info/xiaomo/elasticsearch/model/Article.java @@ -0,0 +1,40 @@ +package info.xiaomo.elasticsearch.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; + +import java.util.List; + +/** + * @Document(indexName = "articles") 把这个类映射成 ES 里的一个索引。 + * 中文分词这类定制需要给索引配置相应 analyzer, 这里用默认标准分析器保持零依赖可跑。 + * + * @author : xiaomo + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Document(indexName = "articles") +public class Article { + + @Id + private String id; + + @Field(type = FieldType.Text) + private String title; + + @Field(type = FieldType.Text) + private String content; + + @Field(type = FieldType.Keyword) + private String author; + + @Field(type = FieldType.Keyword) + private List tags; + +} \ No newline at end of file diff --git a/elasticsearch/src/main/java/info/xiaomo/elasticsearch/repository/ArticleRepository.java b/elasticsearch/src/main/java/info/xiaomo/elasticsearch/repository/ArticleRepository.java new file mode 100644 index 00000000..6c22c754 --- /dev/null +++ b/elasticsearch/src/main/java/info/xiaomo/elasticsearch/repository/ArticleRepository.java @@ -0,0 +1,20 @@ +package info.xiaomo.elasticsearch.repository; + +import info.xiaomo.elasticsearch.model.Article; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +import java.util.List; + +/** + * 方法名即查询: findByTitleContaining 会生成对 title 字段做 match 的查询。 + * 复杂的 bool/聚合查询再换成 NativeQuery 或 @Query 注解。 + * + * @author : xiaomo + */ +public interface ArticleRepository extends ElasticsearchRepository { + + List
findByTitleContaining(String keyword); + + List
findByAuthor(String author); + +} \ No newline at end of file diff --git a/elasticsearch/src/main/java/info/xiaomo/elasticsearch/service/ArticleService.java b/elasticsearch/src/main/java/info/xiaomo/elasticsearch/service/ArticleService.java new file mode 100644 index 00000000..045e5c8b --- /dev/null +++ b/elasticsearch/src/main/java/info/xiaomo/elasticsearch/service/ArticleService.java @@ -0,0 +1,37 @@ +package info.xiaomo.elasticsearch.service; + +import info.xiaomo.elasticsearch.model.Article; +import info.xiaomo.elasticsearch.repository.ArticleRepository; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +/** + * @author : xiaomo + */ +@Service +public class ArticleService { + + private final ArticleRepository repository; + + public ArticleService(ArticleRepository repository) { + this.repository = repository; + } + + public Article index(Article article) { + if (article.getId() == null || article.getId().isBlank()) { + article.setId(UUID.randomUUID().toString()); + } + return repository.save(article); + } + + public List
searchByTitle(String keyword) { + return repository.findByTitleContaining(keyword); + } + + public List
findByAuthor(String author) { + return repository.findByAuthor(author); + } + +} \ No newline at end of file diff --git a/elasticsearch/src/main/resources/config/application.properties b/elasticsearch/src/main/resources/config/application.properties new file mode 100644 index 00000000..501f0323 --- /dev/null +++ b/elasticsearch/src/main/resources/config/application.properties @@ -0,0 +1,10 @@ +logging.config=classpath:config/logback-dev.xml +server.port=8080 + +server.max-http-header-size=20971520 + +spring.jackson.date-format=yyyy-MM-dd HH:mm:ss +spring.jackson.time-zone=GMT+8 + +# elasticsearch +spring.elasticsearch.uris=http://localhost:9200 \ No newline at end of file diff --git a/elasticsearch/src/main/resources/config/logback-dev.xml b/elasticsearch/src/main/resources/config/logback-dev.xml new file mode 100644 index 00000000..39e7cc65 --- /dev/null +++ b/elasticsearch/src/main/resources/config/logback-dev.xml @@ -0,0 +1,17 @@ + + + + + + + [%d{yyyy-MM-dd HH:mm:ss} [%thread] %highlight(%-5level) %cyan(%logger{15}) - %highlight(%msg) %n + + + + + + + + + + \ No newline at end of file diff --git a/flyway/pom.xml b/flyway/pom.xml new file mode 100644 index 00000000..35918ea5 --- /dev/null +++ b/flyway/pom.xml @@ -0,0 +1,57 @@ + + + + xiaomo + info.xiaomo + 2020.1 + + 4.0.0 + + flyway + + + + info.xiaomo + core + 2020.1 + + + org.springframework.boot + spring-boot-starter-jdbc + + + + org.springframework.boot + spring-boot-starter-flyway + + + + com.h2database + h2 + test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + \ No newline at end of file diff --git a/flyway/src/main/java/info/xiaomo/flyway/FlywayMain.java b/flyway/src/main/java/info/xiaomo/flyway/FlywayMain.java new file mode 100644 index 00000000..098d7a91 --- /dev/null +++ b/flyway/src/main/java/info/xiaomo/flyway/FlywayMain.java @@ -0,0 +1,29 @@ +package info.xiaomo.flyway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.data.jpa.autoconfigure.DataJpaRepositoriesAutoConfiguration; +import org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * Flyway 启动器。 + * + *

注意这里和其他模块不一样: 没有排除 DataSourceAutoConfiguration —— Flyway 需要一个 + * DataSource 来执行 db/migration 下的脚本。因为示例用 JdbcTemplate 而非 JPA 读数据, + * 所以只需排除 JPA 相关的自动配置: 有了 DataSource 之后, 若不排除会触发 core 传递引入的 + * Spring Data JPA 去建 entityManagerFactory。 + * + * @author : xiaomo + */ +@Configuration +@EnableAutoConfiguration(exclude = {HibernateJpaAutoConfiguration.class, DataJpaRepositoriesAutoConfiguration.class}) +@ComponentScan("info.xiaomo.flyway") +public class FlywayMain { + + public static void main(String[] args) { + SpringApplication.run(FlywayMain.class, args); + } + +} \ No newline at end of file diff --git a/flyway/src/main/java/info/xiaomo/flyway/controller/UserController.java b/flyway/src/main/java/info/xiaomo/flyway/controller/UserController.java new file mode 100644 index 00000000..268764e5 --- /dev/null +++ b/flyway/src/main/java/info/xiaomo/flyway/controller/UserController.java @@ -0,0 +1,35 @@ +package info.xiaomo.flyway.controller; + +import info.xiaomo.core.base.Result; +import info.xiaomo.flyway.dao.UserDao; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +/** + * @author : xiaomo + */ +@RestController +@RequestMapping("/api/users") +public class UserController { + + private final UserDao userDao; + + public UserController(UserDao userDao) { + this.userDao = userDao; + } + + @GetMapping("/count") + public Result count() { + return new Result<>(userDao.count()); + } + + @GetMapping("/{id}") + public Result> byId(@PathVariable long id) { + return new Result<>(userDao.findById(id)); + } + +} \ No newline at end of file diff --git a/flyway/src/main/java/info/xiaomo/flyway/dao/UserDao.java b/flyway/src/main/java/info/xiaomo/flyway/dao/UserDao.java new file mode 100644 index 00000000..b4a61a29 --- /dev/null +++ b/flyway/src/main/java/info/xiaomo/flyway/dao/UserDao.java @@ -0,0 +1,31 @@ +package info.xiaomo.flyway.dao; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +import java.util.Map; + +/** + * 用 JdbcTemplate 读取被 Flyway 迁移后的表, 纯粹演示"迁移脚本 + 数据访问"的配合。 + * + * @author : xiaomo + */ +@Repository +public class UserDao { + + private final JdbcTemplate jdbcTemplate; + + public UserDao(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public int count() { + Integer count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM users", Integer.class); + return count == null ? 0 : count; + } + + public Map findById(long id) { + return jdbcTemplate.queryForMap("SELECT id, name, email FROM users WHERE id = ?", id); + } + +} \ No newline at end of file diff --git a/flyway/src/main/resources/config/application.properties b/flyway/src/main/resources/config/application.properties new file mode 100644 index 00000000..7091b017 --- /dev/null +++ b/flyway/src/main/resources/config/application.properties @@ -0,0 +1,18 @@ +logging.config=classpath:config/logback-dev.xml +server.port=8080 + +server.max-http-header-size=20971520 + +spring.jackson.date-format=yyyy-MM-dd HH:mm:ss +spring.jackson.time-zone=GMT+8 + +# 生产/本地演示连 MySQL; 测试里会被 @SpringBootTest 的属性覆盖成内嵌 H2 +spring.datasource.url=jdbc:mysql://ip:3306/flyway_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC +spring.datasource.username=你的账号 +spring.datasource.password=你的密码 +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +spring.flyway.enabled=true +spring.flyway.locations=classpath:db/migration +# 已有数据库且里面已经有表时, 需要 baseline 才能让 flyway 接管, 这里保险起见直接开启 +spring.flyway.baseline-on-migrate=true \ No newline at end of file diff --git a/flyway/src/main/resources/config/logback-dev.xml b/flyway/src/main/resources/config/logback-dev.xml new file mode 100644 index 00000000..39e7cc65 --- /dev/null +++ b/flyway/src/main/resources/config/logback-dev.xml @@ -0,0 +1,17 @@ + + + + + + + [%d{yyyy-MM-dd HH:mm:ss} [%thread] %highlight(%-5level) %cyan(%logger{15}) - %highlight(%msg) %n + + + + + + + + + + \ No newline at end of file diff --git a/flyway/src/main/resources/db/migration/V1__create_user_table.sql b/flyway/src/main/resources/db/migration/V1__create_user_table.sql new file mode 100644 index 00000000..f1c07a41 --- /dev/null +++ b/flyway/src/main/resources/db/migration/V1__create_user_table.sql @@ -0,0 +1,5 @@ +CREATE TABLE users ( + id BIGINT PRIMARY KEY, + name VARCHAR(64) NOT NULL, + email VARCHAR(128) NOT NULL UNIQUE +); \ No newline at end of file diff --git a/flyway/src/main/resources/db/migration/V2__seed_users.sql b/flyway/src/main/resources/db/migration/V2__seed_users.sql new file mode 100644 index 00000000..cb25ecb5 --- /dev/null +++ b/flyway/src/main/resources/db/migration/V2__seed_users.sql @@ -0,0 +1,3 @@ +INSERT INTO users (id, name, email) VALUES +(1, 'xiaomo', 'xiaomo@example.com'), +(2, 'guest', 'guest@example.com'); \ No newline at end of file diff --git a/flyway/src/test/java/info/xiaomo/flyway/FlywayMigrationTest.java b/flyway/src/test/java/info/xiaomo/flyway/FlywayMigrationTest.java new file mode 100644 index 00000000..79516bc7 --- /dev/null +++ b/flyway/src/test/java/info/xiaomo/flyway/FlywayMigrationTest.java @@ -0,0 +1,46 @@ +package info.xiaomo.flyway; + +import info.xiaomo.flyway.FlywayMain; +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * 用内嵌 H2 跑一遍迁移脚本: 启动时会自动执行 V1/V2, 测试只验证结果。 + * 不需要 MySQL, 所以 CI 里 mvn clean install 也能稳定通过。 + */ +@SpringBootTest(classes = FlywayMain.class, properties = { + "spring.datasource.url=jdbc:h2:mem:flyway;DB_CLOSE_DELAY=-1", + "spring.datasource.username=sa", + "spring.datasource.password=", + "spring.datasource.driver-class-name=org.h2.Driver" +}) +class FlywayMigrationTest { + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private Flyway flyway; + + @Test + void 迁移脚本执行后表结构和种子数据就绪() { + Integer count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM users", Integer.class); + assertThat(count).isEqualTo(2); + + String name = jdbcTemplate.queryForObject("SELECT name FROM users WHERE id = 1", String.class); + assertThat(name).isEqualTo("xiaomo"); + } + + @Test + void 迁移历史记录了两次成功的版本() { + // 直接用 Flyway 的元数据接口查询, 比手写 SQL 查 flyway_schema_history 更健壮 + // (Flyway 用自己的引号规则建表, 手写裸 SQL 在 H2 上容易因大小写不匹配)。 + assertThat(flyway.info().applied()).hasSize(2); + } + +} \ No newline at end of file diff --git a/kafka/pom.xml b/kafka/pom.xml new file mode 100644 index 00000000..ba18815d --- /dev/null +++ b/kafka/pom.xml @@ -0,0 +1,47 @@ + + + + xiaomo + info.xiaomo + 2020.1 + + 4.0.0 + + kafka + + + + info.xiaomo + core + 2020.1 + + + + org.springframework.boot + spring-boot-starter-kafka + + + org.springframework.boot + spring-boot-starter-kafka-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + \ No newline at end of file diff --git a/kafka/src/main/java/info/xiaomo/kafka/KafkaMain.java b/kafka/src/main/java/info/xiaomo/kafka/KafkaMain.java new file mode 100644 index 00000000..07585dd3 --- /dev/null +++ b/kafka/src/main/java/info/xiaomo/kafka/KafkaMain.java @@ -0,0 +1,27 @@ +package info.xiaomo.kafka; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration; +import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * Kafka 启动器。 + * + *

KafkaTemplate 负责发消息, @KafkaListener 负责收消息, 二者之间靠 topic 解耦。 + * 测试用 @EmbeddedKafka 起一个内嵌 broker, 所以单测不依赖任何外部服务。 + * + * @author : xiaomo + */ +@Configuration +@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class}) +@ComponentScan("info.xiaomo.kafka") +public class KafkaMain { + + public static void main(String[] args) { + SpringApplication.run(KafkaMain.class, args); + } + +} \ No newline at end of file diff --git a/kafka/src/main/java/info/xiaomo/kafka/config/KafkaTopicConfig.java b/kafka/src/main/java/info/xiaomo/kafka/config/KafkaTopicConfig.java new file mode 100644 index 00000000..a25b7250 --- /dev/null +++ b/kafka/src/main/java/info/xiaomo/kafka/config/KafkaTopicConfig.java @@ -0,0 +1,20 @@ +package info.xiaomo.kafka.config; + +import org.apache.kafka.clients.admin.NewTopic; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 声明式创建 topic。生产环境通常由运维或 KafkaAdmin 统一创建, 这里只为示例方便。 + * + * @author : xiaomo + */ +@Configuration +public class KafkaTopicConfig { + + @Bean + public NewTopic orderEventsTopic() { + return new NewTopic("order-events", 1, (short) 1); + } + +} \ No newline at end of file diff --git a/kafka/src/main/java/info/xiaomo/kafka/controller/OrderController.java b/kafka/src/main/java/info/xiaomo/kafka/controller/OrderController.java new file mode 100644 index 00000000..d8fd7999 --- /dev/null +++ b/kafka/src/main/java/info/xiaomo/kafka/controller/OrderController.java @@ -0,0 +1,29 @@ +package info.xiaomo.kafka.controller; + +import info.xiaomo.core.base.Result; +import info.xiaomo.kafka.service.OrderEventProducer; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author : xiaomo + */ +@RestController +@RequestMapping("/api/orders") +public class OrderController { + + private final OrderEventProducer producer; + + public OrderController(OrderEventProducer producer) { + this.producer = producer; + } + + @PostMapping + public Result create(@RequestParam String orderId) { + producer.publish(orderId); + return new Result<>("已发布订单事件: " + orderId); + } + +} \ No newline at end of file diff --git a/kafka/src/main/java/info/xiaomo/kafka/service/OrderEventConsumer.java b/kafka/src/main/java/info/xiaomo/kafka/service/OrderEventConsumer.java new file mode 100644 index 00000000..6dcfa05b --- /dev/null +++ b/kafka/src/main/java/info/xiaomo/kafka/service/OrderEventConsumer.java @@ -0,0 +1,23 @@ +package info.xiaomo.kafka.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +/** + * 消费者。@KafkaListener 的方法参数会按配置的 value 反序列化器直接转成对应类型。 + * + * @author : xiaomo + */ +@Component +public class OrderEventConsumer { + + private static final Logger log = LoggerFactory.getLogger(OrderEventConsumer.class); + + @KafkaListener(topics = "order-events", groupId = "order-service") + public void onOrder(String orderId) { + log.info("收到订单事件: {}", orderId); + } + +} \ No newline at end of file diff --git a/kafka/src/main/java/info/xiaomo/kafka/service/OrderEventProducer.java b/kafka/src/main/java/info/xiaomo/kafka/service/OrderEventProducer.java new file mode 100644 index 00000000..310105cb --- /dev/null +++ b/kafka/src/main/java/info/xiaomo/kafka/service/OrderEventProducer.java @@ -0,0 +1,24 @@ +package info.xiaomo.kafka.service; + +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; + +/** + * 生产者。send 默认是异步的, 返回的 CompletableFuture 上可以挂回调, 示例里保持最简。 + * + * @author : xiaomo + */ +@Service +public class OrderEventProducer { + + private final KafkaTemplate kafkaTemplate; + + public OrderEventProducer(KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + } + + public void publish(String orderId) { + kafkaTemplate.send("order-events", orderId); + } + +} \ No newline at end of file diff --git a/kafka/src/main/resources/config/application.properties b/kafka/src/main/resources/config/application.properties new file mode 100644 index 00000000..a5431d3b --- /dev/null +++ b/kafka/src/main/resources/config/application.properties @@ -0,0 +1,16 @@ +logging.config=classpath:config/logback-dev.xml +server.port=8080 + +server.max-http-header-size=20971520 + +spring.jackson.date-format=yyyy-MM-dd HH:mm:ss +spring.jackson.time-zone=GMT+8 + +# kafka +spring.kafka.bootstrap-servers=localhost:9092 +spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer +spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer +spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer +spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer +spring.kafka.consumer.group-id=order-service +spring.kafka.consumer.auto-offset-reset=earliest \ No newline at end of file diff --git a/kafka/src/main/resources/config/logback-dev.xml b/kafka/src/main/resources/config/logback-dev.xml new file mode 100644 index 00000000..5eae41c2 --- /dev/null +++ b/kafka/src/main/resources/config/logback-dev.xml @@ -0,0 +1,18 @@ + + + + + + + [%d{yyyy-MM-dd HH:mm:ss} [%thread] %highlight(%-5level) %cyan(%logger{15}) - %highlight(%msg) %n + + + + + + + + + + + \ No newline at end of file diff --git a/kafka/src/test/java/info/xiaomo/kafka/OrderKafkaTest.java b/kafka/src/test/java/info/xiaomo/kafka/OrderKafkaTest.java new file mode 100644 index 00000000..92a69628 --- /dev/null +++ b/kafka/src/test/java/info/xiaomo/kafka/OrderKafkaTest.java @@ -0,0 +1,47 @@ +package info.xiaomo.kafka; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.test.context.EmbeddedKafka; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @EmbeddedKafka 会在测试里起一个真实的 Kafka broker(内嵌、随机端口),不依赖外部服务。 + * 测试里的监听器用独立的 groupId, 与 main 里的消费者互不抢消息(不同消费组各自收到一份)。 + */ +@SpringBootTest(classes = KafkaMain.class) +@EmbeddedKafka(partitions = 1, topics = "order-events") +class OrderKafkaTest { + + static final BlockingQueue received = new LinkedBlockingQueue<>(); + + @Autowired + private KafkaTemplate kafkaTemplate; + + @Test + void 生产者发送的消息能被监听器收到() throws Exception { + kafkaTemplate.send("order-events", "order-1001").get(5, TimeUnit.SECONDS); + + String orderId = received.poll(10, TimeUnit.SECONDS); + assertThat(orderId).isEqualTo("order-1001"); + } + + @TestConfiguration + static class TestConsumerConfig { + + @KafkaListener(topics = "order-events", groupId = "order-kafka-test") + void listen(String orderId) { + received.add(orderId); + } + } + +} \ No newline at end of file diff --git a/mail/pom.xml b/mail/pom.xml new file mode 100644 index 00000000..4690b434 --- /dev/null +++ b/mail/pom.xml @@ -0,0 +1,52 @@ + + + + xiaomo + info.xiaomo + 2020.1 + + 4.0.0 + + mail + + + + info.xiaomo + core + 2020.1 + + + + org.springframework.boot + spring-boot-starter-mail + + + org.springframework.boot + spring-boot-starter-test + test + + + com.icegreen + greenmail-junit5 + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + \ No newline at end of file diff --git a/mail/src/main/java/info/xiaomo/mail/MailMain.java b/mail/src/main/java/info/xiaomo/mail/MailMain.java new file mode 100644 index 00000000..83d7833b --- /dev/null +++ b/mail/src/main/java/info/xiaomo/mail/MailMain.java @@ -0,0 +1,27 @@ +package info.xiaomo.mail; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration; +import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * 邮件发送启动器。 + * + *

spring-boot-starter-mail 用 JavaMailSender 统一收发邮件, SMTP 地址在 application.properties 配置。 + * 测试用 GreenMail 在内嵌 SMTP 服务器上接收邮件, 因此不依赖真实邮箱。 + * + * @author : xiaomo + */ +@Configuration +@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class}) +@ComponentScan("info.xiaomo.mail") +public class MailMain { + + public static void main(String[] args) { + SpringApplication.run(MailMain.class, args); + } + +} \ No newline at end of file diff --git a/mail/src/main/java/info/xiaomo/mail/controller/MailController.java b/mail/src/main/java/info/xiaomo/mail/controller/MailController.java new file mode 100644 index 00000000..2e41a39a --- /dev/null +++ b/mail/src/main/java/info/xiaomo/mail/controller/MailController.java @@ -0,0 +1,37 @@ +package info.xiaomo.mail.controller; + +import info.xiaomo.core.base.Result; +import info.xiaomo.mail.service.MailService; +import jakarta.mail.MessagingException; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author : xiaomo + */ +@RestController +@RequestMapping("/api/mail") +public class MailController { + + private final MailService mailService; + + public MailController(MailService mailService) { + this.mailService = mailService; + } + + @PostMapping("/simple") + public Result sendSimple(@RequestParam String to, @RequestParam String subject, @RequestParam String text) { + mailService.sendSimple(to, subject, text); + return new Result<>("已发送纯文本邮件"); + } + + @PostMapping("/html") + public Result sendHtml(@RequestParam String to, @RequestParam String subject, @RequestParam String html) + throws MessagingException { + mailService.sendHtml(to, subject, html); + return new Result<>("已发送 HTML 邮件"); + } + +} \ No newline at end of file diff --git a/mail/src/main/java/info/xiaomo/mail/service/MailService.java b/mail/src/main/java/info/xiaomo/mail/service/MailService.java new file mode 100644 index 00000000..b47f5847 --- /dev/null +++ b/mail/src/main/java/info/xiaomo/mail/service/MailService.java @@ -0,0 +1,43 @@ +package info.xiaomo.mail.service; + +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +/** + * 两种常见邮件: 纯文本用 SimpleMailMessage, 富文本/带附件用 MimeMessageHelper。 + * + * @author : xiaomo + */ +@Service +public class MailService { + + private final JavaMailSender mailSender; + + public MailService(JavaMailSender mailSender) { + this.mailSender = mailSender; + } + + public void sendSimple(String to, String subject, String text) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom("demo@springboot-unity.local"); + message.setTo(to); + message.setSubject(subject); + message.setText(text); + mailSender.send(message); + } + + public void sendHtml(String to, String subject, String html) throws MessagingException { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, "UTF-8"); + helper.setFrom("demo@springboot-unity.local"); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(html, true); + mailSender.send(message); + } + +} \ No newline at end of file diff --git a/mail/src/main/resources/config/application.properties b/mail/src/main/resources/config/application.properties new file mode 100644 index 00000000..0f6d4573 --- /dev/null +++ b/mail/src/main/resources/config/application.properties @@ -0,0 +1,15 @@ +logging.config=classpath:config/logback-dev.xml +server.port=8080 + +server.max-http-header-size=20971520 + +spring.jackson.date-format=yyyy-MM-dd HH:mm:ss +spring.jackson.time-zone=GMT+8 + +# 改成你自己的 SMTP 服务 +spring.mail.host=smtp.example.com +spring.mail.port=587 +spring.mail.username=你的账号 +spring.mail.password=你的密码 +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.starttls.enable=true \ No newline at end of file diff --git a/mail/src/main/resources/config/logback-dev.xml b/mail/src/main/resources/config/logback-dev.xml new file mode 100644 index 00000000..39e7cc65 --- /dev/null +++ b/mail/src/main/resources/config/logback-dev.xml @@ -0,0 +1,17 @@ + + + + + + + [%d{yyyy-MM-dd HH:mm:ss} [%thread] %highlight(%-5level) %cyan(%logger{15}) - %highlight(%msg) %n + + + + + + + + + + \ No newline at end of file diff --git a/mail/src/test/java/info/xiaomo/mail/MailServiceTest.java b/mail/src/test/java/info/xiaomo/mail/MailServiceTest.java new file mode 100644 index 00000000..339e8757 --- /dev/null +++ b/mail/src/test/java/info/xiaomo/mail/MailServiceTest.java @@ -0,0 +1,59 @@ +package info.xiaomo.mail; + +import com.icegreen.greenmail.junit5.GreenMailExtension; +import com.icegreen.greenmail.util.ServerSetupTest; +import info.xiaomo.mail.service.MailService; +import jakarta.mail.internet.MimeMessage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * GreenMail 在测试里起一个真实的 SMTP 服务器(端口 3025), 把 spring.mail 指向它即可离线验证发信逻辑。 + */ +@SpringBootTest(classes = MailMain.class, properties = { + "spring.mail.host=127.0.0.1", + "spring.mail.port=3025", + "spring.mail.username=", + "spring.mail.password=", + "spring.mail.properties.mail.smtp.auth=false", + "spring.mail.properties.mail.smtp.starttls.enable=false" +}) +class MailServiceTest { + + @RegisterExtension + static GreenMailExtension greenMail = new GreenMailExtension(ServerSetupTest.SMTP); + + @Autowired + private MailService mailService; + + @BeforeEach + void 清空收件箱() throws Exception { + greenMail.purgeEmailFromAllMailboxes(); + } + + @Test + void 发送纯文本邮件() throws Exception { + mailService.sendSimple("to@example.com", "验证码", "你的验证码是 123456"); + + assertThat(greenMail.waitForIncomingEmail(5000, 1)).isTrue(); + MimeMessage received = greenMail.getReceivedMessages()[0]; + assertThat(received.getSubject()).isEqualTo("验证码"); + assertThat(received.getAllRecipients()[0].toString()).isEqualTo("to@example.com"); + } + + @Test + void 发送HTML邮件() throws Exception { + mailService.sendHtml("to-html@example.com", "通知", "

你好

"); + + assertThat(greenMail.waitForIncomingEmail(5000, 1)).isTrue(); + MimeMessage received = greenMail.getReceivedMessages()[0]; + assertThat(received.getSubject()).isEqualTo("通知"); + assertThat(received.getContentType()).startsWith("text/html"); + } + +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 73bbbfba..bdc50c27 100644 --- a/pom.xml +++ b/pom.xml @@ -60,6 +60,14 @@ ratelimit graphql + + kafka + + mail + + elasticsearch + + flyway pom @@ -88,6 +96,8 @@ 2.22.0 2.0.65 + + 2.1.3 2.2.0 @@ -205,6 +215,11 @@ poi ${poi.version} + + com.icegreen + greenmail-junit5 + ${greenmail.version} + From 607aa00816aac875010674972b4f2ee31f7090fc Mon Sep 17 00:00:00 2001 From: Evan Hu Date: Mon, 14 Sep 2026 01:08:11 +0900 Subject: [PATCH 2/2] fix: make actuator health check deterministic regardless of disk space --- .../health/DiskSpaceRatioHealthIndicator.java | 14 ++++++++++---- .../info/xiaomo/actuator/ActuatorEndpointTest.java | 5 ++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/actuator/src/main/java/info/xiaomo/actuator/health/DiskSpaceRatioHealthIndicator.java b/actuator/src/main/java/info/xiaomo/actuator/health/DiskSpaceRatioHealthIndicator.java index 8cddb33b..bd6b8179 100644 --- a/actuator/src/main/java/info/xiaomo/actuator/health/DiskSpaceRatioHealthIndicator.java +++ b/actuator/src/main/java/info/xiaomo/actuator/health/DiskSpaceRatioHealthIndicator.java @@ -1,5 +1,6 @@ package info.xiaomo.actuator.health; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.health.contributor.Health; import org.springframework.boot.health.contributor.HealthIndicator; import org.springframework.stereotype.Component; @@ -16,9 +17,14 @@ public class DiskSpaceRatioHealthIndicator implements HealthIndicator { /** - * 可用空间低于该比例就判定为不健康。 + * 可用空间低于该比例就判定为不健康, 可通过配置覆盖(测试里把它设成 0 以消除对磁盘的依赖)。 */ - private static final double THRESHOLD = 0.05; + private final double threshold; + + public DiskSpaceRatioHealthIndicator( + @Value("${actuator.disk-space-ratio.threshold:0.05}") double threshold) { + this.threshold = threshold; + } @Override public Health health() { @@ -30,12 +36,12 @@ public Health health() { } double freeRatio = (double) free / total; - Health.Builder builder = freeRatio >= THRESHOLD ? Health.up() : Health.down(); + Health.Builder builder = freeRatio >= threshold ? Health.up() : Health.down(); return builder .withDetail("totalBytes", total) .withDetail("freeBytes", free) .withDetail("freeRatio", String.format("%.4f", freeRatio)) - .withDetail("threshold", THRESHOLD) + .withDetail("threshold", threshold) .build(); } diff --git a/actuator/src/test/java/info/xiaomo/actuator/ActuatorEndpointTest.java b/actuator/src/test/java/info/xiaomo/actuator/ActuatorEndpointTest.java index c80698c0..ce04c6d7 100644 --- a/actuator/src/test/java/info/xiaomo/actuator/ActuatorEndpointTest.java +++ b/actuator/src/test/java/info/xiaomo/actuator/ActuatorEndpointTest.java @@ -10,7 +10,10 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@SpringBootTest(classes = ActuatorMain.class) +@SpringBootTest(classes = ActuatorMain.class, properties = { + // 把自定义磁盘比例阈值的判定下限改成 0, 让测试不依赖真实磁盘空间, 避免磁盘快满时误报不健康 + "actuator.disk-space-ratio.threshold=0" +}) @AutoConfigureMockMvc class ActuatorEndpointTest {