架构总览
Personal Blog Backend 采用 模块化单体架构(Modular Monolith),这是一种兼顾单体应用便捷性和微服务可扩展性的现代架构模式。
🎯 为什么选择模块化单体?
- 传统单体
- 微服务
- 模块化单体 ✨
优势: ✅ 开发快速 | ✅ 部署简单 | ✅ 调试方便
劣势: ❌ 代码耦合严重 | ❌ 难以扩展 | ❌ 技术债积累
优势: ✅ 高度解耦 | ✅ 独立部署 | ✅ 技术栈灵活
劣势: ❌ 运维复杂 | ❌ 分布式事务 | ❌ 初期成本高
优势: ✅ 开发快速 | ✅ 模块解耦 | ✅ 未来可拆分 | ✅ 调试简单
适用: 初创项目 | 中小型团队 | 快速迭代
核心理念
- 物理维度:所有代码打包在一个 JAR 中,运行在一个 JVM 进程内
- 逻辑维度:严格遵循微服务拆分原则,模块间高度隔离
🏗️ 架构图
graph TB
subgraph "应用层"
APP["🚀 blog-application<br/>启动入口 + 全局配置"]
end
subgraph "服务层"
SYS["🛡️ system-service<br/>用户 / 角色 / 认证"]
ART["📄 article-service<br/>文章管理"]
CMT["💬 comment-service<br/>评论系统"]
FILE["📁 file-service<br/>S3 存储"]
end
subgraph "API 层 (契约)"
SYS_API["system-api"]
ART_API["article-api"]
CMT_API["comment-api"]
FILE_API["file-api"]
end
subgraph "公共层"
COMMON["🔧 blog-common<br/>工具 / 异常 / Base 框架"]
end
APP --> SYS & ART & CMT & FILE
SYS --> SYS_API
ART --> ART_API
ART -.->|跨模块调用| SYS_API
CMT --> CMT_API
CMT -.->|跨模块调用| ART_API
FILE --> FILE_API
SYS_API & ART_API & CMT_API & FILE_API --> COMMON
📦 项目结构
personal-blog-backend/
├── pom.xml # 父 POM(统一版本管理)
│
├── blog-application/ # 🚀 启动模块
│ ├── src/main/java/
│ │ └── com/blog/BlogApplication.java
│ └── src/main/resources/
│ ├── application.yaml # 全局配置
│ └── db/ # Flyway 迁移脚本
│
├── blog-common/ # 🔧 公共基础模块
│ └── src/main/java/com/blog/common/
│ ├── base/ # BaseServiceImpl, BaseConverter
│ ├── exception/ # BusinessException, ErrorCode
│ ├── model/ # Result<T>
│ └── security/ # JwtTokenProvider
│
└── blog-modules/ # 📦 业务模块
├── blog-module-system/
│ ├── blog-system-api/ # DTO, VO, Interface
│ └── blog-system-service/ # Controller, Service, Entity
├── blog-module-article/
├── blog-module-comment/
└── blog-module-file/
📋 模块职责
API 模块 (*-api)
📋 类比:餐厅的菜单 — 只告诉你有什么菜,不告诉你怎么做
| 包含 | 说明 |
|---|---|
| ✅ DTO | 请求数据传输对象 |
| ✅ VO | 响应视图对象 |
| ✅ Interface | 跨模块调用接口 |
| ✅ Enum | 业务枚举 |
| 禁止 | 原因 |
|---|---|
| ❌ Entity | 数据库实体是私有资产 |
| ❌ 业务逻辑 | 只定义契约,不包含实现 |
Service 模块 (*-service)
👨🍳 类比:餐厅的后厨 — 真正做菜的地方
| 包含 | 说明 |
|---|---|
| ✅ Controller | REST API 端点 |
| ✅ Service | 业务逻辑实现 |
| ✅ Entity | 数据库实体 |
| ✅ Mapper | MyBatis-Plus 持久层 |
| ✅ Converter | MapStruct 转换器 |
重要规则
Controller 必须位于 *-service 模块,不是 blog-application!
Application 模块
🚀 职责:应用的组装者和启动入口
| 允许 | 禁止 |
|---|---|
| ✅ 聚合所有 service 依赖 | ❌ 编写业务逻辑 |
| ✅ 提供 main 方法 | ❌ 创建 Controller |
| ✅ 全局配置文件 | ❌ 定义 Entity |
| ✅ Flyway 脚本 |
Common 模块
🔧 职责:项目的工具箱
| 包含 | 说明 |
|---|---|
| ✅ 工具类 | StringUtils, DateUtils |
| ✅ 统一响应 | Result<T> |
| ✅ 异常处理 | BusinessException, ErrorCode |
| ✅ Base 框架 | BaseServiceImpl, BaseConverter |
避免"上帝类"
不要将业务对象(如 User 实体)放入 common,这会破坏封装性。
🔗 依赖规则
✅ 允许的依赖
blog-application ──▶ 所有 *-service 模块
blog-*-service ──▶ blog-*-api (自己的 API)
──▶ blog-common
──▶ 其他模块的 *-api (跨模块调用)
blog-*-api ──▶ blog-common
❌ 禁止的依赖
| 禁止 | 原因 |
|---|---|
*-service → *-service | 模块耦合,无法拆分 |
*-api → *-service | API 不能依赖实现 |
common → 业务模块 | 公共层不能依赖业务 |
🚫 架构红线
1. 禁止跨模块 JOIN
-- ❌ 错误:文章模块直接 JOIN 用户表
SELECT a.*, u.username
FROM art_article a
JOIN sys_user u ON a.author_id = u.id
// ✅ 正确:通过接口调用
List<Article> articles = articleMapper.selectList(...);
List<Long> authorIds = articles.stream()
.map(Article::getAuthorId)
.toList();
List<UserDTO> users = remoteUserService.getUsersByIds(authorIds);
// 在 Java 代码中组装数据
理由:微服务架构下数据库是物理隔离的,JOIN 无法执行。
2. 接口即契约
- 模块间调用必须通过接口(定义在
*-api中) - Spring 自动注入本地实现
- 未来切换为 Feign Client 时,业务代码无需修改
3. 实体不外传
- Entity 只能在
*-service内部使用 - 对外交互必须使用 DTO/VO
🔄 微服务演进
当某个模块需要独立扩展时:
graph LR
A["模块化单体"] -->|"流量暴增"| B["拆分文章模块"]
B --> C["创建 article-app"]
C --> D["配置独立数据库"]
D --> E["替换为 Feign Client"]
Step 1: 创建独立启动模块
blog-article-app/
├── src/main/java/.../ArticleApplication.java
├── src/main/resources/application.yaml
└── pom.xml (依赖 blog-article-service)
Step 2: 配置独立数据库
spring:
datasource:
url: jdbc:mysql://article-db:3306/blog_article
Step 3: 替换接口实现
// 从本地 Bean 替换为 Feign Client
@FeignClient(name = "article-service")
public interface RemoteArticleService {
@GetMapping("/api/articles/{id}")
ArticleDTO getArticleById(@PathVariable Long id);
}
核心优势
整个过程无需重构业务代码,因为模块边界清晰、始终通过接口调用。
🧪 架构守护
项目使用 ArchUnit 自动化测试架构规则,防止代码违反设计原则。
@ArchTest
static final ArchRule services_should_not_depend_on_other_services =
noClasses()
.that().resideInAPackage("..service..")
.should().dependOnClassesThat()
.resideInAPackage("..other.service..");
👉 详见 ArchUnit 指南
📚 延伸阅读
- 项目评价报告 — 架构评分与改进建议
- Base Framework — 核心框架详解
- Security 配置 — 三链安全架构