跳到主要内容

架构总览

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)

👨‍🍳 类比:餐厅的后厨 — 真正做菜的地方

包含说明
ControllerREST API 端点
Service业务逻辑实现
Entity数据库实体
MapperMyBatis-Plus 持久层
ConverterMapStruct 转换器
重要规则

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*-serviceAPI 不能依赖实现
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 指南


📚 延伸阅读