Note 23. 从单体到 Maven 多模块与阿里规范落地
Note 23. 从单体到 Maven 多模块与阿里规范落地
ProriseNote 23. 从单体到 Maven 多模块与阿里规范落地
摘要: 当项目规模膨胀,单体架构(Monolith)将成为开发效率的瓶颈。本章我们将进行一次彻底的 架构重构,将项目拆分为 Common、Framework、System、Admin 等 Maven 模块。更重要的是,我们将引入 《阿里巴巴 Java 开发手册》,对之前的代码进行一次全方位的 规范化清洗,从命名、异常处理、集合处理到并发控制,彻底根除“屎山”基因,打造一个可扩展、标准化的企业级工程。
本章学习路径
- 规范先行:深度解读阿里规范的核心条款(命名、OOP、集合、并发),安装
Alibaba Coding Guidelines插件进行代码扫描与修正。 - 架构蓝图:设计父子工程结构,理解
dependencyManagement的版本仲裁机制。 - 模块拆解:
- Common:提取通用工具与基础对象。
- Framework:剥离技术组件(Redis/Web/MyBatis 配置)。
- System:纯粹的业务逻辑单元。
- Admin:应用启动入口与聚合。
- 循环依赖治理:通过模块化强制解耦,消除类与类之间的恶性依赖。
23.1. 悬在头顶的达摩克利斯之剑:阿里巴巴 Java 开发规范
在拆分模块之前,我们必须先“正衣冠”。如果代码本身不规范,拆分后只会变成“分布式的垃圾堆”。
我们使用的标准是 《阿里巴巴 Java 开发手册 (黄山版)》,这是中国 Java 开发者必须遵守的“宪法”。
23.1.1. 插件安装与全盘扫描
工欲善其事,必先利其器。我们不需要死记硬背几百条规则,让 IDE 帮我们检查。
操作步骤:
- 在 IntelliJ IDEA 中打开
Settings->Plugins。 - 搜索 “Alibaba Java Coding Guidelines” 并安装。
- 重启 IDEA。
- 右键点击项目根目录 ->
Analyze->Alibaba Java Coding Guidelines。
此时,底部的 Inspection 面板会列出所有违规代码(Blocker/Critical/Major)。接下来,我们将结合之前的章节代码,逐一修正这些典型问题。
23.1.2. 【规约一】命名风格:DO/DTO/VO 的严格区分
阿里规范:
【强制】类名使用
UpperCamelCase风格。【强制】POJO 类中布尔类型变量都不要加is前缀。【参考】POJO 类命名规范:
- 数据对象:
xxxDO,xxx为数据表名。- 传输对象:
xxxDTO。- 展示对象:
xxxVO。
实战修正:
在之前的章节中,我们的实体类叫 User。在多模块中,这容易混淆。我们应该重构:
- Database Entity:
User->UserDO(Domain Object) 或保留User但放在domain包下。 - boolean 陷阱:
- 错误:
private boolean isDeleted; - 后果:Jackson 序列化时可能会把
isDeleted解析为deleted,导致前端拿不到值。 - 修正:
private boolean deleted;
- 错误:
23.1.3. 【规约二】OOP 规约:包装类型与空指针
阿里规范:
【强制】所有的 POJO 类属性必须使用 包装数据类型(Integer),RPC 方法的返回值和参数必须使用包装数据类型。【强制】定义 DO/DTO/VO 等 POJO 类时,不要设定任何属性 默认值。
实战修正:
错误代码:
1 | public class UserDTO { |
问题:如果前端没传 age,后端收到的是 0。但 0 岁和“未填写”是两个概念。
修正代码:
1 | public class UserDTO { |
23.1.4. 【规约三】集合处理:asList 与 subList 的坑
阿里规范:
【强制】使用工具类
Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法(add/remove/clear),会抛出UnsupportedOperationException。
场景复现:我们在做测试数据时常写:
1 | List<String> list = Arrays.asList("a", "b"); |
原因:Arrays.asList 返回的是一个内部类 ArrayList,它是一个定长数组,不支持增删。
修正:
1 | List<String> list = new ArrayList<>(Arrays.asList("a", "b")); |
23.1.5. 【规约四】并发处理:线程池的创建
阿里规范:
【强制】线程池不允许使用
Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
我们在 Note 20 中配置的 AsyncConfig 已经完全符合此规范:
1 | // ✅ 符合规范:手动指定核心数、最大数、队列容量 |
反例(严禁使用):
1 | // ❌ 违规:CachedThreadPool 允许创建 Integer.MAX_VALUE 个线程,会导致 OOM |
23.2. 架构蓝图:Maven 多模块设计
清洗完代码细节,我们开始动刀架构。我们将原来的 spring-boot-demo 拆分为一个 父工程 和四个 子模块。
23.2.1. 模块依赖关系图
demo-common: 最底层。存放工具类(Hutool)、通用结果集(Result)、枚举、异常定义。不依赖 Spring Web。demo-framework: 技术底座。依赖 Common。存放配置类(Redis, MyBatis-Plus, Swagger, 拦截器, 全局异常处理)。demo-system: 业务核心。依赖 Framework。存放 Controller, Service, Mapper, DO, DTO, VO。demo-admin: 启动入口。依赖 System。只存放启动类Application和application.yml。
23.3. 实战拆分:Step-by-Step
23.3.1. 第一步:改造父工程 (Root)
- 保留根目录的
pom.xml。 - 删除
src目录(父工程不需要代码)。 - 修改
pom.xml,将<packaging>改为pom。 - 使用
<dependencyManagement>锁定所有子模块和第三方依赖的版本。
文件路径: pom.xml (Root)
1 | <project ...> |
23.3.2. 第二步:构建 demo-common (通用模块)
此模块追求 极致的轻量,通常只引入 hutool、lombok、jackson 等基础库,尽量不要引入 spring-boot-starter-web,以免在不需要 Web 环境的场景(如独立的定时任务服务)中引入不必要的依赖。
迁移内容:
com.example.demo.common.Resultcom.example.demo.common.ResultCodecom.example.demo.exception.BusinessException(只留类定义,移除 Web 依赖)com.example.demo.utils.*
pom.xml:
1 | <artifactId>demo-common</artifactId> |
23.3.3. 第三步:构建 demo-framework (框架模块)
这是 基础设施层。所有的技术栈集成都在这里完成,业务模块只需要关注业务。
迁移内容:
- Config:
MybatisPlusConfig,WebMvcConfig(CORS/拦截器),RedisConfig,OpenApiConfig。 - Aspect:
LogAspect。 - Handler:
GlobalExceptionHandler(全局异常处理)。 - Filter/Interceptor:
JwtAuthenticationTokenFilter。
pom.xml:
1 | <artifactId>demo-framework</artifactId> |
23.3.4. 第四步:构建 demo-system (业务模块)
这是我们平时写代码的主战场。
迁移内容:
controllerservicemapperentity(DO/DTO/VO)
pom.xml:
1 | <artifactId>demo-system</artifactId> |
23.3.5. 第五步:构建 demo-admin (启动模块)
这是“胶水”模块,甚至连 Java 代码都很少。
迁移内容:
DemoApplication.java(启动类)application.yml(配置文件)
pom.xml:
1 | <artifactId>demo-admin</artifactId> |
关键修正:由于类分散在不同包(如 com.example.framework 和 com.example.system),启动类必须扩大扫描范围:
1 | // 扫描所有模块 |
23.4. 循环依赖治理:分层的意义
在单体项目中,我们经常遇到 Service A 引用 Service B,Service B 又引用 Service A 的情况。虽然 Spring 能解决 Setter 循环依赖,但这在架构设计上是 严重的坏味道。
Maven 多模块从物理上禁止了某些循环依赖。
场景:
demo-common不能引用demo-system的类。如果 Common 里的工具类想调用 UserService,编译直接报错!
这倒逼我们思考代码的归属:如果一个功能是通用的(如获取当前登录用户),它不应该依赖具体的 User 实体。我们应该在 Framework 层通过 ThreadLocal 或 SecurityContext 来实现,而不是直接调用 UserService。
23.5. 本章总结与架构规范速查
摘要回顾
本章我们完成了一次脱胎换骨的演进。
- 规范化:通过引入阿里规范,我们修复了命名、集合、并发等潜在隐患,代码质量向大厂看齐。
- 模块化:我们将大单体拆分为 4 个标准模块,明确了各层的职责边界。
Common负责基础,Framework负责技术,System负责业务,Admin负责启动。 - 依赖管理:通过
dependencyManagement实现了版本统一管理。
遇到以下 3 种架构场景时,请直接参考处理模版:
1. 场景一:新增一个业务模块(如订单)
需求:新增 Order 业务,不影响现有的 User 业务。
方案:
- 新建模块
demo-order。 pom.xml依赖demo-framework。- 在
demo-admin中引入demo-order。 - 启动类自动扫描生效。
2. 场景二:DTO/VO 放在哪?
阿里规范建议:
- 如果 DTO 只在一个模块内部使用,放在该模块的
dto包下。 - 如果 DTO 需要跨模块调用(如 Dubbo 接口参数),需要单独提取一个
demo-api模块存放 DTO 和接口定义。
3. 场景三:工具类依赖业务 Bean
需求:写一个 UserUtils,需要查数据库。
错误:放在 demo-common,注入 UserMapper。
正确:
- 如果它是业务工具,放在
demo-system。 - 如果它是通用工具,不应该依赖数据库。
- 或者定义一个
FunctionalInterface回调,由业务层传入数据。
4. 核心避坑指南
打包报错 “Unknown”
- 现象:
mvn package报错找不到子模块。 - 原因:必须在 父工程 目录下执行
mvn install,先将子模块安装到本地仓库。单独打包demo-admin可能会找不到依赖。
- 现象:
Bean 扫描不到
- 现象:启动后访问接口 404,或者 Service 注入失败。
- 原因:模块包名不一致。比如 Framework 用
com.tech, System 用com.biz。 - 对策:统一包前缀(如
com.example),并在启动类显式配置scanBasePackages。
循环依赖报错
- 现象:Maven 报错
Cycle detected between ...。 - 原因:Module A 依赖 Module B,Module B 又依赖 Module A。
- 对策:这是架构设计错误。必须提取公共部分到 Module C(如 Common),让 A 和 B 都依赖 C。
- 现象:Maven 报错









