在 Spring Boot 项目中使用多模块(Maven 或 Gradle)是一种非常好的实践,它有助于将大型项目拆分为职责清晰、易于管理和维护的模块。

下面我将详细解释多模块项目与普通单模块项目的区别、需要修改的地方以及如何正确使用。

核心思想

多模块项目的核心是有一个父模块(Parent Module) 来聚合多个子模块(Child Module)。父模块本身通常不包含业务代码,它主要做三件事:

  1. 项目管理:定义所有子模块。
  2. 依赖管理:统一管理所有子模块共用的依赖版本。
  3. 插件管理:统一配置所有子模块共用的插件。

子模块则是具体的功能单元,例如:

  • app-web:Web 控制器层
  • app-service:业务服务层
  • app-daoapp-repository:数据持久层
  • app-commonapp-core:通用工具、配置、模型等
  • app-generator:代码生成器等特殊任务模块

与普通项目的区别及需要修改的地方

我们将从项目结构、父 POM、子 POM、启动类、包扫描和依赖传递这几个关键点来阐述。

1. 项目结构 (Project Structure)

普通单模块项目结构:

1
2
3
4
5
6
7
8
monolithic-project/
├── src/
│ ├── main/
│ │ ├── java/
│ │ └── resources/
│ └── test/
├── pom.xml
└── ...

多模块项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
multi-module-project/       (父项目/根目录)
├── app-web/ (子模块1,通常是启动模块)
│ ├── src/
│ │ ├── main/
│ │ └── test/
│ └── pom.xml
├── app-service/ (子模块2)
│ ├── src/
│ │ ├── main/
│ │ └── test/
│ └── pom.xml
├── app-dao/ (子模块3)
│ ├── src/
│ │ ├── main/
│ │ └── test/
│ └── pom.xml
├── app-common/ (子模块4)
│ ├── src/
│ │ ├── main/
│ │ └── test/
│ └── pom.xml
└── pom.xml (父POM)

修改点:你需要按照模块职责创建上述目录结构。

2. 父模块的 POM.xml

这是变化最大的地方。父 POM 的打包类型必须为 pom,并且要声明子模块。

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
<?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>

<!-- 父项目坐标 -->
<groupId>com.example</groupId>
<artifactId>multi-module-project</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging> <!-- 关键!打包类型为 pom -->

<name>multi-module-project</name>
<description>Demo project for Spring Boot Multi-Module</description>

<!-- 声明所有子模块 -->
<modules>
<module>app-common</module>
<module>app-dao</module>
<module>app-service</module>
<module>app-web</module>
</modules>

<!-- 继承Spring Boot父项目,以获取默认配置 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version> <!-- 使用最新稳定版 -->
<relativePath/>
</parent>

<!-- 统一依赖管理 -->
<dependencyManagement>
<dependencies>
<!-- 在这里声明所有子模块可能用到的依赖及其版本 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version> <!-- 版本通常由父项目管理,这里可直接引用 -->
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>
<!-- 其他公共依赖 -->
</dependencies>
</dependencyManagement>

<!-- 公共插件管理 -->
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

3. 子模块的 POM.xml

每个子模块需要声明其父模块,并且只需要引入自己直接需要的依赖,无需指定版本(版本由父模块的 dependencyManagement 统一控制)。

app-common 模块为例:

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
<?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>multi-module-project</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <!-- 指向父POM文件 -->
</parent>

<artifactId>app-common</artifactId> <!-- 子模块自己的坐标 -->
<packaging>jar</packaging> <!-- 子模块通常是jar -->

<dependencies>
<!-- 只需要声明依赖,不需要版本 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

app-web (最终启动模块)为例,它需要依赖其他子模块:

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
... (同上,parent部分)
<artifactId>app-web</artifactId>
<packaging>jar</packaging>

<dependencies>
<!-- 引入Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 依赖本项目内的其他模块 -->
<dependency>
<groupId>com.example</groupId> <!-- 与父groupId相同 -->
<artifactId>app-service</artifactId> <!-- 子模块的artifactId -->
<version>0.0.1-SNAPSHOT</version> <!-- 与父version相同 -->
</dependency>

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

<build>
<!-- 作为可执行Jar的模块需要配置Spring Boot Maven Plugin -->
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
...

4. 启动类 (Application Class) 和包扫描

重要规则只有一个模块会包含启动类(即 main 方法),通常是 app-web 这种最顶层的模块。

关键问题:组件扫描(Component Scan)
默认情况下,Spring Boot 启动类只会扫描它所在包及其子包下的组件(如 @Component, @Service, @Controller等)。

如果你的实体类、Service、Dao 分布在不同的模块中,你必须确保它们的包名是启动类包名的子包

最佳实践

  • 基础包名:在父 POM 中定义一个统一的 groupId,例如 com.example
  • 子模块包结构:每个子模块的代码都从这个基础包名开始。
    • app-web 模块的代码放在 com.example.web 包下。
    • app-service 模块的代码放在 com.example.service 包下。
    • app-dao 模块的代码放在 com.example.dao 包下。
    • app-common 模块的代码放在 com.example.common 包下。

这样,启动类 com.example.web.Application@SpringBootApplication 注解默认就能扫描到 com.example 下的所有组件(web, service, dao, common 都是其子包)。

如果无法修改包名,你可以在启动类上使用 @ComponentScan 注解手动指定要扫描的包:

1
2
3
4
5
6
7
@SpringBootApplication
@ComponentScan({"com.example.web", "com.example.service", "com.example.dao"}) // 手动指定包
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

5. 依赖传递 (Dependency Transitivity)

Maven 的依赖是传递的。这意味着:

  • 如果 app-service 依赖了 app-dao
  • app-web 又依赖了 app-service
  • 那么 app-web 也会自动拥有 app-dao 的依赖,无需重复声明。

总结:操作步骤

  1. 创建父项目:使用 Spring Initializr 或 IDE 创建一个新项目,并立即将其 pom.xml<packaging> 改为 pom,并删除其 src 目录(因为父项目不需要代码)。
  2. 创建子模块:在父项目根目录下,右键新建 Module。IDE(如 IntelliJ IDEA)通常会自动在其子模块的 POM 中正确设置 <parent>
  3. 配置父 POM:在父 POM 中定义 <modules>, <dependencyManagement>, 和 <pluginManagement>
  4. 配置子 POM:在每个子 POM 中只声明它们直接需要的依赖。
  5. 建立模块间依赖:在需要依赖其他模块的子 POM 中,像引入普通依赖一样引入兄弟模块(使用它们的 groupId, artifactId, version)。
  6. 规划包结构:确保所有模块的包名是启动类包名的子包,或者手动配置组件扫描。
  7. 编写代码:像在普通项目中一样在各个模块中编写代码。
  8. 构建和运行
    • 构建整个项目:在根目录(父POM所在目录)执行 mvn clean install。Maven 会根据 <modules> 的顺序自动构建所有子模块。
    • 运行项目:只需要运行包含启动类和 spring-boot-maven-plugin 的那个模块(通常是 app-web)。你可以通过 IDE 启动,或者进入该模块目录执行 mvn spring-boot:run

总而言之,多模块项目并不是简单地把代码分开就行,而是需要通过 Maven/Gradle 的聚合与继承机制进行有机的整合。正确配置后,它的优势(代码复用、关注点分离、构建优化)会非常明显。