Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

## 模块一览

一共 26 个模块。`core` 是被其他模块共同依赖的基础包,其余每个模块各自演示一项技术。
一共 30 个模块。`core` 是被其他模块共同依赖的基础包,其余每个模块各自演示一项技术。

| 模块 | 演示内容 | 启动类 | 需要的外部服务 |
| --- | --- | --- | --- |
Expand Down Expand Up @@ -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` 即可跑通,也可以当作各自技术点的可执行文档来读。

## 技术栈

Expand All @@ -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 管理,不在本项目中单独指定。
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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() {
Expand All @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
16 changes: 16 additions & 0 deletions changeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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。
41 changes: 41 additions & 0 deletions elasticsearch/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>xiaomo</artifactId>
<groupId>info.xiaomo</groupId>
<version>2020.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>elasticsearch</artifactId>

<dependencies>
<dependency>
<groupId>info.xiaomo</groupId>
<artifactId>core</artifactId>
<version>2020.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -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 启动器。
*
* <p>和 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);
}

}
Original file line number Diff line number Diff line change
@@ -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<Article> index(@RequestBody Article article) {
return new Result<>(service.index(article));
}

@GetMapping("/search")
public Result<List<Article>> search(@RequestParam String title) {
return new Result<>(service.searchByTitle(title));
}

@GetMapping("/author/{author}")
public Result<List<Article>> byAuthor(@PathVariable String author) {
return new Result<>(service.findByAuthor(author));
}

}
Original file line number Diff line number Diff line change
@@ -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<String> tags;

}
Original file line number Diff line number Diff line change
@@ -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<Article, String> {

List<Article> findByTitleContaining(String keyword);

List<Article> findByAuthor(String author);

}
Original file line number Diff line number Diff line change
@@ -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<Article> searchByTitle(String keyword) {
return repository.findByTitleContaining(keyword);
}

public List<Article> findByAuthor(String author) {
return repository.findByAuthor(author);
}

}
10 changes: 10 additions & 0 deletions elasticsearch/src/main/resources/config/application.properties
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions elasticsearch/src/main/resources/config/logback-dev.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>

<configuration scan="true">

<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<encoder charset="UTF-8">
<pattern>[%d{yyyy-MM-dd HH:mm:ss} [%thread] %highlight(%-5level) %cyan(%logger{15}) - %highlight(%msg) %n</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="stdout"/>
</root>

<logger name="info.xiaomo" level="DEBUG"/>

</configuration>
Loading
Loading