Note 23. 从单体到 Maven 多模块与阿里规范落地

Note 23. 从单体到 Maven 多模块与阿里规范落地

摘要: 当项目规模膨胀,单体架构(Monolith)将成为开发效率的瓶颈。本章我们将进行一次彻底的 架构重构,将项目拆分为 CommonFrameworkSystemAdmin 等 Maven 模块。更重要的是,我们将引入 《阿里巴巴 Java 开发手册》,对之前的代码进行一次全方位的 规范化清洗,从命名、异常处理、集合处理到并发控制,彻底根除“屎山”基因,打造一个可扩展、标准化的企业级工程。

本章学习路径

  1. 规范先行:深度解读阿里规范的核心条款(命名、OOP、集合、并发),安装 Alibaba Coding Guidelines 插件进行代码扫描与修正。
  2. 架构蓝图:设计父子工程结构,理解 dependencyManagement 的版本仲裁机制。
  3. 模块拆解
    • Common:提取通用工具与基础对象。
    • Framework:剥离技术组件(Redis/Web/MyBatis 配置)。
    • System:纯粹的业务逻辑单元。
    • Admin:应用启动入口与聚合。
  4. 循环依赖治理:通过模块化强制解耦,消除类与类之间的恶性依赖。

23.1. 悬在头顶的达摩克利斯之剑:阿里巴巴 Java 开发规范

在拆分模块之前,我们必须先“正衣冠”。如果代码本身不规范,拆分后只会变成“分布式的垃圾堆”。

我们使用的标准是 《阿里巴巴 Java 开发手册 (黄山版)》,这是中国 Java 开发者必须遵守的“宪法”。

23.1.1. 插件安装与全盘扫描

工欲善其事,必先利其器。我们不需要死记硬背几百条规则,让 IDE 帮我们检查。

操作步骤

  1. 在 IntelliJ IDEA 中打开 Settings -> Plugins
  2. 搜索 “Alibaba Java Coding Guidelines” 并安装。
  3. 重启 IDEA。
  4. 右键点击项目根目录 -> Analyze -> Alibaba Java Coding Guidelines

此时,底部的 Inspection 面板会列出所有违规代码(Blocker/Critical/Major)。接下来,我们将结合之前的章节代码,逐一修正这些典型问题。


23.1.2. 【规约一】命名风格:DO/DTO/VO 的严格区分

阿里规范

【强制】类名使用 UpperCamelCase 风格。【强制】POJO 类中布尔类型变量都不要加 is 前缀。【参考】POJO 类命名规范:

  • 数据对象:xxxDOxxx 为数据表名。
  • 传输对象: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
2
3
public class UserDTO {
private int age; // 默认值是 0
}

问题:如果前端没传 age,后端收到的是 0。但 0 岁和“未填写”是两个概念。

修正代码

1
2
3
public class UserDTO {
private Integer age; // 默认值是 null,可以区分“未传”和“0”
}

23.1.4. 【规约三】集合处理:asListsubList 的坑

阿里规范

【强制】使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法(add/remove/clear),会抛出 UnsupportedOperationException

场景复现:我们在做测试数据时常写:

1
2
List<String> list = Arrays.asList("a", "b");
list.add("c"); // ❌ 运行时报错!

原因Arrays.asList 返回的是一个内部类 ArrayList,它是一个定长数组,不支持增删。

修正

1
2
List<String> list = new ArrayList<>(Arrays.asList("a", "b"));
list.add("c"); // ✅ 正常

23.1.5. 【规约四】并发处理:线程池的创建

阿里规范

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

我们在 Note 20 中配置的 AsyncConfig 已经完全符合此规范:

1
2
3
4
5
// ✅ 符合规范:手动指定核心数、最大数、队列容量
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setQueueCapacity(200);
// ...

反例(严禁使用)

1
2
// ❌ 违规:CachedThreadPool 允许创建 Integer.MAX_VALUE 个线程,会导致 OOM
ExecutorService es = Executors.newCachedThreadPool();

23.2. 架构蓝图:Maven 多模块设计

清洗完代码细节,我们开始动刀架构。我们将原来的 spring-boot-demo 拆分为一个 父工程 和四个 子模块

23.2.1. 模块依赖关系图

mermaid-diagram-2025-12-15-205934

  • 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。只存放启动类 Applicationapplication.yml

23.3. 实战拆分:Step-by-Step

23.3.1. 第一步:改造父工程 (Root)

  1. 保留根目录的 pom.xml
  2. 删除 src 目录(父工程不需要代码)。
  3. 修改 pom.xml,将 <packaging> 改为 pom
  4. 使用 <dependencyManagement> 锁定所有子模块和第三方依赖的版本。

文件路径: pom.xml (Root)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<project ...>
<groupId>com.example</groupId>
<artifactId>spring-boot-demo</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging> <!-- 关键 -->

<modules>
<module>demo-common</module>
<module>demo-framework</module>
<module>demo-system</module>
<module>demo-admin</module>
</modules>

<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<hutool.version>5.8.26</hutool.version>
<!-- 统一管理子模块版本 -->
<demo.version>1.0.0</demo.version>
</properties>

<!-- 版本仲裁中心:只声明版本,不引入依赖 -->
<dependencyManagement>
<dependencies>
<!-- 内部模块 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>demo-common</artifactId>
<version>${demo.version}</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>demo-framework</artifactId>
<version>${demo.version}</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>demo-system</artifactId>
<version>${demo.version}</version>
</dependency>

<!-- 第三方库 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- ... 其他如 mybatis-plus, jwt 等 ... -->
</dependencies>
</dependencyManagement>
</project>

23.3.2. 第二步:构建 demo-common (通用模块)

此模块追求 极致的轻量,通常只引入 hutoollombokjackson 等基础库,尽量不要引入 spring-boot-starter-web,以免在不需要 Web 环境的场景(如独立的定时任务服务)中引入不必要的依赖。

迁移内容

  • com.example.demo.common.Result
  • com.example.demo.common.ResultCode
  • com.example.demo.exception.BusinessException (只留类定义,移除 Web 依赖)
  • com.example.demo.utils.*

pom.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<artifactId>demo-common</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId> <!-- Jackson -->
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

23.3.3. 第三步:构建 demo-framework (框架模块)

这是 基础设施层。所有的技术栈集成都在这里完成,业务模块只需要关注业务。

迁移内容

  • Config: MybatisPlusConfig, WebMvcConfig (CORS/拦截器), RedisConfig, OpenApiConfig
  • Aspect: LogAspect
  • Handler: GlobalExceptionHandler (全局异常处理)。
  • Filter/Interceptor: JwtAuthenticationTokenFilter

pom.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<artifactId>demo-framework</artifactId>
<dependencies>
<!-- 依赖 common -->
<dependency>
<groupId>com.example</groupId>
<artifactId>demo-common</artifactId>
</dependency>

<!-- Web 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
</dependencies>

23.3.4. 第四步:构建 demo-system (业务模块)

这是我们平时写代码的主战场。

迁移内容

  • controller
  • service
  • mapper
  • entity (DO/DTO/VO)

pom.xml:

1
2
3
4
5
6
7
8
<artifactId>demo-system</artifactId>
<dependencies>
<!-- 只需要依赖 framework,自动获得所有技术能力 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>demo-framework</artifactId>
</dependency>
</dependencies>

23.3.5. 第五步:构建 demo-admin (启动模块)

这是“胶水”模块,甚至连 Java 代码都很少。

迁移内容

  • DemoApplication.java (启动类)
  • application.yml (配置文件)

pom.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<artifactId>demo-admin</artifactId>
<dependencies>
<!-- 引入业务模块 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>demo-system</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<!-- 只有启动模块需要 Spring Boot 打包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

关键修正:由于类分散在不同包(如 com.example.frameworkcom.example.system),启动类必须扩大扫描范围:

1
2
3
4
5
6
7
@SpringBootApplication(scanBasePackages = {"com.example"}) // 扫描所有模块
@MapperScan("com.example.**.mapper") // 扫描所有模块的 Mapper
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

23.4. 循环依赖治理:分层的意义

在单体项目中,我们经常遇到 Service A 引用 Service B,Service B 又引用 Service A 的情况。虽然 Spring 能解决 Setter 循环依赖,但这在架构设计上是 严重的坏味道

Maven 多模块从物理上禁止了某些循环依赖。

场景

  • demo-common 不能引用 demo-system 的类。如果 Common 里的工具类想调用 UserService,编译直接报错!

这倒逼我们思考代码的归属:如果一个功能是通用的(如获取当前登录用户),它不应该依赖具体的 User 实体。我们应该在 Framework 层通过 ThreadLocalSecurityContext 来实现,而不是直接调用 UserService。


23.5. 本章总结与架构规范速查

摘要回顾
本章我们完成了一次脱胎换骨的演进。

  1. 规范化:通过引入阿里规范,我们修复了命名、集合、并发等潜在隐患,代码质量向大厂看齐。
  2. 模块化:我们将大单体拆分为 4 个标准模块,明确了各层的职责边界。Common 负责基础,Framework 负责技术,System 负责业务,Admin 负责启动。
  3. 依赖管理:通过 dependencyManagement 实现了版本统一管理。

遇到以下 3 种架构场景时,请直接参考处理模版:

1. 场景一:新增一个业务模块(如订单)

需求:新增 Order 业务,不影响现有的 User 业务。
方案

  1. 新建模块 demo-order
  2. pom.xml 依赖 demo-framework
  3. demo-admin 中引入 demo-order
  4. 启动类自动扫描生效。

2. 场景二:DTO/VO 放在哪?

阿里规范建议

  • 如果 DTO 只在一个模块内部使用,放在该模块的 dto 包下。
  • 如果 DTO 需要跨模块调用(如 Dubbo 接口参数),需要单独提取一个 demo-api 模块存放 DTO 和接口定义。

3. 场景三:工具类依赖业务 Bean

需求:写一个 UserUtils,需要查数据库。
错误:放在 demo-common,注入 UserMapper
正确

  1. 如果它是业务工具,放在 demo-system
  2. 如果它是通用工具,不应该依赖数据库。
  3. 或者定义一个 FunctionalInterface 回调,由业务层传入数据。

4. 核心避坑指南

  1. 打包报错 “Unknown”

    • 现象mvn package 报错找不到子模块。
    • 原因:必须在 父工程 目录下执行 mvn install,先将子模块安装到本地仓库。单独打包 demo-admin 可能会找不到依赖。
  2. Bean 扫描不到

    • 现象:启动后访问接口 404,或者 Service 注入失败。
    • 原因:模块包名不一致。比如 Framework 用 com.tech, System 用 com.biz
    • 对策:统一包前缀(如 com.example),并在启动类显式配置 scanBasePackages
  3. 循环依赖报错

    • 现象:Maven 报错 Cycle detected between ...
    • 原因:Module A 依赖 Module B,Module B 又依赖 Module A。
    • 对策:这是架构设计错误。必须提取公共部分到 Module C(如 Common),让 A 和 B 都依赖 C。