在进行分布式系统开发时,我们通常会创建多个模块的工程项目。即每一个功能就是一个Spring Boot工程,作为一个个模块,然后这些模块都会有一个父模块,父模块通常没有代码只有一个pom.xml
。
今天就来分享一下Spring Boot如何创建一个多模块项目,以创建一个两个子模块的工程为例。
1,创建父模块
在IDEA中,创建一个Spring Boot项目,但是不勾选任何依赖:
创建好之后,将父模块中除了pom.xml
文件之外的全部文件删除:
因为父模块只是做一个模块和依赖管理的作用,因此不需要代码。
然后修改这个父模块的pom.xml
文件,首先把dependencies
节点、build
节点和properties
全部删除:
然后修改版本号为自己定义的(方便后续子模块指定父模块):
然后修改父模块打包方式为pom
,在其中加入如下语句即可:
<packaging>pom</packaging>
好的,到这里父模块修改就完成了!
2,创建子模块并继承父模块
在左边项目树中父模块位置右键新建Spring Boot工程:
然后把子模块不需要的文件也删掉(只留pom.xml
和src
文件夹):
修改该子模块的pom.xml
文件,首先把子模块parent
中的工件坐标改成和上述父模块一致:
然后删除子模块的groupId
节点,因为通常子模块继承父模块,子模块的组id是和父模块的一致的:
ok,到此子模块创建并配置完成!此时这个子模块(工件名module-one
)就继承了刚刚的父模块。
然后以这个步骤再创建一个子模块module-two
:
最后整个工程就创建完成了!总共两个子模块。
子模块的
groupId
通常和父模块一致,artifactId
则是子模块自己的要自定义,version
也可以自定义。不过子模块的version
也可以省略,这样的话子模块版本也会继承父模块版本。
3,在父模块中指定子模块
回到父模块的pom.xml
文件,添加modules
节点,在其中加入module
节点以指定子模块:
需要注意的是,module
节点中的内容是子模块工程的文件夹名!所以通常规范起见子模块工程的文件夹名通常和它的组件名一致。
然后在父模块文件夹中执行mvn clean package
试试:
可以看见构建打包成功,以及每个模块的构建时间。
到此,多模块项目就创建完成了!
4,子模块之间的互相引用
在多模块项目中,子模块的互相引用也很方便。
比如说上述module-one
要调用module-two
中的类,就直接把module-two
的工件坐标加入到module-one
的pom.xml
的依赖部分即可!
更新一下Maven工程,就可以在module-one
中调用module-two
的类了!
不过这个时候运行工程是没有任何问题的,但是打包会出错:虽然module-one
依赖了module-two
,但是仍然会在打包module-one
的时候,提示找不到module-two
中的类。
这是由于Spring Boot打包的模式问题,我们打开被依赖模块module-two
的pom.xml
文件找到最下面<build>
节点中,在spring-boot-maven-plugin
插件部分中加入下面配置:
<classifier>exec</classifier>
最终如下:
这个时候对父模块打包,就成功了!
5,依赖管理
多模块项目中模块变多了,依赖管理不当也会导致很多莫名其妙的问题。但是如果对每个模块分别管理依赖及其版本,会相当麻烦。
(1) 共用的依赖
假设上述module-one
和module-two
都需要依赖fastjson2
,我们平常并不会依次在module-one
和module-two
中分别单独加入其依赖,而是直接在父模块pom.xml
中指定,和平时一样,在父模块的pom.xml
的dependencis
节点中加入即可:
这样,子模块中即使是不加入fastjson2
依赖,也可以使用这个库了!因为子模块除了可以使用自己的依赖之外,还会向上查找父模块的依赖,也就是说,父模块的依赖是向下继承的,因此对于所有模块都要使用的依赖,我们可以写在父模块中。
所以,两个模块都依赖于
Spring Web
话,也可以将两个模块的Spring Web
依赖移至父模块。
所以说父模块和子模块中,依赖也有着继承的关系!事实上,父模块的properties
也是向下继承的。
(2) 依赖版本管理
假如现在module-one
依赖于okhttps
的4.0.0
版本,而module-two
依赖于commons-io
的2.11.0
版本,显然这时我们不适合再在父模块中加入了,还是各自加入对应依赖。
目前因为只有两个模块,这么做看起来很合理。但是假设现在又多了module-three
,module-four
等等,它们也是分别依赖于okhttps
与commons-io
,那么我们又要分别在这两个模块中分别单独加入依赖。
摸块 | 依赖 |
---|---|
module-one 和module-three | okhttps |
module-two 和module-four | commons-io |
可能一开始没有问题,但是后面模块依赖版本需要更新,但是一个个地更新难免会有疏漏,导致部分模块虽然依赖一样但是版本不一致,最后整个系统也出现了莫名其妙地错误,还难以排查。
就假设后面你把module-one
的okhttps
更新到了4.0.1
版本,但是module-three
的okhttps
仍然是4.0.0
版本,这样整个系统就可能出现问题甚至无法启动。
尤其是模块变成十几个甚至上百个了,一个个地手动修改是几乎不可能的,要如何让每个模块使用的依赖版本统一呢?
这时,我们就要借助dependencyManagement
标签了!
dependencyManagement
用于管理依赖的版本,我们在父模块的pom.xml
加入这个标签:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>cn.zhxu</groupId>
<artifactId>okhttps</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies>
</dependencyManagement>
然后,在子模块中,就可以把对应的依赖版本去掉了!
可见这里,子模块中只需要引入对应依赖,而不需要指定版本了!因为子模块会向上查找父模块中dependencyManagement
标签中对应依赖的版本。
这样,就起到了统一管理版本的作用,只需要在父模块的dependencyManagement
中修改对应依赖版本,子模块中对应依赖都会相应地使用这个版本。
dependencyManagement
的注意事项:
dependencyManagement
仅用于管理版本,而不会为自己以及子模块导入依赖,因此在dependencyManagement
中声明依赖后,对应的子模块仍然需要在dependencies
中加入依赖- 在
pom.xml
中dependencyManagement
和dependencies
同级,并且dependencyManagement
中也需要有一个dependencies
dependencyManagement
不仅可以管理子模块的依赖版本,也可以管理自身的依赖版本- 若不想让某个子模块使用父模块
dependencyManagement
的版本,那就在这个子模块的dependencies
中声明对应版本 dependencyManagement
还会对自身或者子模块间接依赖的依赖版本产生影响,例如父模块的dependencyManagement
中声明了mongodb-driver-sync
的4.11.1
版本,子模块引入了spring-boot-starter-data-mongodb
且版本为2.7.11
,而spring-boot-starter-data-mongodb
依赖mongodb-driver-sync
的4.6.1
版本,但是受到父模块版本管理影响,子模块实际间接引入了mongodb-driver-sync
的4.11.1
版本,这就可能导致spring-boot-starter-data-mongodb
的2.7.11
版本和mongodb-driver-sync
的4.11.1
版本不能互相兼容,导致项目启动失败,因此这种情况下可能就要避免在父模块依赖管理中声明mongodb-driver-sync
了
(3) 总结
总而言之,对于所有子模块都共用的依赖,我们只需在父模块的dependencies
中引入这个依赖即可,而不需要再在子模块pom.xml
中引入。
而对于不是所有子模块都需要的依赖,而是部分子模块需要的,又要统一版本管理,这时除了在需要这个依赖的子模块中引入依赖之外,还需要在父模块中的dependencyManagement
声明这个依赖及其版本,这时,可以去掉子模块中对应依赖的版本号,使其遵循父模块中声明的版本。
事实上,整个项目父模块之下还可以有父模块(package
属性为pom
的),然后再在其下有子模块,这样多级父模块的结构大家也可以进行尝试或者参考下列示例仓库。
6,再看parent
和modules
标签
在上述无论是子模块继承父模块还是父模块中声明子模块,主要就是用到了parent
标签和modules
标签。那么这里有一个疑问了:既然子项目使用了parent
指定父项目,那父项目中为什么还需使用modules
指定子项目?这么做是否有些冗余?
事实上,这两个标签不仅仅是指定关系,还有着其它不同的作用。
(1) parent
标签
parent
标签就是用于声明该子模块的父模块了!也就是说,一个子模块要继承父模块,只需在parent
中声明父模块的工件坐标即可。
当多个子模块继承了同一个父模块时:
- 它们都会继承父模块中的依赖、插件、属性等等,通常是继承父模块下列部分:
groupId
父模块的组id
version
父模块的版本号properties
父模块中的属性变量dependencies
父模块中定义的依赖关系,子模块可以继承这些依赖项dependencyManagement
父模块中定义的依赖管理部分,子模块可以继承这些依赖项的版本号等信息,但不会自动引入依赖项pluginRepositories
父模块中定义的插件仓库repositories
父模块中定义的依赖仓库plugins
父模块使用的插件- ...
- 这些子模块之间也可以相互引用
在上述例子中,我们的子模块文件夹和父模块pom.xml
在同一目录下,如果不在同一目录呢?那还需要在子模块的parent
中使用relativePath
来指定父模块的pom.xml
的相对路径。
例如现在父项目的pom.xml
文件不在子项目的上一级目录中,而是在子项目的上两级目录中,你可以将relativePath
设置为../../pom.xml
如下:
<parent>
<groupId>com.gitee.swsk33</groupId>
<artifactId>total-module</artifactId>
<version>1.0.0</version>
<!-- 指定父项目pom.xml位置 -->
<relativePath>../../pom.xml</relativePath>
</parent>
注意,relativePath
中指定的是相对路径,并且是父模块pom.xml
相对于该子模块pom.xml
文件的路径,relativePath
属性的默认值是../pom.xml
,所以当父模块pom.xml
就位于子模块的上一级目录时,就可以省略该属性配置。
除此之外,子模块也是可以覆盖从父模块继承来的属性值的,比如说在properties
中,父模块定义了属性java.version
值为1.8
,而子模块想将其覆盖掉,使用自定义的值17
,那么只需在子模块properties
中定义同名属性java.version
值为17
即可,其余部分同理。
(2) modules
标签
当你现在需要构建父项目,比如在父项目的根目录下执行构建命令(如mvn clean install
)时,所有继承了该父项目的子模块都会被构建吗?显然不是的。
只有声明在父项目的modules
中的子模块,才会被构建。
想必现在大家知道了两者的区别了,parent
声明继承,主要是继承属性、依赖等等父项目的属性,而父项目中的modules
是指定哪些子模块会随着该父模块一起被构建。
所以如果你的项目中有的用于测试的子项目需要继承父项目,但是又不希望这些用于测试的模块被构建,你就可以不在父项目中的modules
中声明它们。
示例仓库地址:
本章节中,父模块也是继承了Spring Boot的Parent的,如果说对于更加复杂的情况,比如说我们的父模块不能继承Spring Boot的Parent,子项目需要继承父项目并且需要使用Spring Boot时,可以借助依赖管理的导入功能,可以参考这篇文章:传送门