基于 springboot + maven 开发,这里整理一波多模块(module)项目的开发管理。

1. 聚合与继承

多模块的项目结构,基本离不开聚合继承两种maven管理方式。二者不是相悖的,很多项目结构是二者组合在一起管理的。

1.1. 聚合

目的

可以一次构建多个模块的项目。

当一个项目中有多个需要构建的模块项目时,如果每个模块单独构建,太费工作量。最好可以基于某个模块一次构建配置的所有模块,这就是聚合。

应用方式

先确定一个专门用于打包的maven项目,针对该项目pom 文件做以下的特殊处理:

  • <packaging> 值为 pom。(后续会讲 packaging 值的区别)
  • 基于<modules> 申明需要打包的所有模块,来实现模块的聚合。

1.2. 继承

1、目的

减少重复的配置。

继承比较好理解,类似于java类的继承,子模块可以自动继承父模块的一些属性。在maven项目中,可以把多个子模块共同的配置放到父模块中,那么子模块就不需要重复维护配置了。

2、应用方式

先确定一个父模块项目,针对该项目pom 文件做以下的特殊处理:

  • <packaging> 值为 pom。(后续会讲 packaging 值的区别)
  • 子模块中需要声明:(1)<parent> 为父模块的信息;(2)<relativePath>为父模块 pom 的相对路径。当项目构建时,Maven会首先根据 <relativePath> 检查父POM,如果找不到,再从本地仓库查找。
3、配置中可继承的元素
  • groupId :项目组 ID ,项目坐标的核心元素;
  • version :项目版本,项目坐标的核心元素;
  • properties :自定义的 Maven 属性;
  • dependencies :项目的依赖配置;
  • dependencyManagement :醒目的依赖管理配置;
  • repositories :项目的仓库配置;
  • build :包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等;
  • reporting :包括项目的报告输出目录配置、报告插件配置等;
  • description :项目的描述信息;
  • organization :项目的组织信息;
  • inceptionYear :项目的创始年份;
  • url :项目的 url 地址
  • develoers :项目的开发者信息;
  • contributors :项目的贡献者信息;
  • distributionManagerment :项目的部署信息;
  • issueManagement :缺陷跟踪系统信息;
  • ciManagement :项目的持续继承信息;
  • scm :项目的版本控制信息;
  • mailingListserv :项目的邮件列表信息;
4、dependencies和dependencyManagement(plugins与pluginManagement)

(1)dependencies

如果父项目pom中定义的是单独的 dependencies,则代表引用对应的所有依赖项。其所有子项目的pom中,就算没有引入父项目中定义的依赖,也自动会继承父项目pom文件 dependencies 中的所有依赖项。

(2)dependencyManagement

父项目pom中只是声明依赖,并不实现引入,因此子项目需要显示的声明需要的依赖。当父项目中申明过一些依赖项目,但如果不在子项目中声明依赖,是不会从父项目中继承的。

另外,只有在子项目中写了该依赖项,并且没有指定具体版本,才会从父项目中继承该项,并且version和scope都读取自父pom;
如果子项目中指定了版本号,那么会使用子项目中指定的version和scope版本。

一般推荐用 dependencyManagement,因为配置起来更灵活,子项目应该按需配置需要的依赖项,以及可自定义版本号。但 dependency 也有它的作用,看实际情况搭配了。

pluginspluginManagement 的关系就如同 dependenciesdependencyManagement,这里就不多说了。

代码不可被继承

要注意,前面讲的继承都是 pom 文件配置中的元素可被继承。但父模块中的代码是不可以被继承的。

如果你说,想和子模块依赖公共的配置一样,想将子模块公共调用的代码放到父模块中,这样子模块就能公共调用了。非放父模块也行,可以在子模块中引入 build-helper-maven-plugin 等插件,将父模块的代码路径手动添加到子模块,如下面配置插件:

    <properties>
        <main.basedir>${project.basedir}/..</main.basedir>
        <shared.srcdir>${project.basedir}/src</shared.srcdir>
    </properties>

    ... ...

    <build>
        <plugins>
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>build-helper-maven-plugin</artifactId>
                    <version>${build-helper-maven-plugin.version}</version>
                    <inherited>true</inherited>
                    <executions>
                        <execution>
                            <id>add-source</id>
                            <phase>generate-sources</phase>
                            <goals>
                                <goal>add-source</goal>
                            </goals>
                            <configuration>
                                <sources>
                                    <source>${shared.srcdir}/main/java</source>
                                </sources>
                            </configuration>
                        </execution>
                        <execution>
                            <id>add-test-source</id>
                            <phase>generate-sources</phase>
                            <goals>
                                <goal>add-test-source</goal>
                            </goals>
                            <configuration>
                                <sources>
                                    <source>src/it/java</source>
                                    <source>${shared.srcdir}/test/java</source>
                                </sources>
                            </configuration>
                        </execution>
                        <execution>
                            <id>add-test-resource</id>
                            <phase>generate-resources</phase>
                            <goals>
                                <goal>add-test-resource</goal>
                            </goals>
                            <configuration>
                                <resources>
                                    <resource>
                                        <directory>src/it/resources</directory>
                                    </resource>
                                    <resource>
                                        <directory>${shared.srcdir}/test/resources</directory>
                                    </resource>
                                </resources>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
        </plugins>
    </build>

这的做法比较麻烦,还不如定义一个公共的模块,将需要多模块复用的代码都放入该模块中。然后在子模块的pom中引用公共模块的依赖即可。后面的例子中会有这种体现(demo-comm)。

1.3. 聚合与继承比较

聚合是为了方便快速构建项目,而继承是为了消除重复配置。

相同点:打包方式都是pom,除了pom文件之外没有其他实际的内容。

不同点:聚合是聚合模块通过module引用被聚合模块,而继承是子模块通过parent引用父模块。

实际项目中通常把聚合和继承结合起来一起使用。parent项目既是聚合模块,也是父模块。

2. 多模块示例

下面会举一个多模块项目的简单例子,便于了解核心的配置过程。

2.1. 代码结构

示例的maven项目就叫 demo-service。删掉了 src目录,父pom 中定义的 artifactId 为 demo-parent。下面简单列了三个子模块:

  • demo-api: 对外提供Http API的可启动模块。
  • demo-mqc: MQ消费端的可启动模块。
  • demo-comm:被其他子模块公共依赖的不可启动模块。

代码结构如下:

.
├── HELP.md
├── demo-api
│   ├── HELP.md
│   ├── demo-api.iml
│   ├── mvnw
│   ├── mvnw.cmd
│   ├── pom.xml
│   └── src
│       └── main
│           ├── java
│           │   └── pers
│           │       └── kerry
│           │           └── demoapi
│           │               ├── DemoApiApplication.java
│           │               └── controller
│           │                   └── DemoController.java
│           └── resources
│               └── application.properties
├── demo-comm
│   ├── HELP.md
│   ├── demo-comm.iml
│   ├── mvnw
│   ├── mvnw.cmd
│   ├── pom.xml
│   └── src
│       └── main
│           ├── java
│           │   └── pers
│           │       └── kerry
│           │           └── democomm
│           │               ├── config
│           │               │   └── DemoConfig.java
│           │               ├── mapper
│           │               │   └── DemoMapper.java
│           │               └── service
│           │                   └── DemoService.java
│           └── resources
│               ├── application.properties
│               └── mapper
│                   └── DemoMapper.xml
├── demo-mqc
│   ├── HELP.md
│   ├── demo-mqc.iml
│   ├── mvnw
│   ├── mvnw.cmd
│   ├── pom.xml
│   └── src
│       └── main
│           ├── java
│           │   └── pers
│           │       └── kerry
│           │           └── demomqc
│           │               ├── DemoMqcApplication.java
│           │               └── consumer
│           │                   └── DemoConsumer.java
│           └── resources
│               └── application.properties
├── demo-service.iml
├── mvnw
├── mvnw.cmd
└── pom.xml

当然还可以基于业务再做模块细分,如:

  • 横向:(1)可增加 demo-socket 服务处理长连接;(2)可增加 demo-job 服务处理如 xxl-job 之类的服务。
  • 纵向:(1)demo-api 如果有多个业务 api,可以在上面在提炼一层 api-apps 模块(demo-parent -> api-apps -> order-api、log-api);(2)公共模块的服务如果可细分,不希望其他模块依赖全量的公共模块代码,也可以提炼一层 comm-apps(demo-parent -> comm-apps -> order-comm、log-comm)。

2.2. pom配置(demo-parent)

pom.xml
<?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>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>pers.kerry</groupId>
    <artifactId>demo-parent</artifactId>
    <version>${revision}</version>
    <name>demo-parent</name>
    <description>demo-service</description>
    <packaging>pom</packaging>

    <properties>
        <java.version>1.8</java.version>
        <revision>0.0.1-SNAPSHOT</revision>
        <main.basedir>${project.basedir}</main.basedir>
        <spring-boot-starter.version>2.6.3</spring-boot-starter.version>
        <lombok.version>1.18.24</lombok.version>
        <rocketmq.version>2.2.0</rocketmq.version>
        <flatten-maven-plugin.version>1.2.7</flatten-maven-plugin.version>
        <maven-antrun-plugin.version>3.0.0</maven-antrun-plugin.version>
    </properties>

    <modules>
        <module>demo-api</module>
        <module>demo-mqc</module>
        <module>demo-comm</module>
    </modules>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>pers.kerry</groupId>
                <artifactId>demo-comm</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>${spring-boot-starter.version}</version>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.rocketmq</groupId>
                <artifactId>rocketmq-spring-boot-starter</artifactId>
                <version>${rocketmq.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>flatten-maven-plugin</artifactId>
                    <version>${flatten-maven-plugin.version}</version>
                    <configuration>
                        <updatePomFile>true</updatePomFile>
                        <flattenMode>clean</flattenMode>
                    </configuration>
                    <executions>
                        <execution>
                            <id>flatten</id>
                            <phase>process-resources</phase>
                            <goals>
                                <goal>flatten</goal>
                            </goals>
                        </execution>
                        <execution>
                            <id>flatten-clean</id>
                            <phase>clean</phase>
                            <goals>
                                <goal>clean</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-antrun-plugin</artifactId>
                    <version>${maven-antrun-plugin.version}</version>
                    <inherited>true</inherited>
                    <executions>
                        <execution>
                            <phase>validate</phase>
                            <goals>
                                <goal>run</goal>
                            </goals>
                            <configuration>
                                <target>
                                    <mkdir dir="${main.basedir}/.git/hooks"/>
                                </target>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>
2. pom配置分析

在示例中,就是一个聚合与继承合二为一的结构,具体分析 pom 特殊元素如下:

  • packaging:无论是聚合还是继承,都要求父模块或聚合构建模块中该值为pom
  • properties:可定义变量,变量值可以是 ${project.basedir} 这类maven内置变量,也可以是依赖包版本这类常量。建议在 properties 中统一维护所有 <version>,便于变量统一管理。
  • modules:作为聚合结构的项目,需要通过 modules 来申明需要构建的模块。例如,如果注释掉 <module>demo-api</module>,在 demo-parent 路径执行 mvn package 命令时,就不会打包 demo-api 的模块。
  • dependencyManagement:配置 dependencyManagement,意味着父模块只做申明,子模块可以按需引用所需要的依赖项。
  • dependency:demo-comm:demo-comm 作为公共依赖的模块,因为会有多个子模块会依赖。可直接在父模块中申明(基于dependencyManagement方式),定义好 version。这样需要用到中子模块直接申明即可,不用再定义版本号。
  • src:建议删除,原因前面有说。
  • spring-boot-maven-plugin:建议删除,该插件是构建 springboot 特殊 jar包的,该模块打包方式是pom,用不上。

2.3. pom配置(demo-comm)

1、pom.xml
<?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>pers.kerry</groupId>
        <artifactId>demo-parent</artifactId>
        <version>${revision}</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <groupId>pers.kerry</groupId>
    <artifactId>demo-comm</artifactId>
    <name>demo-comm</name>
    <description>demo-comm</description>
    <packaging>jar</packaging>

    <properties>
        <main.basedir>${project.basedir}/..</main.basedir>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
    
</project>
2. pom配置分析
  • packaging:虽然 demo-comm 中只是代码,并没有可启动的fat jar(内置tomcat 的 springboot程序)。但代码需供其他模块调用,依然是需要配置为 jar类型。
  • properties:继承了父模块中的属性,可自定义自己特性的属性值。
  • dependency:因为只需要2个依赖项,所以只需要引入父模块中2个即可。如果版本没特殊要求,可不特殊申明,继承自父模块定义的version。
  • 启动类:因为无需启动,建议删除无用的启动类。
  • spring-boot-maven-plugin:和启动项一样,因为无需启动,也无需使用 spring-boot-maven-plugin 将项目打包成 fat jar。这个插件是创建 springboot 项目时默认生成的插件,但这里必须删除掉,否则在项目构建时会报错。因为该插件在打包项目时,生成的jar(fat jar)包结构和普通jar结构不同,会导致其他模块依赖该模块代码时报错。

2.4. pom配置(demo-api、demo-mqc)

1、pom.xml(demo-api)
<?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>pers.kerry</groupId>
        <artifactId>demo-parent</artifactId>
        <version>${revision}</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <groupId>pers.kerry</groupId>
    <artifactId>demo-api</artifactId>
    <name>demo-api</name>
    <description>demo-api</description>
    <packaging>jar</packaging>

    <properties>
        <main.basedir>${project.basedir}/..</main.basedir>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>pers.kerry</groupId>
            <artifactId>demo-comm</artifactId>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-antrun-plugin</artifactId>
                <version>${maven-antrun-plugin.version}</version>
                <inherited>true</inherited>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                        <configuration>
                            <target>
                                <copy todir="${main.basedir}/target" overwrite="true">
                                    <fileset dir="${project.build.directory}">
                                        <include name="*.jar"/>
                                    </fileset>
                                </copy>
                            </target>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>
2、pom.xml(demo-mqc)
<?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>pers.kerry</groupId>
        <artifactId>demo-parent</artifactId>
        <version>${revision}</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <groupId>pers.kerry</groupId>
    <artifactId>demo-mqc</artifactId>
    <name>demo-mqc</name>
    <description>demo-mqc</description>
    <packaging>jar</packaging>

    <properties>
        <main.basedir>${project.basedir}/..</main.basedir>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>pers.kerry</groupId>
            <artifactId>demo-comm</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-antrun-plugin</artifactId>
                <inherited>true</inherited>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                        <configuration>
                            <target>
                                <copy todir="${main.basedir}/target" overwrite="true">
                                    <fileset dir="${project.build.directory}">
                                        <include name="*.jar"/>
                                    </fileset>
                                </copy>
                            </target>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
3. pom配置分析
  • packaging:两个模块都是可执行模块,需要打包成正常内置tomcat 的 springboot fat jar的。值为 jar
  • properties:继承了父模块中的属性,可自定义自己特性的属性值。
  • dependency:因为各自功能性,引入各自真正需要的依赖项。如果版本没特殊要求,可不特殊申明,继承自父模块定义的version。
  • 启动类:需要。
  • spring-boot-maven-plugin:需要。因为两个模块都是可执行模块,需要打包成正常内置tomcat 的 springboot fat jar的。

3. 工具帮助

3.1. maven内置变量

前面在 properties 的介绍中,有提到 maven内置变量,下面是一些总结:

  • ${basedir}:项目根目录
  • ${project.build.directory}:构建目录,缺省为target
  • ${project.build.outputDirectory}:构建过程输出目录,缺省为target/classes
  • ${project.build.finalName}:产出物名称,缺省为${project.artifactId}-${project.version}
  • ${project.packaging}:打包类型,缺省为jar
  • ${project.xxx}:当前pom文件的任意节点的内容

3.2. maven 构建(build)过程

maven 构建生命周期定义了一个项目构建和发布的过程。
简单来看,一个典型的 Maven 构建(build)生命周期是由以下几个阶段(phase)的序列组成的。

  1. validate 验证项目:验证项目是否正确且所有必须信息是可用的
  2. compile 执行编译:源代码编译在此阶段完成
  3. test 测试:使用适当的单元测试框架执行测试
  4. package 打包:创建JAR/WAR包如在 pom.xml 中定义提及的包
  5. verify 检查:对集成测试的结果进行检查,以保证质量达标
  6. install 安装:安装打包好项目到本地仓库, 以供其他项目使用。
  7. deploy 发布:拷贝最终打包好的工程包到远程仓库,以共享给其他项目和开发人员

它们是按照顺序执行的,当我们执行 mvn package 时,实际会自动按照 1~4 步骤执行,所以有时会发现自动执行了测试的阶段。

不过 maven 只是规定了生命周期的各个阶段和步骤,具体事情,由集成到 maven 中的插件完成。maven 在生命周期的每个阶段都设计了插件接口。用户可以在接口上根据项目的实际需要绑定第三方的插件,做该阶段应该完成的任务,从而保证所有 maven 项目构建过程的标准化。当然,maven 对大多数构建阶段绑定了默认的插件,通过这样的默认绑定,又简化和稳定了实际项目的构建。

有关maven 的内容比较深,这里就讲浅浅的这一点。

3.3. 插件:flatten-maven-plugin

可以看到,我们在 demo-parent 的 pom文件中,引入了 flatten-maven-plugin 的插件。另外所有依赖 demo-parent 的版本时,都用了 ${revision}。这个插件的作用,就是用来做统一的版本管理。

假设我们将 demo-parent 的 version 写死为 1.0.0,那么在所有子模块依赖 <parent> 的地方,version 也要写成 1.0.0。可一旦我们需要将版本升为 1.0.1,就需要手动将多处所依赖的版本号一一做修改,工作量大也容易出错。

flatten-maven-plugin 插件就可以解决这个问题,${revision} 可以被理解成一种占位符,会全局的将这个变量的值覆盖。

3.4. 插件:maven-antrun-plugin

该插件提供从Maven内运行Ant任务的功能。例如在 demo-api、demo-mqc 中的pom插件定义:

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-antrun-plugin</artifactId>
                <version>${maven-antrun-plugin.version}</version>
                <inherited>true</inherited>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                        <configuration>
                            <target>
                                <copy todir="${main.basedir}/target" overwrite="true">
                                    <fileset dir="${project.build.directory}">
                                        <include name="*.jar"/>
                                    </fileset>
                                </copy>
                            </target>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
  • <phase>package</phase> 表示插件要在Maven 的 package 时执行
  • <goal>run</goal> 这时插件内部的一个执行目标
  • <target></target> 之间可以写任何Ant支持的task

那么这段插件的作用,就是在package阶段,将子模块的生成的 jar 包复制到父模块的 /target 路径中。因为有些 DevOps 部署脚本,通常只在固定项目路径中寻找可执行jar包,而不愿意深入子模块路径。


KerryWu
641 声望159 粉丝

保持饥饿