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, 域名枚举, 安全防御评估, 提示词优化, 版权保护, 请求拦截