《从零开发校园墙微信小程序》第 2 篇。这篇讲项目怎么起步、依赖怎么选、配置和密钥怎么管,以及整个后端最重要的一条原则:别在事件循环线程上阻塞 。
技术选型
| 项 | 版本 / 选择 |
|---|---|
| JDK | 17 |
| 构建 | Gradle(Kotlin DSL) |
| 框架 | Spring Boot 3.2.5,WebFlux (Netty) |
| 持久层 | MyBatis-Plus 3.5.5(阻塞)+ Spring Data R2DBC + r2dbc-mariadb(响应式) |
| 鉴权 | Sa-Token 1.42(reactor 版 starter) |
| 工具 | Hutool、Jsoup、imgscalr、MinIO SDK、springdoc |
为什么不是纯 WebFlux、也不是纯 Spring MVC? 项目最初是 Servlet / Tomcat + MyBatis,大量存量代码是阻塞的。WebFlux 是后来逐步迁移 的:高并发、高 IO 的读路径(列表、课表、反馈)先改成响应式,写路径和复杂业务保持阻塞,两套数据层并存 。这是真实项目里非常常见的状态。
依赖里值得注意的几行
// 响应式 Web:不要再同时引入 starter-web,
// 否则两个 web starter 并存时 Boot 默认会走 Servlet
implementation("org.springframework.boot:spring-boot-starter-webflux:3.3.1")
// 响应式数据库访问
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
implementation("org.mariadb:r2dbc-mariadb:1.2.2")
// 阻塞世界:MyBatis-Plus + JDBC
implementation("com.baomidou:mybatis-plus-boot-starter:3.5.5")
runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
// 让 Sa-Token 这类基于 ThreadLocal 的上下文,在 Reactor 切线程时自动恢复
implementation("io.micrometer:context-propagation:1.1.1")两个踩坑后写在注释里 的结论:
- 只留一个 web starter 。同时有
starter-web和starter-webflux时,Spring Boot 会默认走 Servlet,你以为在用 WebFlux,其实不是。 - 不要引入
**spring-boot-starter-data-jdbc**。它会和 R2DBC 并存时因为自动配置缺DataSourceProperties而启动失败;我们只需要starter-jdbc提供的 DataSource / Hikari。
如果你在国内网络下拉依赖慢,在 repositories 里把阿里云镜像放前面:
repositories {
maven { url = uri("https://maven.aliyun.com/repository/central") }
mavenCentral()
}配置分层:密钥永远不进仓库
这是我对新手最想强调的一点。配置分三层:
| 文件 | 内容 | 入 Git? |
|---|---|---|
application.yaml | 通用配置,不含任何真实密钥 | ✅ |
application-local.example.yaml | 配置模板,值全是占位符 | ✅ |
application-local.yaml | 真实的数据库、Redis、邮件、MinIO 连接 | ❌ .gitignore |
主配置里用占位符 + 环境变量兜底:
spring:
profiles:
active: local # 默认加载 application-local.yaml
datasource:
driver-class-name: org.mariadb.jdbc.Driver
username: ${DB_USERNAME:}
password: ${DB_PASSWORD:}
url: ${DB_URL:}
r2dbc:
url: ${DB_R2DBC_URL:} # 例:r2dbc:mariadb://127.0.0.1:3306/pzforest
username: ${DB_USERNAME:}
password: ${DB_PASSWORD:}
pool:
initial-size: 2
max-size: 10注意阻塞数据源和 R2DBC 数据源是两个独立的连接池 ,指向同一个库,用同一组账号。
几个「看起来无聊但救过命」的配置
1. 监控端点只开**health**
management:
endpoints:
web:
exposure:
include: health
endpoint:
health:
show-details: neverenv、heapdump、beans 这些端点一旦暴露,数据库口令、第三方 token 会整个泄露。部署脚本和负载均衡只需要 health 探活,够了。
2. Redis 一定要设超时
spring:
data:
redis:
timeout: 2s
lettuce:
pool:
max-active: 8
max-wait: 2s不设 max-wait 时 Lettuce 取连接会无限等待 。Redis 一抖,所有请求线程全挂住,用户看到的是「转圈到最后报错」。给 2 秒上限,让它快速失败,由上层降级。
3. 调度线程池别用默认的 1 个线程
spring:
task:
scheduling:
pool:
size: 4
thread-name-prefix: pzsl-sched-默认只有 1 个线程,意味着所有 @Scheduled 任务排队执行 。微博拉取一慢,教务登录队列、反馈汇总邮件会被连带拖停。拆成 4 个线程各干各的。
核心原则:别在 Netty 事件循环线程上阻塞
WebFlux 的 handler 直接跑在少量的 Netty 事件循环线程 上(比如 reactor-http-epoll-N),线程数一般等于 CPU 核数。如果你在里面做阻塞操作(MyBatis 查库、.block()、同步 HTTP 调用、读写文件),这条线程就被占住,整个服务的吞吐都跟着掉;Reactor 为了保护你,直接在检测到 block() 时抛异常:
IllegalStateException: block()/blockFirst()/blockLast() are blocking,
which is not supported in thread reactor-http-epoll-2解决模式 :把阻塞逻辑整体丢到弹性线程池 boundedElastic:
@PostMapping("/login")
public Mono<Result> login(@RequestBody Map<String, String> params, ServerWebExchange exchange) {
return Mono.fromCallable(() -> {
// 这里面可以放心写阻塞代码:HTTP 调微信、MyBatis、Redis……
return doLogin(params.get("code"));
}).subscribeOn(Schedulers.boundedElastic());
}记住这个套路:**Mono.fromCallable(阻塞逻辑).subscribeOn(Schedulers.boundedElastic())**。本项目里登录、图片上传、投稿前置校验、手动触发微博拉取,全是这个写法。
一个文件上传相关的坑
WebFlux 里接收 multipart/form-data 时,所有参数都要用**@RequestPart**,不要混用 @RequestParam:
@PostMapping(value = "/sendStatus", consumes = "multipart/form-data")
public Mono<Result> send_status(ServerWebExchange exchange,
@RequestPart("file") FilePart file,
@RequestPart(value = "text", required = false) String text,
@RequestPart(value = "mode", required = false) String modeStr) { ... }原因:@RequestParam 读 form 字段和 @RequestPart 会争抢请求体 ,body 被 FilePart 消费之后,@RequestParam 拿到的是空,表现为一个莫名其妙的 400。
统一返回结构
整个项目接口返回统一为:
{ "code": 200, "message": "查询成功", "data": { } }对应一个很小的 Result 类。前端只认 code:200 成功,401 登录失效(触发自动重登,见第 4 篇),其他按 message 提示。统一结构 + 全局异常处理(**GlobalExceptionHandler**) 能省掉大量前端兜底代码。
小结
- 迁移 WebFlux 要渐进 ,阻塞与响应式两套数据层可以并存;
- 只留一个 web starter,别引 data-jdbc;
- 密钥分三层管理,真实配置不入库;
- 监控只开 health,Redis 设超时,调度池别用默认;
- 所有阻塞逻辑 →
**Mono.fromCallable(...).subscribeOn(boundedElastic())**。
下一篇,我们把登录做出来。你会看到:为什么上面这条原则一到 Sa-Token 就多出一个新问题。