abhishekomp/product-service-docker-postgres
GitHub: abhishekomp/product-service-docker-postgres
基于 Spring Boot 和 PostgreSQL 的后端学习项目,演示 REST API 开发、Docker 容器化部署以及 Testcontainers 集成测试的完整实践。
Stars: 0 | Forks: 0
# 产品服务 — Spring Boot + Docker + PostgreSQL
一个学习项目,演示了如何**使用 Spring Boot 构建 REST API**,在 **PostgreSQL** 中持久化数据,使用 **Docker** 打包所有内容,以及**使用 Testcontainers 编写集成测试**——所有这些集中在一个项目中。
📖 **对这些概念感到陌生?从这里开始:** [Concepts.md](./Concepts.md) 解释了每一个术语——Spring Boot、JPA、Docker、REST、MockMvc、AssertJ——以及它们在本项目中是如何相互关联的。
📋 **HTTP 负载日志记录:** [Payload-Logging.md](./Payload-Logging.md)——介绍了如何捕获传入请求和传出响应、日志存储位置以及如何读取它们。
## 🎯 你将从本项目中学到什么
| 概念 | 查看位置 |
|---------|---------------|
| 分层架构(Controller → Service → Repository) | `controller/`, `service/`, `repository/` |
| 具有正确 HTTP 状态码的完整 CRUD REST API | `ProductController.java` |
| 输入验证(`@Valid`, `@NotBlank`) | `Product.java`, `ProductController.java` |
| 全局错误处理(`@RestControllerAdvice`) | `exception/GlobalExceptionHandler.java` |
| 结构化错误响应(Java Record) | `exception/ErrorResponse.java` |
| Spring Data JPA 与 PostgreSQL | `ProductRepository.java`, `application.properties` |
| 多阶段 Docker 构建 | `Dockerfile` |
| 使用 Docker Compose 一起运行多个容器 | `docker-compose.yaml` |
| 使用 Testcontainers 进行集成测试(4 种不同方法) | `src/test/` |
| HTTP 负载日志记录(`OncePerRequestFilter`、`ContentCachingWrapper`、MDC) | `filter/PayloadLoggingFilter.java`, `logback-spring.xml` |
| 使用 `data.sql` 填充数据库 | `src/main/resources/data.sql` |
## 🏗️ 项目架构
```
HTTP Request
│
▼
ProductController ← receives HTTP requests, returns HTTP responses
│
▼
ProductService ← business logic lives here
│
▼
ProductRepository ← Spring Data JPA interface, talks to the DB
│
▼
PostgreSQL Database ← stores product data
```
**包结构:**
```
org.aom.product
├── controller/ ProductController.java — REST endpoints
├── service/ ProductService.java — business logic
├── repository/ ProductRepository.java — data access
└── model/ Product.java — the JPA entity (maps to a DB table)
```
## 🛠️ 技术栈
| 技术 | 版本 | 用途 |
|------------|---------|---------|
| Java | 21 | 编程语言 |
| Spring Boot | 3.3.2 | 应用程序框架 |
| Spring Data JPA | (通过 Boot) | ORM / 数据库访问 |
| Hibernate | (通过 Boot) | JPA 实现 |
| PostgreSQL | 16 | 关系型数据库 |
| Docker & Docker Compose | 任何较新版本 | 容器化 |
| Testcontainers | 1.20.1 | 在测试中启动真实的 Postgres |
| JUnit 5 | (通过 Boot) | 测试框架 |
## 🗄️ 数据模型
只有一个实体:**Product**。
```
┌──────────────────────────────────────────┐
│ product │
├─────────────┬──────────────┬─────────────┤
│ prod_num │ p_name │ sku_code │
│ (PK, seq) │ (varchar) │ (varchar) │
├─────────────┼──────────────┼─────────────┤
│ 1 │ Apple iPhone │ APP2024 │
│ 2 │ Samsung S24 │ SAM2024 │
└─────────────┴──────────────┴─────────────┘
```
表名和列名由 Hibernate 从 `Product` 实体类自动派生。
上面两行数据在启动时通过 `src/main/resources/data.sql` 填充。
## 🚀 如何运行项目
### 选项 A — Docker Compose(推荐,无需本地 Postgres ✅)
**前置条件:** 安装并运行兼容 Docker 的运行时——[Docker Desktop](https://www.docker.com/products/docker-desktop/)、[Colima](https://github.com/abiosoft/colima)(`brew install colima && colima start`)、[Rancher Desktop](https://rancherdesktop.io/) 或 [Podman Desktop](https://podman-desktop.io/)。
**步骤:**
```
# 1. 克隆 / 打开项目
cd product-service-docker-postgres
# 2. 从示例创建本地 .env 文件(只需一次)
cp .env.example .env
# 然后打开 .env 并为 POSTGRES_DB_PWD 设置你首选的密码
# 3. 启动所有内容(构建 app image + 启动 Postgres + 启动 app)
docker compose up
# 在后台运行(detached mode):
docker compose up -d
# 停止所有内容:
docker compose down
```
### 选项 B — 本地运行(需要本地 PostgreSQL)
如果你安装了 Postgres(例如通过 [Homebrew](https://brew.sh/):`brew install postgresql@16`):
```
# 1. 启动本地 Postgres(如果尚未运行)
brew services start postgresql@16
# 2. 确保 'postgres' 用户和数据库存在(Homebrew 默认会进行此设置)
# 3. 运行 app
./mvnw spring-boot:run
```
`application.properties` 默认使用 `jdbc:postgresql://localhost:5432/postgres`,用户名为 `postgres` / 密码为 `admin`。
如果你的本地设置不同,请进行调整。
## 📡 API 接口
基础 URL:`http://localhost:8081/product-service`
### 创建产品 — `POST /addProduct`
```
POST /addProduct
Content-Type: application/json
{
"pName": "Google Pixel 9",
"skuCode": "GOO2024"
}
```
返回 **201 Created** 以及保存的产品(包含生成的 `prodNum`)。
如果 `pName` 或 `skuCode` 为空,则返回 **400 Bad Request**。
### 获取单个产品 — `GET /getProduct/{id}`
```
GET /getProduct/1
```
返回 **200 OK** 以及该产品。
如果该 ID 不存在,则返回 **404 Not Found** 以及结构化的错误主体。
### 获取所有产品 — `GET /getAllProducts`
```
GET /getAllProducts
```
返回 **200 OK** 以及一个 JSON 数组(如果不存在产品,则为空数组)。
### 更新产品 — `PUT /updateProduct/{id}`
```
PUT /updateProduct/1
Content-Type: application/json
{
"pName": "Apple iPhone 17",
"skuCode": "APP2025"
}
```
返回 **200 OK** 以及更新后的产品。
如果该 ID 不存在,则返回 **404 Not Found**。
如果 `pName` 或 `skuCode` 为空,则返回 **400 Bad Request**。
### 删除产品 — `DELETE /deleteProduct/{id}`
```
DELETE /deleteProduct/1
```
成功时返回 **204 No Content**(无主体)。
如果该 ID 不存在,则返回 **404 Not Found**。
### 错误响应格式
所有错误都返回一致的 JSON 主体:
```
{
"timestamp": "2024-08-16T10:30:00",
"status": 404,
"error": "Not Found",
"message": "Product not found with id: 99",
"path": "/product-service/getProduct/99"
}
```
项目根目录中包含了一个 Postman 集合:
`Product-Service-SpringBoot-docker.postman_collection.json`——将其导入 Postman 即可立即尝试所有接口。
## 🧪 测试 — Testcontainers
本项目特意展示了**四种不同方式**来使用 Testcontainers,以便你可以对它们进行并排比较。
| 测试类 | 方法 |
|------------|----------|
| `ProductRepositoryTestContainerManualStartTest` | 手动生命周期(`@BeforeAll` / `@AfterAll`) |
| `ProductRepositoryTestContainerTest` | 自动生命周期(`@Testcontainers` + `@Container`) |
| `ProductRepositoryWithTCJdbcUrlTest` | TC JDBC URL(`jdbc:tc:postgresql:16:///`) |
| `ProductControllerIntegrationUsesInitializerTest` | 共享自定义初始化器(`@ContextConfiguration`) |
📖 **有关每种方法的完整说明,请参阅 [Testcontainers.md](./Testcontainers.md)。**
### 运行测试
```
# 确保你的 Docker 运行时正在运行(Docker Desktop、Colima 等),然后:
./mvnw test
```
## 🐳 理解 Dockerfile
```
# ── Stage 1: Build ──────────────────────────────────────────────────
FROM maven:3.9.8-eclipse-temurin-21 AS buildstage
# 使用官方的 Maven + JDK 21 image 来编译项目并
# 生成 fat JAR(所有依赖都打包在一个 .jar 文件内)。
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests # builds the JAR, skips tests
# ── Stage 2: Runtime image ───────────────────────────────────────────
FROM eclipse-temurin:21-jre
# 使用精简的纯 JRE image — 没有 Maven,没有源代码,只有 JRE。
# 这使得最终的 image 保持较小,并减少了攻击面。
WORKDIR /app
COPY --from=buildstage /app/target/*.jar ./app.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "app.jar"]
```
**为什么要有两个阶段?**
构建阶段需要 Maven 和完整的 JDK。运行时阶段只需要一个 JRE 即可运行已编译的 `.jar`。将它们分开意味着最终的 Docker 镜像要小得多。
## 🔧 实用 Docker 命令
```
# 查看正在运行的 containers
docker ps
# 查看特定 container 的日志
docker logs product_service_con
docker logs db
# 在正在运行的 container 中打开一个终端
docker exec -it db /bin/bash
# 进入 container 后连接到 Postgres
psql -U postgres -d productDb
# 连接后有用的 psql 命令:
\l # list all databases
\c productDb # connect to productDb
\dt # list all tables
SELECT * FROM product; # query the product table
\q # quit psql
# 停止并移除 containers(保留 volumes)
docker compose down
# 停止并移除 containers 以及 volumes(清除数据库)
docker compose down -v
# 从头开始重新构建 app image(例如在代码更改后)
docker compose up --build
```
## 📁 项目结构
```
product-service-docker-postgres/
├── src/
│ ├── main/
│ │ ├── java/org/aom/product/
│ │ │ ├── ProductServiceApplication.java ← entry point (@SpringBootApplication)
│ │ │ ├── controller/ProductController.java ← REST endpoints
│ │ │ ├── service/ProductService.java ← business logic
│ │ │ ├── repository/ProductRepository.java ← Spring Data JPA interface
│ │ │ └── model/Product.java ← JPA entity
│ │ └── resources/
│ │ ├── application.properties ← app config (port, datasource, JPA)
│ │ └── data.sql ← seed data inserted on startup
│ └── test/
│ └── java/org/aom/product/
│ ├── controller/ ← full integration tests (MockMvc)
│ ├── repository/ ← JPA layer tests (4 TC approaches)
│ └── infra/ ← shared Testcontainers initializer
├── Dockerfile ← multi-stage build
├── docker-compose.yaml ← runs app + postgres together
├── pom.xml ← Maven dependencies
└── Product-Service-SpringBoot-docker.postman_collection.json
```
## ❓ 常见问题解答
**问:我的 Mac 上需要安装 PostgreSQL 吗?**
答:**不需要**,如果你使用 `docker compose up`。Docker 会同时管理应用程序和数据库。
**需要**,仅当你想直接使用 `./mvnw spring-boot:run` 运行应用程序时(即上面的选项 B)。
**问:运行测试需要 Docker 处于运行状态吗?**
答:**需要。** 测试使用 Testcontainers,它会自动拉取并启动一个真实的 `postgres:16` Docker 容器。在运行 `./mvnw test` 之前,请确保你兼容 Docker 的运行时(Docker Desktop、Colima、Rancher Desktop 等)正在运行。
**问:应用程序启动了,但我收到连接错误。**
答:使用 Docker Compose 时,应用程序有时会在 Postgres 完全就绪之前启动。`docker-compose.yaml` 中的 `restart: always` 策略会处理此问题——应用程序容器将不断重启,直到能够连接。请等待几秒钟,它就会启动。
**问:我修改了代码。如何重新构建 Docker 镜像?**
答:运行 `docker compose up --build`。这会强制 Docker 根据更新后的源代码重新构建应用程序镜像。
标签:Docker, PostgreSQL, RESTful API, Spring Boot, Testcontainers, 域名枚举, 安全防御评估, 提示词优化, 版权保护, 请求拦截