《Java 编程实战》附录 C 故障排查

按「构建、数据库、迁移、连接池、事务、HTTP、容器」七个层次组织 libraryd 的常见故障:给出真实报错原文、触发条件、定位命令与修复方式,并说明排查时为什么应先确认失败发生在哪一层,而不是先改配置。附录末尾附一页式诊断命令清单,便于按层逐项核对。

先定位层次,再改配置

服务端问题的排查顺序,比具体命令更重要。同一个「接口 500」,可能来自编译产物、数据库连接、SQL、事务、序列化或容器网络。先确认失败发生在哪一层,再决定看什么日志。

本书建议的顺序是:

编译/构建 → 进程启动 → 数据库连接 → 迁移 → SQL/事务 → HTTP 处理 → 容器/部署

每一层都有明确的失败信号:

层次典型信号先看什么
构建mvn package 失败Maven 的 [ERROR] 首行与模块名
启动进程退出、端口未监听启动日志最后 30 行、BindException
数据库连接启动慢、健康检查 DOWNJDBC URL、容器状态、pg_isready
迁移启动即失败Flyway 校验和、schema history 表
SQL/事务接口 500、超时、死锁数据库日志、pg_locks、SQL 参数
HTTP4xx/5xx、格式不对状态码、Content-Type、响应体
容器镜像启动即退出docker logs、端口映射、环境变量

下面按这个顺序列出真实报错与处理方式。所有输出都在本机 macOS + JDK 25.0.5 + PostgreSQL 17.11 上实际产生;你的行号、端口和对象名可能不同。

构建阶段

编译目标版本不支持

下面是一次用 compiler 插件 3.13.0 复现的原始输出(本书工程锁定 3.16.0,报错文案一致):

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.13.0:compile
(default-compile) on project libraryd-domain: Fatal error compiling:
error: release version 99 not supported -> [Help 1]

触发条件:maven.compiler.release 写了一个当前 JDK 不支持的版本,或者 Maven 实际使用的 JDK 低于目标版本。

排查:

mvn -version
java -version
mvn -q help:evaluate -Dexpression=maven.compiler.release -DforceStdout

mvn -version 里的 Java version 必须是 25.x。如果它显示 21 或 17,先修 JAVA_HOME,不要改 <release> 去迁就错误的 JDK。

依赖找不到

[ERROR] Failed to execute goal on project libraryd-web: Could not resolve dependencies
for project com.plume:libraryd-web:jar:1.0.0-SNAPSHOT
[ERROR] 	Could not find artifact com.example:does-not-exist:jar:99.99.99 in central
(https://repo.maven.apache.org/maven2)

触发条件:坐标拼错、版本不存在、私服缺少该构件,或离线模式下列表不全。

排查顺序:

  1. 在 Maven Central 搜索页确认 groupId:artifactId:version 三者都存在。
  2. mvn -q dependency:tree -Dincludes=com.example:does-not-exist 看是谁引入的。
  3. 检查 settings.xml 的镜像与私服配置。
  4. 删除本地缓存中对应目录再重试(只删该构件,不要清空整个 .m2):
rm -rf ~/.m2/repository/com/example/does-not-exist

依赖收敛失败

Enforcer 的 dependencyConvergence 报错通常长这样:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-enforcer-plugin:3.6.3:enforce
(enforce-toolchain) on project libraryd-parent: Rule 0: org.apache.maven.enforcer.rules.dependency.DependencyConvergence failed with message:
Dependency convergence error for org.slf4j:slf4j-api:2.0.20 paths to dependency are:
...

处理方式不是关掉规则,而是找到分叉点:

mvn -q dependency:tree -Dverbose -Dincludes=org.slf4j:slf4j-api

然后在父 POM 的 dependencyManagement 里显式锁定版本,或在引入方排除旧版本。关闭规则只会把问题推迟到运行时。

可执行 jar 没有主清单

no main manifest attribute, in libraryd-app-1.0.0-SNAPSHOT.jar

触发条件:直接运行 mvn package 产出的普通 jar,而不是 Shade 重新打包后的 fat jar;或者 Shade 的 ManifestResourceTransformer 没配置 mainClass。

排查:

unzip -p libraryd-app/target/libraryd-app-1.0.0-SNAPSHOT.jar META-INF/MANIFEST.MF

确认有 Main-Class: com.plume.library.AppMain,并且 jar 里包含依赖类。如果只在 target/ 下看到 original-*.jar,说明 Shade 没有执行。

Shade 后签名校验失败

Exception in thread "main" java.lang.SecurityException: Invalid signature file digest
for Manifest main attributes

触发条件:某个依赖 jar 带签名文件(META-INF/*.SF、*.DSA、*.RSA),Shade 合并后签名与内容不匹配。

修复:在 Shade 的 filter 里排除签名文件:

<filters>
  <filter>
    <artifact>*:*</artifact>
    <excludes>
      <exclude>META-INF/*.SF</exclude>
      <exclude>META-INF/*.DSA</exclude>
      <exclude>META-INF/*.RSA</exclude>
    </excludes>
  </filter>
</filters>

找不到 SPI 实现

现象:启动时报 No suitable driver、Unable to locate appropriate Flyway configuration extension,或 Jackson 的多态类型找不到子类。

原因通常是 Shade 覆盖了 META-INF/services/*,只保留了最后一个依赖的文件。

修复:加入 ServicesResourceTransformer:

<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>

数据库启动与连接

Docker 端口已被占用

docker: Error response from daemon: failed to set up container networking:
driver failed programming external connectivity on endpoint libraryd-pg
(760e86b24ff0...): Bind for 127.0.0.1:5432 failed: port is already allocated

排查本机监听:

lsof -nP -iTCP:5432 -sTCP:LISTEN

处理方式二选一:停掉占用端口的进程,或换一个宿主端口:

docker run -d --rm --name libraryd-pg \
  -e POSTGRES_DB=libraryd -e POSTGRES_USER=libraryd -e POSTGRES_PASSWORD=libraryd \
  -p 127.0.0.1:55432:5432 postgres:17-alpine

换端口后,JDBC URL 和 LIBRARYD_DB_URL 也要同步改。容器内的 5432 不变,变的只是宿主端口。

密码错误

psql: error: connection to server at "127.0.0.1", port 5432 failed:
FATAL:  password authentication failed for user "libraryd"

排查:

  1. 容器启动时的 POSTGRES_PASSWORD 是否与连接配置一致。
  2. 是否连到了另一个同名容器或本机已有的 PostgreSQL。
  3. 环境变量是否被 shell 中的旧值覆盖:
env | rg '^LIBRARYD_DB_'

POSTGRES_PASSWORD 只在容器首次初始化数据目录时生效。改了环境变量但复用旧数据卷,密码不会变。

数据库不存在

psql: error: connection to server at "127.0.0.1", port 5432 failed:
FATAL:  database "libraryd" does not exist

确认容器内的库列表:

docker exec -it libraryd-pg psql -U libraryd -d postgres -c '\l'

常见原因是启动容器时漏了 POSTGRES_DB=libraryd,默认库名变成了用户名。补建数据库:

docker exec -it libraryd-pg createdb -U libraryd libraryd

连接被拒绝

Caused by: java.net.ConnectException: Connection refused (connect failed)

这表示 TCP 层没有服务在监听,不是密码或库名问题。检查:

docker ps --filter name=libraryd-pg
docker exec libraryd-pg pg_isready -U libraryd -d libraryd

容器刚启动时 PostgreSQL 需要几百毫秒到几秒初始化。应用启动脚本应等待 pg_isready,而不是 docker run 后立刻连接。

迁移

校验和不匹配

org.flywaydb.core.api.exception.FlywayValidateException: Validate failed:
Migrations have failed validation
Migration checksum mismatch for migration version 1
-> Applied to database : -1891601629
-> Resolved locally    : 186179074
Either revert the changes to the migration, or run repair to update the schema history.

触发条件:已经执行过的 V1__init.sql 被修改。Flyway 用校验和确认「历史迁移没有被改过」。

正确做法:

  • 开发阶段、数据库可丢弃:重建数据库,重新迁移。
  • 数据库已有数据:不要改历史迁移,新增 V3__...sql 表达变更。
  • 确实需要修正历史记录(例如本地实验库):flyway repair 只更新 schema history 的校验和,不会改表结构,生产环境慎用。

查看已执行记录:

select installed_rank, version, description, checksum, success
from flyway_schema_history
order by installed_rank;

表已存在

ERROR:  relation "books" already exists

触发条件:手工建过表,或迁移脚本被重复执行,但 schema history 里没有对应记录。

处理:先确认表结构是否与迁移一致。如果一致,可以 baseline 当前版本;如果不一致,删除表后让 Flyway 重新执行,或写一个新的迁移修正差异。不要直接删 flyway_schema_history 了事,那会让 Flyway 以为什么都没执行过。

连接池

连接超时

java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available,
request timed out after 1001ms (total=0, active=0, idle=0, waiting=0)
	at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:714)
Caused by: java.net.ConnectException: Connection refused (connect failed)

这段输出说明两件事:池里一个连接都没建起来(total=0),底层是连接被拒绝。先修数据库可达性,再谈池大小。

如果 total 接近 maximumPoolSize、active 长期占满,才是池太小或连接泄漏:

HikariPool-1 - Connection is not available, request timed out after 30000ms
(total=10, active=10, idle=0, waiting=7)

排查顺序:

  1. 确认每个 getConnection() 都在 try-with-resources 里关闭。
  2. 打开泄漏检测:
config.setLeakDetectionThreshold(10_000);

日志会出现持有连接超过阈值的堆栈,指向具体代码行。
3. 检查长事务:一个事务里做 HTTP 调用、文件读写或大批量计算,会长时间占用连接。
4. 最后才考虑调大 maximumPoolSize。数据库连接是有限资源,盲目调大只会把压力转移到数据库。

连接池指标

HikariCP 通过 Micrometer 暴露 hikaricp.connections.active、hikaricp.connections.idle、hikaricp.connections.pending 等指标。排障时看 active 与 pending 的比值,比看单次日志更快。

SQL 与事务

重复借阅:部分唯一索引生效

ERROR:  duplicate key value violates unique constraint "ux_loans_active_isbn"
DETAIL:  Key (isbn)=(B1) already exists.

这是预期行为。ux_loans_active_isbn 是 where returned_on is null 的部分唯一索引,用来保证一本书同时只有一条活动借阅。

应用层应当在捕获 SQLException、SQLState 为 23505 时,转换成 BOOK_ALREADY_BORROWED(HTTP 409),而不是把数据库错误直接暴露给调用方。

外键 violation

ERROR:  insert or update on table "loans" violates foreign key constraint "loans_isbn_fkey"
DETAIL:  Key (isbn)=(B404) is not present in table "books".

说明借阅记录引用了不存在的图书。应用层应先查图书是否存在,返回 BOOK_NOT_FOUND;数据库约束是最后一道防线,不是主要校验手段。

序列化失败

ERROR:  could not serialize access due to concurrent update

触发条件:REPEATABLE READ 或 SERIALIZABLE 隔离级别下,两个事务同时修改同一行,后提交者失败。

这不是 bug,而是隔离级别保证一致性的方式。应用层需要识别 SQLState 40001,回滚并重试整个事务。重试次数要有上限,并且事务内不能有不可重复的副作用(发邮件、调用外部支付)。

死锁

ERROR:  deadlock detected
DETAIL:  Process 79 waits for ShareLock on transaction 742; blocked by process 77.
Process 77 waits for ShareLock on transaction 743; blocked by process 79.
HINT:  See server log for query details.
CONTEXT:  while updating tuple (0,1) in relation "loans"

触发条件:两个事务以相反顺序锁定同一组行。PostgreSQL 会自动检测并回滚其中一个。

处理:

  1. 统一加锁顺序。借阅和归还都按 isbn 排序后再更新,能从根上减少死锁。
  2. 缩短事务,避免在事务中做网络调用。
  3. 捕获 SQLState 40P01,回滚后重试。
  4. 查看当前锁等待:
select pid, state, wait_event_type, wait_event, query
from pg_stat_activity
where datname = 'libraryd' and state <> 'idle';

HTTP 服务

端口被占用

io.javalin.util.JavalinBindException: Port already in use.
Make sure no other process is using port 8080 and try again.
	at io.javalin.jetty.JettyServer.start(JettyServer.kt:103)
Caused by: java.io.IOException: Failed to bind to 0.0.0.0/0.0.0.0:8080
Caused by: java.net.BindException: Address already in use

排查:

lsof -nP -iTCP:8080 -sTCP:LISTEN

如果是上一个 libraryd 没停干净,先结束进程;如果是其他服务,改 LIBRARYD_HTTP_PORT,或者停掉冲突服务。不要用随机端口掩盖问题——端口是部署契约的一部分。

缺少 Content-Type:返回 400 而不是 415

请求缺少 Content-Type: application/json 时,Javalin 的 bodyAsClass 无法确定解析方式。libraryd 的 parseBody 显式检查,返回 400:

{
  "type": "https://plumphp.com/problems/invalid-command",
  "title": "Bad Request",
  "status": 400,
  "detail": "Content-Type must be application/json",
  "code": "E_INVALID_COMMAND",
  "requestId": "5f1c2a0e-...",
  "errors": {}
}

这里没有用 415,是为了让所有「请求本身不合法」的错误共享同一套 Problem Details 结构和客户端处理分支。如果团队更希望遵循语义,把 INVALID_COMMAND 的状态码改成 415 即可,但要在接口契约里统一,不能一部分接口 400、一部分 415。

排查时先看 curl -i 的请求头,不要只看响应体。很多「参数没收到」的问题,实际是请求头或 JSON 字段名不对。

Jackson 反序列化失败

com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException:
Unrecognized field "publishedYear" (class com.plume.library.web.dto.CreateBookRequest),
not marked as ignorable (4 known properties: "isbn", "title", "author", "year"])
 at [Source: (String)"{...}"; line: 1, column: 42]

这说明请求字段与 DTO 不一致。本书统一用 year,对应数据库列 published_year。要么改请求,要么在 DTO 上显式兼容旧字段——不要全局打开 FAIL_ON_UNKNOWN_PROPERTIES=false 来掩盖拼写错误。

日期序列化失败

com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Java 8 date/time type `java.time.LocalDate` not supported by default:
add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling

修复:注册 JavaTimeModule,并关闭时间戳输出:

ObjectMapper mapper = new ObjectMapper()
        .registerModule(new JavaTimeModule())
        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

只加依赖不注册模块,报错不会消失。

容器与发布

容器启动即退出

docker ps -a --filter name=libraryd
docker logs --tail 50 libraryd

常见原因:

  • 缺少环境变量:LIBRARYD_DB_URL 指向 localhost,但容器里的 localhost 是容器自己,应改为宿主机名或 compose 服务名。
  • 迁移失败:Flyway 校验和或权限问题,进程在启动阶段退出。
  • 端口写错:应用监听 8080,-p 映射的是 8080,但配置被覆盖成别的端口。

容器连不上宿主机数据库

容器内的 localhost 不等于宿主机。macOS 与 Linux 的处理不同:

  • Docker Desktop(macOS):用 host.docker.internal。
  • Linux:用 --add-host=host.docker.internal:host-gateway,或直接使用容器网络中的服务名。

更稳妥的做法是把应用和数据库放进同一个 compose 网络,用服务名互相访问。

镜像架构不匹配

WARNING: The requested image's platform (linux/amd64) does not match the detected
host platform (linux/arm64/v8)

Apple Silicon 上运行 amd64 镜像会走模拟层,性能明显下降。构建时指定目标架构:

docker build --platform linux/arm64 -t libraryd:dev .

CI 与生产环境统一架构,避免「本地能跑、线上很慢」。

一页诊断命令

# 版本与工具链
java -version && mvn -version && docker --version

# Maven 依赖
mvn -q dependency:tree -Dincludes=<groupId>:<artifactId>
mvn -q help:effective-pom -Doutput=/tmp/effective-pom.xml

# 容器与数据库
docker ps --filter name=libraryd
docker logs --tail 50 libraryd-pg
docker exec libraryd-pg pg_isready -U libraryd -d libraryd
docker exec -it libraryd-pg psql -U libraryd -d libraryd -c '\dt'

# 端口
lsof -nP -iTCP:8080 -sTCP:LISTEN
lsof -nP -iTCP:5432 -sTCP:LISTEN

# HTTP
curl -i http://localhost:8080/health
curl -i -X POST http://localhost:8080/api/books \
  -H 'Content-Type: application/json' \
  -d '{"isbn":"978-7-115-54736-1","title":"Java 编程实战","author":"Leeting Yan","year":2026}'

# 数据库活动与锁
psql -U libraryd -d libraryd -c \
  "select pid, state, wait_event_type, wait_event, query from pg_stat_activity where datname='libraryd';"

把这些命令按「构建 → 容器 → 端口 → HTTP → 数据库」的顺序跑一遍,大多数问题能在五分钟内定位到某一层。

小结

  • 先确认失败发生在哪一层,再决定看什么;不要从最熟悉的配置开始改。
  • 构建阶段最常见的是 JDK 版本、依赖坐标和 Shade 资源合并。
  • 数据库阶段先确认容器可达,再谈密码、库名和迁移。
  • Flyway 校验和错误不要靠改历史迁移解决,新增迁移或 repair。
  • Hikari 超时先看 total/active/pending,再判断是池小、泄漏还是数据库慢。
  • 数据库的唯一约束、外键、序列化失败和死锁都是信号,应用层要把它们翻译成领域错误。
  • HTTP 4xx 先看请求头和字段名,5xx 先看服务端日志和异常栈。
  • 容器问题优先看 docker logs、环境变量和网络地址,不要先怀疑镜像。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Netty 高性能网络编程》15.3 linkd-v2 验收与复盘
  2. 《Netty 高性能网络编程》15.2 限流、超时与降级
  3. 《Netty 高性能网络编程》15.1 网关路由与转发