选型不是选「最好的库」
同一类能力通常有三到五个成熟实现。它们的差别很少是「能不能用」,更多是:出问题时能不能查到原因、团队里有没有人熟悉、升级路径是否清楚、文档和社区是否还活跃。
这份附录把 libraryd 用到的依赖集中列出来,说明每个选择的约束和替代方案。本书的选择服务于教学,不一定是你的项目的最优解。 读的时候重点看「为什么不选另一个」,而不是记结论。
JDBC 驱动与连接池
PostgreSQL JDBC 驱动
| 项 | 值 |
|---|---|
| 坐标 | org.postgresql:postgresql |
| 版本 | 42.7.14 |
| 作用 | 实现 JDBC 接口,负责协议、类型映射与 SSL |
PostgreSQL 只有这一个官方维护的 JDBC 驱动,没有真正的替代品。需要注意的是版本与服务器版本的兼容性:42.x 支持 PostgreSQL 9.4 到 17,JDK 8 及以上。驱动升级通常向后兼容,但涉及 bytea、jsonb、timestamptz 时,建议在集成测试里覆盖。
连接池:为什么是 HikariCP
| 连接池 | 特点 | 适用 |
|---|---|---|
| HikariCP | 轻量、默认配置合理、Spring Boot 默认 | 绝大多数服务端项目 |
| Apache DBCP2 | 老牌、配置项多 | 遗留系统、需要特定行为 |
| Agroal | 性能好、Quarkus 默认 | Quarkus 项目 |
| c3p0 | 历史悠久,维护活跃度低 | 不建议新项目使用 |
HikariCP 的优势不是「最快」这种无法脱离环境验证的结论,而是三点:代码量小、默认值合理、泄漏检测和指标暴露清楚。本书第 3.2 节会讲 maximumPoolSize、connectionTimeout、leakDetectionThreshold 三个最常改的参数。
连接池不是越大越好。数据库的连接数是有限资源,池太大只会把压力推到数据库侧,表现为锁等待和上下文切换。经验起点是「CPU 核数 × 2 + 磁盘数」,然后用真实压测调整;本书给的是测量方法,不是固定值。
数据库迁移:Flyway 与 Liquibase
| 项 | Flyway | Liquibase |
|---|---|---|
| 迁移描述 | SQL 文件 | XML/YAML/JSON/SQL |
| 学习成本 | 低 | 中 |
| 跨数据库抽象 | 弱(SQL 方言自理) | 强(changeSet 抽象) |
| 回滚 | 社区版有限 | 支持 rollback |
| 适合 | 单一数据库、SQL 可控 | 多数据库、需要审计与回滚 |
本书选 Flyway,因为读者需要看清真实 SQL,而不是被抽象层遮住。V1__init.sql、V2__book_search_index.sql 这样的命名让 schema 演进一目了然。
Flyway 的坑集中在三处:校验和(修改已执行的迁移会导致启动失败)、并发启动(多个实例同时迁移需要锁)、基线(已有数据库接入时要 baselineOnMigrate)。附录 C 会给出对应报错。
HTTP 框架:为什么是 Javalin
| 方案 | 模型 | 特点 |
|---|---|---|
| Javalin | 轻量、Jetty 之上 | 路由直观、启动快、依赖少 |
| Jetty 原生 | Servlet / Handler | 控制细,样板代码多 |
| Undertow | Handler | 性能好,API 偏底层 |
| Netty | 事件循环 | 适合自定义协议,Web 开发成本高 |
| Spring MVC | 注解 + 容器 | 生态完整,学习曲线陡 |
| Quarkus / Micronaut | 编译期装配 | 启动快,框架约定强 |
本书需要的是一个「能看到路由、中间件、错误处理原始形态」的框架。Javalin 的 API 足够薄:一个 cfg.routes.post(...) 就是一条路由,一个 cfg.routes.before(...) 就是一段中间件,异常映射也是显式注册。读者不会把时间花在框架约定上。
如果你的项目需要认证授权、内容协商、参数解析、OpenAPI 生成等一整套能力,Spring MVC 更省事;如果你需要极致启动速度,Quarkus 或 Micronaut 更合适。Javalin 的定位是「小服务、清晰边界」。
JSON:Jackson
| 库 | 特点 | 注意 |
|---|---|---|
| Jackson | 生态最广、模块多 | 默认配置偏宽松,需显式收紧 |
| Gson | API 简单 | 对 record、Java Time 支持较弱 |
| JSON-B | Jakarta 标准 | 实现少,生态不如 Jackson |
| Moshi | Kotlin 友好 | Java 生态使用较少 |
Jackson 的关键不是「怎么序列化」,而是「默认允许什么」。本书会显式配置:
FAIL_ON_UNKNOWN_PROPERTIES:请求出现未知字段时是失败还是忽略,要按契约决定。JavaTimeModule:LocalDate默认不能直接序列化,必须注册模块。WRITE_DATES_AS_TIMESTAMPS:关闭后输出 ISO-8601 字符串。- 循环引用:用 DTO 切断,而不是靠
@JsonIgnore到处打补丁。
参数校验:Jakarta Validation
| 方案 | 优点 | 代价 |
|---|---|---|
| Jakarta Validation + Hibernate Validator | 标准注解、生态好 | 需要 EL 实现,错误消息要自己组织 |
| 手写校验 | 无依赖、逻辑直观 | 规则一多就重复 |
Spring 的 Validator 接口 | 与 Spring 集成好 | 绑定框架 |
本书两者都用:字段级规则用注解,跨字段规则(如 dueOn 必须晚于 borrowedOn)手写。只靠注解会把业务规则拆散,只靠手写会重复大量非空、长度、范围判断。
Hibernate Validator 9.x 需要 Jakarta Validation 3.1 与 EL 实现(本书用 Expressly 6.0.0)。缺少 EL 时,消息插值会退化成 {jakarta.validation.constraints.NotBlank.message} 这样的原始模板,这是很常见的配置遗漏。
日志:SLF4J + Logback
| 组合 | 说明 |
|---|---|
| SLF4J + Logback | 本书选择;Spring Boot 默认 |
| SLF4J + Log4j2 | 异步日志性能好,配置更复杂 |
| JUL | JDK 自带,能力有限 |
SLF4J 是门面,Logback 是实现。应用代码只依赖 slf4j-api,实现由运行时提供,这样测试和部署可以换实现而不改代码。
结构化日志用 logstash-logback-encoder 9.0。它把日志事件编码成 JSON,字段名与 Elastic Common Schema 接近。代价是每行日志更大,磁盘和网络成本上升;第 12.3 节会讲采样和脱敏。
指标与追踪
| 能力 | 本书选择 | 替代 |
|---|---|---|
| 指标门面 | Micrometer 1.17.2 | 直接依赖 Prometheus client |
| 指标暴露 | micrometer-registry-prometheus | 自建 /metrics 文本输出 |
| 追踪 | OpenTelemetry SDK 1.67.0 | Micrometer Tracing、Brave |
| 导出 | opentelemetry-exporter-logging(教学) | OTLP exporter + Collector |
Micrometer 的价值是「一次埋点,多种后端」:计数器、计时器、Gauge 的 API 不绑定 Prometheus。生产环境通常接 Prometheus + Grafana;本书用日志导出器是为了让读者在本地就能看到 span,不需要额外部署 Collector。
追踪的学习重点是上下文传播:traceparent 头怎么解析、span 怎么嵌套、异步线程里怎么不丢上下文。换导出器不影响这些概念。
缓存
| 层 | 本书选择 | 替代 | 注意 |
|---|---|---|---|
| 本地缓存 | Caffeine 3.3.1 | Guava Cache、Ehcache | 容量、TTL、失效顺序 |
| Redis 客户端 | Jedis 8.0.2 | Lettuce、Redisson | 连接模型、线程安全 |
| 分布式缓存 | Redis 7 | Memcached、Hazelcast | 一致性、序列化、雪崩 |
Caffeine 的 API 比 Guava Cache 更现代,支持异步加载和更细的淘汰策略。Jedis 是同步客户端,API 直观,适合教学;Lettuce 基于 Netty,支持异步和响应式,高并发场景更常见。
缓存章节的重点不是 API,而是三个问题:缓存什么、什么时候失效、失效失败怎么办。第 14 章会用具体的书目详情缓存演示。
测试
| 能力 | 选择 | 说明 |
|---|---|---|
| 测试框架 | JUnit Jupiter 5.14.4 | 与卷一一致 |
| 容器 | Testcontainers 1.21.4 | PostgreSQL 模块 |
| HTTP 断言 | JDK HttpClient + JUnit 断言 | 不额外引入 REST Assured |
| 数据库断言 | JDBC 直接查询 | 看清真实数据 |
本书不引入 Mockito、AssertJ、REST Assured。不是它们不好,而是每多一个库,读者就多一层「这是库的行为还是框架的行为」的判断成本。第 15 章的端到端测试用 JDK 自带的 HttpClient,断言用 JUnit 原生 API,足够覆盖契约。
Testcontainers 需要 Docker。CI 环境没有 Docker 时,可以把集成测试放到独立 profile,单元测试仍然可跑;不要用 H2 假装 PostgreSQL,SQL 方言和事务行为差异会让测试失去意义。
构建与镜像
| 能力 | 选择 | 替代 |
|---|---|---|
| 编译 | maven-compiler-plugin 3.16.0 | Gradle、javac 直接调用 |
| 单元测试 | Surefire 3.6.1 | — |
| 集成测试 | Failsafe 3.6.1 | 用 Surefire 跑全部 |
| 覆盖率 | JaCoCo 0.8.16 | Cobertura(已过时) |
| 静态分析 | Checkstyle 10.26.1、SpotBugs 4.10.5.0 | PMD、Error Prone |
| fat jar | Shade 3.6.2 | Assembly、Spring Boot 插件 |
| 镜像 | 多阶段 Dockerfile、Jib 3.5.2 | Buildpacks |
Shade 的坑是资源合并与签名文件:多个依赖包含 META-INF/services 时需要 ServicesResourceTransformer,签名文件要过滤掉,否则运行时报 Invalid signature file digest。第 16.1 节会给出完整配置。
Jib 不需要 Docker daemon 就能构建镜像,适合 CI;多阶段 Dockerfile 更透明,适合学习和排查。两者不是互斥关系,团队按流水线条件选择。
一张选型表
| 能力 | 本书 | 主要替代 | 选择理由 |
|---|---|---|---|
| JDBC 驱动 | PostgreSQL JDBC 42.7.14 | 无 | 官方唯一 |
| 连接池 | HikariCP 7.1.0 | DBCP2、Agroal | 轻量、默认合理 |
| 迁移 | Flyway 13.11.1 | Liquibase | SQL 透明 |
| HTTP | Javalin 7.2.4 | Spring MVC、Quarkus | API 薄,适合教学 |
| JSON | Jackson 2.22.4 | Gson、JSON-B | 生态与模块 |
| 校验 | Hibernate Validator 9.1.5 | 手写 | 标准注解 |
| 日志 | SLF4J + Logback 1.6.6 | Log4j2 | 默认组合 |
| 指标 | Micrometer 1.17.2 | Prometheus client | 门面解耦 |
| 追踪 | OpenTelemetry 1.67.0 | Micrometer Tracing | 标准协议 |
| 本地缓存 | Caffeine 3.3.1 | Guava Cache | API 现代 |
| Redis | Jedis 8.0.2 | Lettuce、Redisson | 同步、直观 |
| 测试 | JUnit 5 + Testcontainers | Mockito + H2 | 真实依赖 |
四个常见误判
- 「连接池越大并发越高」。 数据库连接是有限资源,池过大只会增加锁竞争和内存占用。先测再调。
- 「用 H2 测过就等于 PostgreSQL 没问题」。 SQL 方言、隔离级别、锁行为、
jsonb类型都可能不同。集成测试必须用真实数据库。 - 「日志越详细越好」。 日志有存储、网络和隐私成本。请求体、身份证号、token 不应默认打印。
- 「缓存能解决所有性能问题」。 缓存引入一致性问题和失效复杂度。没有测量过的热点,不要先加缓存。
小结
- 选型看约束:团队熟悉度、排查成本、升级路径,而不是「性能最好」。
- 本书的组合是教学导向:每一层都能看到原始形态,依赖数量控制在可解释范围内。
- 连接池、迁移、HTTP、JSON、校验、日志、指标、缓存、测试各有一个主选和一个备选。
- 最容易被忽略的不是库的 API,而是默认行为:Jackson 的未知字段、Flyway 的校验和、Shade 的签名文件、Hibernate Validator 的 EL。
- 是否替换某个库,先问「出问题时我能不能查到原因」。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。