当前位置: 首页 > news >正文

若依多模块 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-commonruoyi-systemruoyi-admin
ruoyi-system用户/角色/菜单/部门等系统管理业务ruoyi-commonruoyi-frameworkruoyi-admin
ruoyi-generator代码生成器ruoyi-commonruoyi-admin
ruoyi-quartz定时任务管理ruoyi-commonruoyi-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-adminapplication.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 系统独立开发),欢迎自由转发。

http://www.jsqmd.com/news/1098782/

相关文章:

  • 《悬浮窗效果》二、Interface_WindowStage使用指南
  • openclaw 0512版本部署(ubuntu 26.04)
  • 泰戈尔的诗歌2
  • Kimi LeetCode 3420. 统计 K 次操作以内得到非递减子数组的数目 C++实现
  • 终极Unity游戏汉化指南:XUnity自动翻译器让外语游戏无障碍畅玩
  • 浅析NVMe协议:PRP/SGL数据传输格式
  • 怎么用一张图做产品视频?用 seedance2.0 快速生成 360 度动态视频实战教程
  • DAY 2 TIM定时器
  • 移动语义与容器极致优化,emplace/push底层差异、对象复用、std::allocator原理、自定义STL分配器实战
  • 对称加密算法的扩散层(P盒)密码学指标详细介绍
  • C++开发者如何学好汇编(上)
  • 不要把CNC机内测头当成三坐标
  • PCL 垂距法实现点云精简
  • 深入解析Hermes Agent:从Skill驱动架构到实战部署的AI Agent框架指南
  • 东莞翻译机构 韩语审计报告重点
  • Kimi LeetCode 3425. 最长特殊路径 Java实现
  • 从入门到精通:Python OpenPyXL完整教程
  • 3个突破性技巧:如何用SRWE实现Windows窗口的实时魔法编辑
  • 6个月小白逆袭AI初级工程师:收藏这份保姆级学习路线,从零基础到实战大模型!
  • 嵌入式音频开发实战:AU-60 全功能 DSP 语音模组一站式开发指南
  • 揭秘AI写教材黑科技!低查重的AI教材生成,为教学助力
  • MCP 协议传输层进化:从 stdio 到 Streamable HTTP,我的踩坑实录
  • 5分钟免费解锁英雄联盟所有皮肤:R3nzSkin国服特供版完整指南
  • How To: Create A Word Document In Powershell – Part 1 – Opening The Document, Writing Some Text, Usi
  • Kimi LeetCode 3425. 最长特殊路径 Python3实现
  • 低查重AI写教材攻略:精选5款AI工具,轻松搞定教材写作难题!
  • GNSS数据处理新手必看:手把手教你读懂RINEX 3.04钟差文件(CLK)里的关键信息
  • django文件对象是什么?
  • Highcharts有版权吗?
  • 对称加密算法的混淆层(S盒)密码学指标详细介绍