☰
Spring Boot多模块工程搭建:从IDEA新建Module到Maven依赖管理实战
2026/10/3 14:57:26 网站建设 项目流程

从一个很常见的场景说开去。前段时间有个刚转 Java 的朋友在 IDEA 里建好了一个 Spring Boot 单模块项目,然后跑来问我:“我要把用户模块和订单模块拆开,新建 module 的时候到底是选 Maven 还是 Gradle?选 Spring Initializr 还是普通 Maven?”我一看,他已经在根项目里写了快两千行代码,正准备手动拆分。这种“先把代码写肿,再想着拆 module”的路径,我见得太多了。老实讲,新建 module 这个动作本身不难,难的是理解 module 之间怎么协作、Maven 在中间干了什么、Spring Boot 在多模块工程里怎么定位。把这些想清楚,新建 module 就是几分钟的事;想不清楚,后面等着你的就是编译不过、启动找不到类、依赖对不上三个经典的连环坑。

这篇文章就是写给刚开始认真搞 Spring 工程的同学,尤其是还在用 IntelliJ IDEA 做练习、想把项目拆成一个像样的多模块工程的人。我会从最基础的概念讲起,然后带你在 IDEA 里完整操作一遍,最后把那些很容易踩的坑摊开说。我在一线写过交易系统、后端服务、各种内部工具库,日常基本都在跟 Spring 多模块工程打交道,下面这些内容不是官方文档的复读,而是踩过坑之后沉淀下来的做法。

1. 先想清楚:什么时候需要给 Spring 项目新建 module

1.1 单模块项目在什么情况下会越来越难用

刚开始学 Spring Boot,直接在 start.spring.io 上勾选依赖生成一个项目,写一个 Controller、配一个数据源,这个阶段真的不需要 module。一个项目里就三五个类,硬拆模块只是自找麻烦。但项目一旦真正开始承接业务,几个问题就会很快冒出来:

  • 某个工具类被好几个服务引用,想改一个通用方法,得全局搜索,翻三四个地方逐个改。
  • 配置类、拦截器、公共实体在各个模块里复制粘贴,后面要统一加一个字段时,“Ctrl + H”全局替换都救不了你。
  • 编译越来越慢。哪怕只是改了一个日志工具类,整个大项目都要重新构建,一次构建三四十秒起步,耐心很容易被消磨掉。
  • 团队多人协作时,一个分支改的东西经常跟另一个分支互相覆盖。代码之间没有边界,大家只能靠口头约定“这块你别动”。

出现这些症状,说明你该在工程层面把代码拆开了。拆分不等于微服务架构,你先要有一个朴素的理解:新建 module 是在告诉构建工具“我要把这块代码独立管理,保留自己的引用关系,谁想用我,就在 pom 里声明依赖”。Spring 技术栈里,module 是最常见的物理边界,后面做微服务的时候,很多 module 又会演变成独立的服务,但那是另一回事。

1.2 典型的多模块划分方式

多模块工程怎么分,一般有两种主流思路。

第一种是按技术层划分,适合业务还不复杂、但代码已经很有规模的项目。比如一个后台管理项目,可以拆成下面几层:

  • xxx-common:通用工具类、常量、统一返回结构。
  • xxx-dal:数据库访问层,放 MyBatis/JPA 的 Mapper、Entity。
  • xxx-service:业务逻辑层,Service 接口和实现。
  • xxx-web:Controller 层,统一接收 HTTP 请求。

第二种是按业务域划分,适合业务边界很清晰的项目。比如电商项目,就拆成user-module、order-module、product-module,每个模块内部再自己去分 Controller、Service、Mapper。这种拆分更接近微服务的感觉,但初期模块之间的互相调用要慎重,否则容易变成“高耦合模块”。

我更推荐绝大多数学习者采用“技术层优先,业务域后置”的方式。先学会怎么让 module 之间松耦合协作,再考虑把业务切成一块块。很多新手一上来就把一个完整业务拆得七零八落,结果一个订单功能要跨三个模块才能跑通,调试起来非常痛苦。

2. 动手之前必须理解的 Maven 模块机制

2.1 parent pom 和 modules 节点的关系

多模块工程的核心不是 IDEA 界面,而是 Maven 的聚合机制。一个多模块工程根目录下会有一个 parent pom.xml,里面用<packaging>pom</packaging>声明它自己是个纯聚合工程,不产生任何 jar/war 包。然后通过<modules>把子模块列出来:

<packaging>pom</packaging> <modules> <module>my-common</module> <module>my-service</module> <module>my-web</module> </modules>

每个子模块也有自己的 pom.xml,它们通过<parent>指回根 pom,这样整个工程就形成了一棵清晰的依赖树。

<parent> <groupId>com.example</groupId> <artifactId>my-parent</artifactId> <version>1.0.0</version> <relativePath>../pom.xml</relativePath> </parent> <artifactId>my-service</artifactId>

我刚开始学的时候,最困惑的一点是:relativePath要写什么?它表示当前子模块的 pom 相对于父 pom 的路径。默认值是../pom.xml,如果你的目录结构是“父工程根目录/子模块目录”,那子模块的 pom 确实就在上一级的pom.xml,所以 IDEA 自动生成后,你常常不需要改这个值,但最好要知道它是什么含义。用了它,Maven 构建时就能直接在文件系统里找到父 pom,而不是先去本地仓库找。

2.2 Spring Boot parent 和业务 parent 怎么配合

很多新手会在每个子模块里都写一遍:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.5</version> </parent>

这是典型的错误。Spring Boot 的spring-boot-starter-parent是一个特殊的父 pom,它管理了大量依赖版本,比如 Spring、Jackson、Logback 等,还配置了构建插件的默认行为。如果每个 module 都继承它,一方面会让 spring-boot 相关依赖的版本在子模块之间无法统一治理,另一方面你最终构建多个 module 时很可能会出现“同时依赖两个不同 Spring Boot 版本”的荒诞局面。

正确做法是:多模块工程的最外层父 pom 继承 Spring Boot 的 parent,或者用dependencyManagement导入spring-boot-dependencies,然后在父 pom 里统一管理各子模块的版本。子模块只需要继承你自己工程的父 pom,依赖里写artifactId就够了,版本号交给父 pom。

以父 pom 继承 Spring Boot parent 为例:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.5</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>my-parent</artifactId> <version>1.0.0</version> <packaging>pom</packaging>

然后子模块里写:

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>

不用写<version>,因为 Spring Boot parent 已经把版本管住了。如果你看到最近的 Spring Boot 3.2、3.3 工程,这也是标准做法。理解了这套机制,新建 module 就只是“填信息”而已,心里会踏实很多。

2.3 三种 packaging 类型到底怎么选

新建 module 时,IDEA 或 Maven archetype 会问到 packaging 类型。我直接给你结论:

  • pom:父工程、聚合工程,以及专门管理依赖清单的 BOM 模块。
  • jar:绝大多数业务模块,Spring Boot 打可执行 jar 也是用这个。
  • war:老旧的外置 Tomcat 部署方式,Spring Boot 内嵌 Tomcat 时基本用不上。

所以你在 IDEA 里右键新建 Module,选 Maven,然后绝大多数子模块都保持默认 jar 就对了。只有最外层根 pom 需要手动改成 pom。这里不需要额外的 archetype,选最普通的 Maven 模块就行,后面依赖配置自己写。

3. 在 IntelliJ IDEA 里新建 module 的完整实操

3.1 先建一个不写代码的 Maven 父工程

很多人一上来就在 IDEA 里随便新建一个普通工程,然后在里面“新建 module”,结果发现这个普通工程根本不是 Maven 工程,parent pom 都没法写。我建议的起点是:先手动建一个新的 Maven 空工程作为父工程。

具体步骤:

  1. 打开 IDEA,File→New→Project,左侧选Maven,不要勾选任何模板。
  2. 填好GroupId、ArtifactId,比如com.example和my-parent,Version默认1.0.0即可。
  3. 点Finish生成工程。此时它会自带src目录,但你不需要它,直接删掉。
  4. 打开根 pom.xml,把<packaging>从 jar 改成 pom,然后按上面说的加<modules>空节点。
  5. 在根工程上右键 →New→Module,左侧选Maven,输入my-common,点 Finish。

连续重复第五步,把my-service、my-web都建出来。IDEA 会主动修改根 pom 的<modules>,不需要你手填,但你最好打开根 pom 确认一下,因为有些老版本 IDEA 不会同步得非常及时。

到这一步,你已经完成了“新建 module”的第一层。也就是说,IDEA 中的目录结构变成了:

my-parent ├── pom.xml ├── my-common │ ├── pom.xml │ └── src ├── my-service │ ├── pom.xml │ └── src └── my-web ├── pom.xml └── src

看着这个结构,你心里要有数:根 pom 是个壳,真正的代码全部在子模块里。

3.2 为每个 module 补齐 parent 与依赖元信息

新建出来的子模块 pom.xml 大概率长得很简单,只有 artifactId 和 dependencies 空节点,甚至没有 parent 节点。你需要手动改成这样:

<?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>my-parent</artifactId> <version>1.0.0</version> </parent> <artifactId>my-common</artifactId> </project>

这里有个关于relativePath的细节,IntelliJ 里新建的 module 默认会在生成的parent里自动加上../pom.xml,这本来没问题。但如果你新建的 module 并不在根目录下,而是嵌套在某个子目录里,那就要注意路径层级,必要时手动改为相对路径。大多数时候,保持../pom.xml就好。

3.3 设置模块依赖,这才是新建 module 的核心目的

现在我有了三个 module,但它们彼此孤立,工程根本称不上“多模块”。要让 module 协作,就得配置依赖关系。典型场景是:my-web依赖my-service,my-service依赖my-common。

在my-service的 pom.xml 里加:

<dependencies> <dependency> <groupId>com.example</groupId> <artifactId>my-common</artifactId> <version>${project.version}</version> </dependency> </dependencies>

在my-web的 pom.xml 里加:

<dependencies> <dependency> <groupId>com.example</groupId> <artifactId>my-service</artifactId> <version>${project.version}</version> </dependency> </dependencies>

这里用${project.version}是我推荐的做法。因为父 pom 的版本号升级时,子模块之间依赖的版本号不用一个个改,这算是用 Maven 属性管理版本的一个基础实践。

加完依赖后,不要急着写代码。先在 IDEA 右侧 Maven 面板里点击一下Reload All Maven Projects,让依赖关系刷进 IDEA 的类路径里,否则代码里 import 时会一片红。

3.4 社区版与专业版的差异处理

热词里有人问“IntelliJ IDEA 社区版怎么用 Spring Boot”,这里顺便说清楚。IDEA 社区版功能上确实少了一些 Spring Initializr 的集成向导,新建工程时没有Spring Initializr选项。但新建 Maven module 完全不受影响。我平时在家练习用的就是社区版,操作路径一样:先建 Maven 父工程,再新建 Maven 子模块,然后手动在 pom 里加 Spring Boot 相关依赖。或者你去 start.spring.io 下载一个 Spring Boot 初始工程,把它作为父工程,再往里加子模块,也是可以的。区别只是少了一点自动生成,多了一点手动配置,反而能帮你加深理解。

4. 新建完 module 后,如何跑起来第一个 Spring Boot 接口

4.1 启动类放在哪个 module,Bean 扫描边界怎么定义

很多人在多模块项目里遇到的第一个真正的运行期问题,是“日志显示启动成功,但 Controller 404”。究其原因,绝大多数是启动类位置放错了。

Spring Boot 默认只会扫描启动类所在包及其子包。比如你把@SpringBootApplication放在了com.example.web包里,那com.example.service包里的@Service可能根本不会被扫描注册。这里有两个办法:

第一个办法,把所有业务模块的包名统一成同前缀。例如都用com.example.common、com.example.service、com.example.web,然后启动类放在com.example.web.boot下,这样 Spring Boot 默认扫描com.example,三个模块的组件都能扫到。这是最推荐的做法。

第二个办法,在启动类上显式指定扫描包:

@SpringBootApplication(scanBasePackages = "com.example") public class WebApplication { public static void main(String[] args) { SpringApplication.run(WebApplication.class, args); } }

这么做虽然简单粗暴,但要注意:如果模块很多,扫描整个根包可能会引入一些你不想托管的组件。更好的做法是结合@ComponentScan的 includeFilters 或自定义注解,不过初学者用统一包名最稳妥。

启动类本身,我建议放在最外层的 web 模块,或者单独建一个xxx-boot模块。不要在好几个模块里各放一个带@SpringBootApplication的类,否则运行时容易遇到多个 DataSource 自动配置、多个 ApplicationContext 初始化之类的问题。

4.2 多模块下的配置文件加载顺序

单模块项目里,application.yml放在src/main/resources下,Spring Boot 会自动加载。多模块项目里情况就会变得有点微妙:每个 module 都有src/main/resources,到底读哪个?

我的实践经验是,不要指望所有模块的 resources 目录都会被自动加载到 classpath 的根路径。如果你的启动模块是my-web,它依赖了my-service,那么my-service的src/main/resources/config下的配置文件并不一定会被当成 Spring Boot 的默认配置。Spring Boot 默认只会从启动模块的 classpath 根目录读取application.yml、application-{profile}.yml。

想要加载其他模块的配置文件,有几种方案:

  • 把公共配置统一放在启动模块的resources下,比如数据源配置、Redis 配置都由 web 模块负责加载。
  • 使用 Spring Boot 2.4 之后的spring.config.import语法,在启动模块里显式导入其他路径的配置。
  • 使用配置中心,比如 Nacos、Spring Cloud Config,把配置完全从工程中抽出去。

对于学习和前期项目,我建议就用第一种方案,简单直接。等后面项目大了,配置多了,再考虑配置中心。

4.3 实战扩展:用新建的 module 跑通一个 Spring Security 受保护接口

新建了三个模块,如果不做点功能验证,你很难判断它是否真的“通了”。所以下面我带你做一个最小可验证的实践:在my-web里加 Spring Security,然后提供一个需要登录才能访问的接口。

先在父 pom 的 dependencies 里加一个 Spring Boot 版本管理不用写 version 的依赖。注意我是在my-web模块的 pom 里加:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>

然后写一个最简单的配置类:

package com.example.web.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/api/public/**").permitAll() .anyRequest().authenticated() ) .httpBasic(); return http.build(); } }

再在my-web里写两个接口:

@RestController @RequestMapping("/api") public class DemoController { @GetMapping("/public/hello") public String publicHello() { return "public hello"; } @GetMapping("/private/hello") public String privateHello() { return "private hello"; } }

启动my-web后,访问/api/public/hello会被放行,访问/api/private/hello则会弹出登录框。默认用户名user,密码在启动日志里,是一个 UUID。这就完整验证了新建的 module 可以被 Spring Boot 正常启动、扫描、依赖,而不仅仅是在 IDEA 里能编译通过。

5. 新建 module 过程中最常见的几个报错

5.1 Maven 依赖解析失败,IDEA 能识别但命令行构建报错

这个坑我踩过无数次。表现是:IDEA 里面代码不报红、import 正常,但执行mvn clean package时,报“Could not resolve dependencies for project ...”。

为什么会出现这种“看似正常、实则失败”的情况?因为 IDEA 的编译机制和 Maven 构建机制不完全一致。IDEA 可以基于当前工程下的多个模块直接做模块间依赖解析,但 Maven 命令式构建时,如果一个模块 A 依赖了同一个工程里的模块 B,Maven 需要先确保 B 被正确安装到本地仓库。如果你的 B 模块还没有执行过install,或者 B 模块发生了重大变更却没有 install,Maven 就会拿着旧坐标去本地仓库找,找不到就报错。

解决办法很直接:

mvn clean install -DskipTests

在父工程根目录执行一次,把整个工程所有模块都 install 到本地仓库。以后每次某个底层模块有改动,至少要把改动的模块重新 install 一次。不要觉得这个步骤多余,它恰恰是很多人第一次接触多模块工程时最容易忽略的一环。

5.2 启动时 ClassNotFoundException 或 NoClassDefFoundError

这类错误通常发生在你通过 IDEA 启动 Spring Boot 后,发现某个来自 common 模块的工具类找不到了。出现的原因一般有两个。

第一个原因是依赖范围问题。比如你在my-web中依赖my-service,而my-service依赖my-common时用的是<scope>provided</scope>,那my-common的类不会传递到my-web的运行时 classpath。检查 pom 里有没有把provided乱用。

第二个原因是打包插件配置问题。Spring Boot 多模块最终打包时,通常只在可执行模块配置spring-boot-maven-plugin。如果每个模块都配了 plugin,或者启动模块没有配 plugin,那么打出来的 jar 可能不是 fat jar,依赖的类自然不会带全。

修正方式:在启动模块my-web的 pom 里加:

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

其余模块不需要配置这个插件,保持默认打包即可。

5.3 模块之间循环依赖,和 Spring 三级缓存不是一回事

有些人看到“循环依赖”就会激动地想到 Spring 三级缓存,但那是两码事。

模块层面,如果my-web依赖my-service,同时my-service又反过来依赖my-web,Maven 在编译阶段就会直接失败,因为两个模块互相等待对方先构建完成。这跟 Spring IoC 容器内的对象循环依赖完全不同。Spring 三级缓存解决的是“容器里两个 Bean 互相注入”的问题,而 Maven 模块循环依赖无法靠 Spring 容器缓存解决,只能从结构上打破。

我的建议是:依赖方向必须保持单向。比如common层不依赖任何业务层,service层依赖common,web层依赖service。如果业务上确实需要相互访问,可以通过引入一个更底层的模块来承载公共接口,或者把需要互相调用的部分下沉到common模块。这个意识从新建 module 的第一天就要建立,否则后面越拆越乱。

6. 我的几个实操习惯和建议

6.1 模块命名一定要统一且可读

很多同学建 module 时名字乱来:module1、utils、demo-server,过一个月自己都看不懂。我常用的命名规则是:

  • 顶层:project-name-parent
  • 通用能力:project-name-common
  • 接口定义/DTO:project-name-api
  • 数据访问:project-name-dal
  • 业务逻辑:project-name-service
  • 启动层:project-name-web或project-name-boot

如果是业务模块,就按照业务先分词,比如user-service、order-service。模块名统一使用中划线,不要用下划线,因为 Java 包名和 Maven artifactId 习惯上保持一致更好。

6.2 用 dependencyManagement 把版本管起来

我见过太多项目,每个模块的 pom 里写着一堆版本号,还有像2.7.3、3.2.0这种打架的版本。多模块工程应该用好父 pom 的<dependencyManagement>。把所有的第三方依赖版本集中在父 pom 里声明,子模块只写 artifactId,这样升级依赖版本时只改一处。

如果你做的是 Spring Boot 项目,继承spring-boot-starter-parent后,Spring 相关的依赖已经不用写版本了,但其他第三方库,建议还是手动管。每次升级依赖后,在根目录跑一遍mvn dependency:tree看看完整的依赖树,能帮你发现重复、冲突、旧版本的问题。

6.3 新建 module 不是终点,多模块只是起点

我在实操中的体会是,新建 module 本身没什么技术含量,真正的门槛在于你如何看待“模块边界”。是依赖工具类就用 common,是依赖接口就下沉接口,是依赖领域就抽独立服务,这是需要一点点积累的。一个项目拆得好不好,不是看有多少个 module,而是看修改一个功能时需要动几个 module。能尽量控制在两三个以内,说明边界划得还可以。

如果你后续打算往 Spring Cloud 微服务方向走,那现在的多模块结构就是很好的底子。每个业务模块未来可以独立成微服务,公共模块继续承担依赖治理。Spring AI 出现之后,我还会习惯单独留一个ai-module放和大模型交互的客户端、提示词模板,这也算多模块工程带来的便利。

最后分享一个小技巧:IDEA 里多模块工程改完 pom 后,如果依赖一直刷新不出来或报一些奇怪的错误,不要急着重启电脑,先执行File→Invalidate Caches清理一下缓存,再Reload All Maven Projects。大部分“新建 module 之后整个工程都崩了”的恐慌,其实都是缓存和索引的问题,项目本身往往是好的。希望这篇内容能让你下次点击 New Module 的时候,心里更有底。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询