14-[架构重构] 迈向企业级:Maven 多模块项目

7. [架构重构] 迈向企业级:Maven 多模块项目

摘要: 随着我们功能的不断累加,我们最初的单体项目正变得日益臃肿和混乱。本章,我们将进行一次意义深远的架构重构,借鉴 RuoYi 等企业级框架的设计思想,亲手将我们的项目改造为一个职责清晰、高度解耦的 Maven 多模块工程。这次重构将极大提升项目的可维护性和扩展性,为未来承载更复杂的业务打下坚实的基础。

7.1. 痛点分析与模块化思想

7.1.1. 现状:单体应用的困境

在开始重构之前,我们首先要清醒地认识到当前项目存在的“成长烦恼”。请看我们目前的项目包结构:

虽然我们遵循了 controller, service, mapper 等内部分层,但从整体工程角度看,它依然是一个单体应用。随着项目规模的扩大,这种结构逐渐暴露出一系列问题:

  • 包结构扁平化: advice, common, config, controller, converter, dto, entity… 所有这些包都挤在同一个 src 目录下。当未来我们新增“订单模块”、“产品模块”时,这个列表将变得越来越长,不同业务模块的代码会混杂在一起,难以管理。
  • 依赖关系混乱: 我们的 pom.xml 文件中,既有 spring-boot-starter-web 这样的 Web 框架依赖,也有 mybatis-plus 这样的数据持久化依赖,还有 hutool-all 这样的通用工具依赖。所有依赖都打包在一起,职责不清,我们很难说清楚哪个模块具体需要哪个依赖。
  • 高度耦合: 所有代码都在一个模块中,理论上任何一个类的改动(即使是一个工具类),都可能需要对整个项目进行重新的编译、测试和部署,模块间的边界是模糊的。
  • 复用性差: 如果我们想在另一个新项目中复用当前的 common 包或 util 包下的工具类,除了复制粘贴代码,没有更优雅的方式。

7.1.2. 蓝图:借鉴 RuoYi 的模块化设计

要解决以上问题,我们需要引入软件工程领域一个非常重要的思想——模块化,而在 Maven 项目中,实现模块化的最佳实践就是构建**多模块项目 **。

我们将借鉴 RuoYi 这类成熟企业级框架的设计精髓,将我们的单体应用“分而治之”,拆分为以下四个核心模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
📂 spring-boot-demo (父模块, pom)
├── 📄 pom.xml (统一管理版本和模块)

├── 📂 demo-common (通用工具模块, jar)
│ └── ... (存放 Result, BusinessException, JwtUtil 等)

├── 📂 demo-framework (框架核心模块, jar)
│ └── ... (存放 WebConfig, MybatisPlusConfig, GlobalExceptionHandler, 拦截器等)

├── 📂 demo-system (系统业务模块, jar)
│ └── ... (存放所有用户管理相关的 Controller, Service, Mapper, DTO, VO 等)

└── 📂 demo-admin (启动与部署模块, jar)
└── ... (仅存放 SpringBootDemoApplication.java 启动类)

各模块核心职责

模块核心职责详细说明
demo-common通用工具与核心定义存放与具体业务无关的、可被所有其他模块复用的公共类,如 Result 封装、自定义异常、Hutool 依赖、枚举、常量等。它是一个高度抽象的底层工具包。
demo-framework框架核心配置与增强存放与 Spring 框架本身相关的配置和增强功能,如 WebMvcConfigurer (CORS, 拦截器)、MybatisPlusConfigSpringDocConfig、全局异常处理器等。它为业务模块提供技术支撑。
demo-system具体业务模块存放具体业务模块的代码。目前我们只有一个“用户管理”功能,所以所有 User 相关的 Controller, Service, Mapper, DTO, VO 都会放在这里。未来新增“订单模块”时,我们会创建一个新的 demo-order 业务模块。
demo-admin应用启动入口这是一个非常轻量的“胶水”模块。它本身几乎没有代码,只包含 main 启动类。它的主要作用是通过 Maven 依赖,将 frameworksystem 等模块组装起来,最终打包成一个可运行的 Spring Boot 应用。

通过这样的拆分,我们的项目结构将变得异常清晰。每个模块都有明确的边界和单一的职责,实现了物理层面的解耦。这为我们后续的开发、测试和维护工作,奠定了坚实的企业级工程基础。


7.2. 实战:父工程改造与依赖管理

理论学习完毕,我们现在正式开始对项目进行“大手术”。第一步,也是最关键的一步,就是将我们当前的 spring-boot-demo 项目,从一个可运行的 jar 工程,改造为一个只负责管理、不包含任何代码的 pom 父工程

7.2.1. 修改 packaging 为 pom

请打开项目根目录下的 pom.xml 文件。我们需要做的第一件事,就是将 <packaging> 标签的内容,从默认的 jar 修改为 pom

文件路径: pom.xml (修改)

1
2
3
4
5
<groupId>com.example</groupId>
<artifactId>spring-boot-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>spring-boot-demo</name>

这个小小的改动,从根本上改变了 pom.xml 的性质。它告诉 Maven:“我不再是一个需要被打成 jar 包的普通应用了,我的新身份是一个父工程,我的职责是管理我的子模块们”。

修改后,您可能会发现项目中的 src 目录在 IDE 中变成了灰色或普通文件夹图标,这是完全正常的,因为 pom 类型的父工程本身不包含业务代码。

7.2.2. 统一版本:使用 <properties><dependencyManagement>

目前,我们所有的依赖都直接写在 <dependencies> 标签下,版本号散落在各处。当项目模块变多时,这会导致版本混乱和难以升级。专业的做法是使用 <dependencyManagement> 在父工程中进行统一的版本声明。

我们将进行两步操作:

  1. 使用 <properties> 标签将所有版本号提取为变量。
  2. 将整个 <dependencies> 块移动到 <dependencyManagement> 块内部。

文件路径: 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
</parent>

<groupId>com.example</groupId>
<artifactId>spring-boot-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-demo</name>

<!-- 1. 关键修改:将打包方式改为 pom -->
<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>
<lombok.version>1.18.32</lombok.version>
<hutool.version>5.8.27</hutool.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<springdoc.version>2.7.0</springdoc.version>
<jjwt.version>0.11.5</jjwt.version> <!-- 建议也统一管理 jjwt 版本 -->
</properties>

<!-- 3. 最佳实践:使用 dependencyManagement 统一管理版本,不强制引入 -->
<dependencyManagement>
<dependencies>
<!-- 内部模块依赖 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>demo-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>demo-framework</artifactId>
<version>${project.version}</version>
</dependency>

<!-- 第三方库依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

<!-- 4. 最佳实践:移除父POM中的 build 插件,它应该在可执行的子模块中 -->
<!-- <build> ... </build> -->

</project>

代码解析

  • <properties>: 我们将所有第三方库的版本号都提取到了这里,便于未来统一升级。
  • <dependencyManagement>: 这就像一个“依赖版本仲裁中心”。在这里声明的依赖,并不会被实际引入,它只是一个“版本清单”。
  • 清空 <dependencies>: 父 pom 作为一个管理者,它本身不应该包含任何具体的实现代码,因此也不需要直接依赖任何 jar 包。

经过改造后,所有子模块未来在引入这些依赖时,只需要提供 groupIdartifactId无需再指定 version,它们会自动继承父工程中声明的版本。这保证了整个项目所有模块使用的依赖版本是高度统一的。

最后,父工程需要知道它到底管理了哪些子模块。我们在 pom.xml 的顶层(与 <properties> 同级)添加 <modules> 标签,并在其中声明我们规划好的四个子模块。


7.3. 实战:拆分核心模块

父工程的改造完成后,现在就如同我们已经画好了建筑蓝图。接下来的任务,就是按照蓝图,一砖一瓦地搭建起每个独立的模块(房间),并将我们现有的代码(家具)搬运到它们各自正确的位置。

在 IDEA 中,您可以通过右键点击项目根目录 -> New -> Module... 来创建新的 Maven 子模块。请确保在创建时,它能被正确识别为当前父工程的子模块。

7.3.1. 创建并迁移 demo-common (通用工具模块)

这个模块是我们的基础工具库,存放与具体业务无关的、可被所有其他模块复用的公共代码。

第一步:创建模块与 pom.xml

首先,在项目根目录下创建 demo-common 文件夹,并在其中创建 pom.xml 文件。

文件路径: demo-common/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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>spring-boot-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>demo-common</artifactId>

<dependencies>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
</dependencies>

</project>

注意:在子模块的 pom.xml 中,我们引入依赖时无需再指定 <version><groupId>,因为它会从父工程的 <dependencyManagement> 中自动继承,这正是父工程的价值所在!

第二步:迁移代码

现在,我们将主项目中所有通用的代码包,整体移动到 demo-common 模块的 src/main/java/ 目录下。

迁移清单:

  • common 包 (包含 Result.java, ResultCode.java)
  • enums 包 (包含 UserStatusEnum.java)
  • exception 包 (包含 BusinessException.java)
  • util 包 (目前没有文件)

7.3.2. 创建并迁移 demo-framework (框架核心模块)

这个模块负责所有与 Spring 框架相关的配置和增强功能。

第一步:创建模块与 pom.xml

文件路径: demo-framework/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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>spring-boot-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>demo-framework</artifactId>

<dependencies>
<!-- 依赖通用模块 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>demo-common</artifactId>
<!--
版本号可以省略,Maven会从parent中继承。
写上 ${project.version} 也可以,效果相同。
-->
</dependency>

<!-- Spring Boot Web 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- MyBatis-Plus 数据库操作 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>

<!-- SpringDoc API文档 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>

<!-- Spring Boot 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>

第二步:迁移代码

迁移清单:

  • advice 包 (包含 GlobalExceptionHandler.java)
  • config 包 (包含 MybatisPlusConfig.java, SpringDocConfig.java, WebConfig.java)
  • converter 包 (包含 StringToUserStatusEnumConverter.java)
  • interceptor 包 (包含 AuthInterceptor.java, LogInterceptor.java)

7.3.3. 创建并迁移 demo-system (系统业务模块)

这个模块是我们的核心业务模块,存放所有与“用户管理”相关的代码。

最理想的结构是让 demo-system 依赖 demo-framework,而不是重复声明 spring-boot-starter-web、mybatis-plus 等依赖。这样可以形成清晰的依赖链:admin -> system -> framework -> common

第一步:创建模块与 pom.xml

文件路径: demo-system/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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>spring-boot-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>demo-system</artifactId>

<dependencies>
<!-- 依赖框架层,它已经包含了 Web, MyBatis-Plus, SpringDoc 等所有基础能力 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>demo-framework</artifactId>
</dependency>

<!-- 仅保留测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>

第二步:迁移代码

迁移清单:

  • controller 包 (所有 Controller)
  • dto 包 (所有 DTO)
  • entity 包 (所有 Entity)
  • mapper 包 (所有 Mapper)
  • service 包 (所有 Service 及其 impl)
  • validation 包 (所有校验分组)
  • vo 包 (所有 VO)

在修复代码之前,必须先让项目能正确加载。

  1. 打开根目录下的 pom.xml (spring-boot-demo/pom.xml)。
  2. 找到 <modules> 标签。
  3. 注释或删除那一行 <module>demo-admin</module>,因为这个模块还不存在。
  4. 在 IDEA 右侧的 Maven 面板中,点击刷新按钮,确保项目能无错误地加载。

步骤 1: 执行全局搜索和替换

现在,我们要把所有旧的包引用 com.example.springbootdemo.* 替换成新的包名。

  1. 按下快捷键 Ctrl + Shift + R (Windows/Linux) 或 Cmd + Shift + R (Mac)。这将打开全局搜索和替换窗口。

  2. 执行第一次替换

    • 在上面的输入框 (Find) 中,输入旧的包前缀: com.example.springbootdemo

    • 在下面的输入框 (Replace) 中,输入新的包前缀: com.example

  • 重要: 仔细检查下面的预览窗口,确保替换是你想要的结果。

    • 点击 “Replace All” 或 “Replace” 按钮。

因为我们的新包结构是 com.example.democommon, com.example.demosystem 等,而不是 com.example.springbootdemo.common。原来的所有 DTO, VO, Service 等都引用了 com.example.springbootdemo 下的类。

建议开启自动导包,对于之前结构的包都进行删除并全部自动导入的操作

img

步骤 2: 优化 Imports

替换完成后,可能还有一些多余或无效的 import 语句。

  1. 在项目根目录上右键。
  2. 选择 “Optimize Imports”。IDEA 会自动清理所有文件中未使用的导入。
  3. 你也可以使用快捷键 Ctrl + Alt + O (Windows/Linux) 或 Cmd + Option + O (Mac) 在单个文件中操作。

7.4. 实战:创建 Admin 启动模块

7.4.1. 创建 demo-admin 模块与 pom.xml

首先,请按照同样的方式,创建第四个子模块:demo-admin

这个模块的 pom.xml 非常关键,它不直接依赖具体的第三方库,而是依赖于我们自己创建的 frameworksystem 模块,像胶水一样将它们粘合在一起。同时,它需要 spring-boot-maven-plugin 插件来将整个应用打包成一个可执行的 jar

文件路径: demo-admin/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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>spring-boot-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>demo-admin</artifactId>

<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>demo-framework</artifactId>
<!-- 添加版本号 -->
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>com.example</groupId>
<artifactId>demo-system</artifactId>
<!-- 添加版本号 -->
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

7.4.2. 迁移并改造启动类

现在,我们将父工程根目录下那个“光杆司令” SpringBootDemoApplication.java 移动到 demo-admin 模块中。

同时,我们需要对它进行一个小小的改造,以确保 Spring Boot 能够扫描到我们所有子模块中的组件(Bean)。

文件路径: demo-admin/src/main/java/com/example/springbootdemo/SpringBootDemoApplication.java (迁移并修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example.demoadmin;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* @SpringBootApplication 注解默认扫描的是当前类所在的包及其子包。
* 由于我们的 Bean (如 Controller, Service, Config 等)现在分布在
* com.example.demoframework, com.example.demosystem 等不同的包中,
* 我们需要通过 scanBasePackages 属性,将扫描的根路径扩大到 com.example,
* 从而覆盖所有子模块。
*/
@SpringBootApplication(scanBasePackages = "com.example")
@MapperScan("com.example.*.mapper")
public class SpringBootDemoApplication {

public static void main(String[] args) {
SpringApplication.run(SpringBootDemoApplication.class, args);
}

}

7.4.3. 清理项目

重要信息: 记得将配置文件搬迁到 demo-admin

为了让我们的项目结构达到最终的完美形态,请执行以下清理操作:

  1. 删除父工程根目录下的 src 文件夹: 因为 SpringBootDemoApplication.java 已经被移动到了 demo-admin 模块,父工程根目录下的 src 文件夹现在是多余的,可以安全删除
  2. 删除子模块中多余的启动类: 您在 demo-common, demo-frameworkdemo-system 中由 IDE 自动生成的 Demo...Application.java...Tests.java 文件是不需要的,也请一并删除,因为整个应用只有一个启动入口和一套集成的测试环境。

7.4.4. 回归测试:验证重构结果

现在,整个项目的结构已经焕然一新。请确保您在 IDE 中选择的启动目标是 demo-admin 模块中的 SpringBootDemoApplication.java

为了不影响读者的阅读和可能读者操作失误,这里提供一个整理好的仓库供读者快速Clone Spring_Mvc_Study: 教学用的SpringMVC文件

启动应用后,请打开 Swagger UI 或 Postman,尝试调用一个我们之前写好的接口,例如 GET /users/1

如果接口能够正常返回数据,那么——

恭喜您! 您已经成功地将一个单体应用,重构为了一个结构清晰、职责分明、高度解耦的企业级多模块项目!这次重构的成功,为您未来驾驭大型复杂项目奠定了坚实的工程基础。