若依多模块 Maven 项目架构实战:从单体到模块化
若依项目做到第三个月,admin 模块越来越臃肿——启动 40 秒、改一行代码全量编译、两个人改同一个文件天天冲突。这篇文章复盘我是怎么把若依从单模块拆成多模块的:哪个先拆、边界怎么画、依赖怎么管、趟过的坑一个不落。
一、单体项目的"三个月魔咒"
最开始搭若依的时候,目录干净得让人舒服:
ruoyi-admin/
├── src/main/java/com/ruoyi/
│ ├── controller/ ← 用户、角色、菜单、工单、客户...
│ ├── service/ ← 所有业务逻辑
│ ├── mapper/ ← 所有数据库映射
│ └── ...
└── pom.xml
前两周没毛病,改什么都快。代码生成器一键出 CRUD,一天能上两张表。
问题从第一个月开始冒头:
| 症状 | 具体表现 | 谁难受 |
|---|---|---|
| 启动变慢 | 从 8 秒涨到 40 秒 | 每次调试都等得心烦 |
| 编译范围大 | 改一行 Service,全量 300+ 类重新编译 | 浪费时间 |
| 合并冲突 | 两个人都在 admin 里加东西,每天 merge 都有冲突 | 协作成本飙升 |
| 边界模糊 | CRM 的 Service 和 MES 的 Service 混在一起,越写越不敢删 | 谁敢重构谁死 |
到第二个月底,ruoyi-admin 已经塞了 CRM 模块和 MES 模块的全部代码——Controller 40 多个,Service 30 多个,Mapper 50 多个。每次 IDE 里打开项目树,要滚好几页才能找到想看的文件。
单模块不坏,但当你开始纠结"这个类该放哪个包"的时候,就是该拆的时候了。
二、若依框架的模块化设计全景
先看若依官方给的标准模块结构,这对理解后面的拆分至关重要:
ruoyi/
├── ruoyi-common/ # 公共工具类、异常、注解、枚举
├── ruoyi-framework/ # 框架核心配置(安全、数据源、Swagger)
├── ruoyi-system/ # 系统管理业务(用户、角色、菜单、部门)
├── ruoyi-generator/ # 代码生成器
├── ruoyi-quartz/ # 定时任务
└── ruoyi-admin/ # 启动模块 + Controller 汇总
2.1 各模块的职责与边界
| 模块 | 职责 | 可以依赖谁 | 被谁依赖 |
|---|---|---|---|
ruoyi-common | 工具类、通用异常、基础注解、常量 | 无(最底层) | 所有模块 |
ruoyi-framework | 安全配置、数据源配置、全局拦截器 | ruoyi-common | ruoyi-system、ruoyi-admin |
ruoyi-system | 用户/角色/菜单/部门等系统管理业务 | ruoyi-common、ruoyi-framework | ruoyi-admin |
ruoyi-generator | 代码生成器 | ruoyi-common | ruoyi-admin |
ruoyi-quartz | 定时任务管理 | ruoyi-common | ruoyi-admin |
ruoyi-admin | 启动类、全局 Controller、静态资源 | 以上所有 | 无(顶层) |
2.2 核心原则:依赖只向下
若依的依赖链路是一条单向链:
ruoyi-admin ──→ ruoyi-system ──→ ruoyi-framework ──→ ruoyi-common
(顶层) (业务层) (配置层) (基础层)
没有任何一个底层模块依赖上层模块。 这是 Maven 多模块拆分的第一铁律。
如果你想不明白这个原则,记住一句土话:common 不能 import 任何业务代码,framework 不关心具体业务,system 只管自己的表。
三、实战第一步:把业务模块从 admin 里拆出来
3.1 拆之前的状态
在我们项目里,ruoyi-admin 底下实际长这样:
ruoyi-admin/src/main/java/com/ruoyi/
├── web/controller/system/ ← 若依自带的系统管理
├── web/controller/crm/ ← CRM:客户、联系人、商机...
├── web/controller/mes/ ← MES:工单、工序、质检...
├── service/system/
├── service/crm/
├── service/mes/
├── mapper/system/
├── mapper/crm/
└── mapper/mes/
包名分开了,但物理上全在一个模块。只要改一行,整个模块重新编译。 这跟没拆没区别。
3.2 怎么拆
别贪多,一次只拆一个模块。 我先拆的是 CRM,因为它和若依自带的系统管理耦合最小。
Step 1:创建新模块
在根 pom.xml 的 <modules> 里加一行:
<!-- 根 pom.xml -->
<modules>
<module>ruoyi-common</module>
<module>ruoyi-framework</module>
<module>ruoyi-system</module>
<module>ruoyi-generator</module>
<module>ruoyi-quartz</module>
<module>ruoyi-admin</module>
<module>ruoyi-crm</module> <!-- 新增 -->
</modules>
新建 ruoyi-crm 目录,里面放一个 pom.xml:
<!-- ruoyi-crm/pom.xml -->
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi</artifactId>
<version>3.3.0</version>
</parent>
<artifactId>ruoyi-crm</artifactId>
<name>ruoyi-crm</name>
<dependencies>
<!-- 只依赖 common 和 system,不依赖 admin -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-system</artifactId>
</dependency>
</dependencies>
⚠️ 关键:
ruoyi-crm不要依赖ruoyi-admin。admin 是顶层,业务模块不能反向依赖。
Step 2:迁移代码
目录结构对齐到标准的 Maven 模块目录:
ruoyi-crm/
├── pom.xml
└── src/main/java/com/ruoyi/crm/
├── controller/ ← 从 admin 搬过来的
├── service/
├── service/impl/
├── mapper/
└── domain/ ← entity 放这
按住 IDE 的拖拽,把 CRM 相关的包整个拖到 ruoyi-crm/src/main/java/com/ruoyi/crm/ 下。
注意包路径变了:原来 com.ruoyi.web.controller.crm → 现在 com.ruoyi.crm.controller。所有 import 语句需要同步修改,IDE 的批量替换可以搞定大部分。
Step 3:admin 加上依赖
<!-- ruoyi-admin/pom.xml -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-crm</artifactId>
</dependency>
这样 ruoyi-admin 启动时才会把 ruoyi-crm 的类扫进 Spring 容器。
MES 模块同理,再建一个 ruoyi-mes,操作完全一样。
3.3 第一个坑:Spring Boot 扫描不到 Bean
代码搬过去,依赖加好,启动——报了一堆 NoSuchBeanDefinitionException。
原因:若依的启动类在 com.ruoyi 包下,@SpringBootApplication 默认只扫描启动类所在包及其子包。CRM 代码移到了 com.ruoyi.crm 下,还在 com.ruoyi 子包里,所以理论上扫得到。
但我踩的坑是:有些代码路径习惯性写成了 com.ruoyi.web.controller.crm,搬家后变成了 com.ruoyi.crm.controller,中间的 web 层级没了,包结构发生变化。而原来 admin 里有些地方的 @MapperScan 还是指向老路径。
解决:统一把 @MapperScan 放到各模块各自的配置类里,不要全堆在 admin 中:
// ruoyi-crm 模块中
@Configuration
@MapperScan("com.ruoyi.crm.mapper")
public class CrmConfig {
}
这样每个模块管好自己的 Mapper 扫描,admin 不用操心。
四、实战第二步:公共代码怎么不重复——common 的演进
4.1 什么时候抽到 common
拆完 CRM 和 MES 后,很快发现一个问题:两个模块都需要 Excel 导出工具 ExcelUtil。
最蠢的做法:各 copy 一份。等你改了 bug 忘了同步另一份,线上就出灵异事件。
正确做法:抽到 ruoyi-common:
ruoyi-common/src/main/java/com/ruoyi/common/utils/
├── ExcelUtil.java ← CRM 和 MES 都用
├── PageUtils.java ← 通用分页工具
└── DictUtils.java ← 字典工具
抽取原则很简单:
| 该放 common | 不该放 common |
|---|---|
| 所有业务模块都用的(分页、导出、日期工具) | 只有两个模块共用的业务 DTO |
| 异常类、枚举、基础注解 | 跟某个具体业务强绑定的工具 |
| 全局常量 | 临时性、可能随时变的配置 |
4.2 什么时候不该放 common
CRM 和 MES 之间有一个共享场景:MES 的质检模块需要查 CRM 的客户信息。我们就建了一个 CrmCustomerBriefDTO 用于跨模块传递。
这个 DTO 如果放 ruoyi-common,就会导致 common 开始膨胀——今天塞一个客户 DTO,明天塞一个工单 DTO,最终 common 又变成了大杂烩。
方案:建一个 ruoyi-shared-dto 模块,只放跨模块共享的数据对象:
<!-- ruoyi-shared-dto/pom.xml -->
<dependencies>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
</dependency>
</dependencies>
然后 CRM 和 MES 都依赖它。common 保持干净。
common 的使命是"基础能力",不是"业务共享"。业务共享另有其位。
五、实战第三步:多模块下的配置管理
5.1 每个模块能各配各的吗?
不能。Spring Boot 在启动时只会加载一次 application.yml——默认从 classpath 下找,具体位置是启动类所在模块的 resources 目录。其他模块的 application.yml 不会被自动合并。
所以正确的做法是:所有全局配置都放在 ruoyi-admin/src/main/resources/ 下。
5.2 那模块的专属配置怎么办?
用 @ConfigurationProperties + 配置前缀隔离:
// ruoyi-crm 模块中
@Data
@Component
@ConfigurationProperties(prefix = "crm")
public class CrmConfig {
/** 客户导入最大行数 */
private int importMaxRows = 5000;
/** 商机过期天数 */
private int opportunityExpireDays = 30;
}
对应在 ruoyi-admin 的 application.yml 中:
# application.yml(只在 admin 中维护)
crm:
import-max-rows: 5000
opportunity-expire-days: 30
mes:
work-order-prefix: "WO"
quality-check-levels: IPQC,IPFQC,FQC
每个模块内部注入自己的配置类,互不干扰。
5.3 多环境配置
多模块下的多环境配置跟单模块没区别,仍然用 spring.profiles.active:
ruoyi-admin/src/main/resources/
├── application.yml ← 公共配置
├── application-dev.yml ← 开发环境
├── application-test.yml ← 测试环境
└── application-prod.yml ← 生产环境
启动时指定:--spring.profiles.active=dev
核心原则不变:所有 yml 文件都放在 ruoyi-admin 的 resources 里,业务模块只定义 @ConfigurationProperties 类。
六、Maven 多模块构建与打包
6.1 统一版本号
拆了多模块后,最怕版本号散落各处。Maven 的 <dependencyManagement> 是救命稻草:
<!-- 根 pom.xml -->
<properties>
<ruoyi.version>3.3.0</ruoyi.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<hutool.version>5.8.28</hutool.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- 内部模块版本统一 -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${ruoyi.version}</version>
</dependency>
<!-- 外部依赖版本统一 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
子模块引用时不写版本号,由根 pom 统一管控:
<!-- ruoyi-crm/pom.xml -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
<!-- 版本号继承根 pom 的 dependencyManagement -->
</dependency>
6.2 打包产物
在根目录执行:
mvn clean package
最终在 ruoyi-admin/target/ 下生成一个可执行 jar。所有子模块的 class 都被打包进去了,部署时只需要一个 jar 文件。
6.3 第二个大坑:循环依赖
拆 MES 模块的时候,我遇到过:CRM 需要一个方法来查客户名称 → 我直接在 MES 的 Service 里注入 CRM 的 Service。反过来,CRM 有一段逻辑需要调 MES 的工单状态,我又在 CRM 里注入了 MES 的 Service。
结果:Maven 编译直接报错 Circular dependency。
正确的解法:
CRM ←→ MES 互调的场景 → 提取共享接口到 ruoyi-common
// ruoyi-common 中定义接口
public interface CustomerBriefService {
CustomerBriefDTO getBrief(Long customerId);
}
// ruoyi-crm 中实现接口
@Service
public class CustomerBriefServiceImpl implements CustomerBriefService {
// 实现从 CRM 数据库查客户简要信息
}
// ruoyi-mes 中依赖接口,不依赖实现
@Service
public class QualityCheckService {
@Autowired
private CustomerBriefService customerBriefService; // 注入接口
}
Spring 会自动找到 ruoyi-crm 中的实现并注入。MES 只依赖 common 中的接口,不依赖 CRM 模块本身——循环依赖就这样解了。
七、拆分后的最终架构
拆完后的项目结构:
ruoyi/
├── ruoyi-common/ # 工具类、异常、基础注解
├── ruoyi-framework/ # 安全、数据源、全局配置
├── ruoyi-system/ # 系统管理(用户、角色、菜单)
├── ruoyi-crm/ # CRM 业务模块
├── ruoyi-mes/ # MES 业务模块
├── ruoyi-shared-dto/ # 跨模块共享 DTO
├── ruoyi-generator/ # 代码生成器
├── ruoyi-quartz/ # 定时任务
└── ruoyi-admin/ # 启动模块
拆分后的实际收益
| 维度 | 拆之前 | 拆之后 |
|---|---|---|
| 启动时间 | ~40 秒 | ~18 秒(只改了 CRM 时热加载更快) |
| 编译时间 | 改一行全量 300+ 类 | 只编译当前模块 |
| 代码冲突 | 天天 merge 冲突 | 基本不冲突,各改各的模块 |
| 新人上手 | 要弄懂整个 admin 才能改 | 读懂一个模块就能开工 |
| 删除成本 | 不敢删,怕影响别处 | Maven 依赖关系图一眼看穿,敢删 |
八、总结 & 核心原则
回顾这次拆分,我一直遵循三条原则:
① 依赖只向下。 底层模块永远不依赖上层,admin 依赖一切,common 不依赖任何业务。
② 公共代码提 common,业务代码放模块。 common 是基础能力集,不是垃圾桶。
③ 一次只拆一个模块。 不要想着一次拆干净——拆一个,跑通,再拆下一个。稳定压倒一切。
什么时候该拆?
如果你对下面三个问题有两个回答"是",就该动手了:
-
启动时间超过 30 秒?
-
两个人以上在同一个模块里写代码?
-
你开始纠结"这个类到底该放哪个包"?
什么时候先别拆?
-
项目还在 Demo 阶段,业务都不确定
-
只有你一个人写,而且一个月内不会加人
-
代码量不到 100 个类
拆分是为协作和长期维护服务的,不要为了拆而拆。
如果你也在独立开发产品,或者对制造业数字化感兴趣,欢迎关注这个公众号。我会持续分享从代码到产品的全过程——包括成功的经验,也包括踩过的坑。
一个人的产品之路,不孤单。👇
原创作者 MqCode(全栈开发者,印刷包装行业 MES+CRM 系统独立开发),欢迎自由转发。
更多推荐



所有评论(0)