1.分模块开发
1.1.意义
分模块开发的意义可以观看这个。
1.2.快速开始
这里作为示例的项目是,对应的示例数据是。
解压后用 Idea 打开/mave-demo/mvc-demo
可以尝试运行项目,确保项目是正常的。
新建一个子模块 mvc-demo-entity,用于拆分 mvc-demo 中的实体类部分代码:
注意,这里不需要选择父模块,且目录设置为和 mvc-demo 平级。
将 mvc-demo 模块下的 entity 包拷贝到 mvc-demo-entity 模块。
会报错,因为缺少 lombok依赖,添加:
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
</dependency>
</dependencies>
删除 mvc-demo 模块下的 entity 包。
现在 mvc-demo 模块下依赖于实体类的类会报错,因为访问不到对应的实体类了。所以这里要添加对 mvc-demo-entity 模块的依赖:
<dependency>
<groupId>cn.icexmoon</groupId>
<artifactId>mvc-demo-entity</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
好了,项目已经拆分成了两个模块,其中 mvc-demo-entity 包含了项目中的实体类部分代码,这样做是按照代码层次进行横向切分。
但是现在尝试编译(maven compile)模块 mvc-demo 会报错:
[ERROR] Failed to execute goal on project mvc-demo: Could not resolve dependencies for project cn.icexmoon:mvc-demo:war:1.0-SNAPSHOT: Could not find artifact cn.icexmoon:mvc-demo-entity:jar:1.0-SNAPSHOT -> [Help 1]
这是因为虽然子模块 mvc-demo-entity 在 Idea 中存在,但 Maven 编译的时候只会从本地仓库查找,该模块并不存在于本地仓库中,显然也不可能在中央仓库,所以就无法找到并报错。
所以需要先通过 Maven 生命周期命令maven install
将 mvc-demo-entity 模块安装到本地仓库:
[INFO] Installing D:\workspace\learn-maven\ch2\maven-demo\mvc-demo-entity\pom.xml to C:\Users\70748\.m2\repository\cn\icexmoon\mvc-demo-entity\1.0-SNAPSHOT\mvc-demo-entity-1.0-SNAPSHOT.pom
现在就可以成功编译 mvc-demo 模块了。
可以用同样的方式将持久层代码拆分成 mvc-demo-mapper 模块。
拆分后的示例代码可以从获取。
2.依赖传递
2.1.依赖传递
在我们上边的示例中,mvc-demo 模块依赖于 mvc-demo 和 mvc-demo-entity 以及 mvc-demo-mapper,而 mvc-demo-mapper 又依赖于 mvc-demo-entity,在 Idea 的依赖层级中,体现为:
现在从 mvc-demo 的依赖项中删除对 mvc-demo-entity 的依赖。现在 mvc-demo 依然可以使用相应的实体类,只不过对 mvc-demo-entity 模块的引用从直接依赖变成了间接依赖。
2.2.依赖冲突
如果在同一个模块的依赖项中添加了两个仅版本号不同的坐标,就会存在依赖冲突:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
此时后声明的依赖会覆盖前面的依赖,所以这里实际生效的是 4.12 版本的 JUnit:
如果存在冲突的依赖都是间接依赖,此时依赖层级深度较浅的依赖会生效。
比如上图中的依赖 E1 和 E2,假设它们存在依赖冲突,对于 A 来说,E1 的依赖层级是2,E2 的依赖层级是3,因此 E1 会生效。
如果是相同层级的依赖冲突,比如上图中的 D1 和 D2,此时的规则是在 A 的依赖项中先声明的会生效,换言之,如果在 A 的依赖项中,B 在 C 之前声明,那么 D1 就会生效。
2.3.依赖关系
可以通过 Idea 的 Maven 工具查看依赖之间的关系图:
示例中三个模块的依赖关系图:
3.隐藏依赖
3.1.可选依赖
可以通过可选依赖屏蔽外部对当前模块某些依赖的“可见性”。
比如当前 mvc-demo 通过 mvc-demo-mapper 依赖 mvc-demo-entity 模块:
现在 mvc-demo-mapper 模块中我们将依赖 mvc-demo-entity 设置为可选依赖:
<dependency>
<groupId>cn.icexmoon</groupId>
<artifactId>mvc-demo-entity</artifactId>
<version>1.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>
现在 mvc-demo-mapper 的依赖项中就看不到原本存在的间接依赖 mvc-demo-entity 了:
同时,相应引用实体类的控制层代码会报错。
这同样说明了,依赖项默认的optional
属性值是false
,也就是说对外部模块是可见(可以依赖传递)的。
3.2.排除依赖
如果我们不需要某些因为依赖传递而生效的间接依赖,可以在当前模块中主动排除。
假设 mvc-demo-mapper 模块使用了 Log4j 日志:
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.14</version>
</dependency>
这个依赖项不是可选的,所以显然会在模块 mvc-demo 中生效:
假设我们在模块 mvc-demo 中不打算使用 Log4j 日志,而是使用 Logback 日志,就可以将其主动排除:
<dependency>
<groupId>cn.icexmoon</groupId>
<artifactId>mvc-demo-mappper</artifactId>
<version>1.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency>
现在 mvc-demo 的依赖中就不再有 Log4j 的间接依赖了。
4.聚合和继承
4.1.聚合
现在我们已经将一个项目拆分成多个模块,这些模块之前存在依赖关系。如果模块之前有改动,要重新编译整个项目,我们就要考虑这些依赖关系。比如在当前这个示例中,就需要先编译和安装 mvc-demo-entity 模块,在编译和安装 mvc-demo-mapper 模块,最后编译 mvc-demo 模块。
可以用一个聚合模块来解决这个问题。
在 Idea 中创建一个新模块 mvc-demo-parent,这个模块将管理其它的模块,所以不需要添加任何代码。
在 pom.xml 文件中将打包方式设置为pom
:
<packaging>pom</packaging>
添加其它模块信息:
<modules>
<module>../mvc-demo</module>
<module>../mvc-demo-mappper</module>
<module>../mvc-demo-entity</module>
</modules>
module
标签中指定其它模块相对于 mvc-demo-parent 模块的路径。
现在 Maven 工具中其它模块就会被包含在 mvc-demo-parent 模块下:
对 mvc-demo-parent 模块进行编译:
[INFO] Reactor Build Order: [INFO] [INFO] mvc-demo-entity [jar] [INFO] mvc-demo-mappper [jar] [INFO] mvc-demo Maven Webapp [war] [INFO] mvc-demo-parent [pom]
可以看到输出信息中 Maven 会按照各模块之间的依赖关系将模块依次进行编译。
4.2.继承
除了可以在模块之间设置聚合关系,还可以设置继承关系。让一个模块继承另一个模块后,子模块可以继承父模块的 Maven 配置(主要是依赖项),这样做可以简化配置以及实现版本管理。
首先我们需要一个父模块,并且其打包方式为 pom
,这里就使用之前添加的 mvc-demo-parent 模块。
通常聚合和继承会同时使用,作为聚合使用的模块也同时会作为其它子模块的父模块。
在模块 mvc-demo 中添加父模块信息:
<parent>
<groupId>cn.icexmoon</groupId>
<artifactId>mvc-demo-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../mvc-demo-parent/pom.xml</relativePath>
</parent>
relativePath
标签指向父模块的 pom 文件,用于快速定位父模块,这是可选的。
子模块中通用的依赖可以放在父模块中,这样子模块中就无需再次添加依赖,可以直接从父模块中继承并使用。
这里假设所有的子模块都需要 Spring 相关依赖,因此将 Spring 相关的依赖添加到父模块:
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- ... -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.0</version>
</dependency>
</dependencies>
这些依赖就可以从子模块 mvc-demo 中删除,因为它们现在可以从父模块中继承。
并非所有依赖都是所有子模块都需要的,对于只有部分子模块会用到的依赖,可以在父模块中进行依赖管理:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.5</version>
</dependency>
</dependencies>
</dependencyManagement>
在子模块 mvc-demo-mapper 中,相应的依赖不再需要指定版本号:
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
因为该依赖已经被父依赖管理,所以版本号会使用父依赖指定的版本号:
这样做的好处是可以统一管理版本,减少版本不兼容的问题出现。
5.属性
5.1.自定义属性
项目中使用了多个 Spring 相关依赖,并且这些依赖使用的版本号相同:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
这会对版本维护造成麻烦,比如某个 Spring 依赖的版本因为疏忽设置的与其它 Spring 依赖不一致。
可以使用 Maven 属性解决这个问题:
<properties>
<spring.version>5.2.10.RELEASE</spring.version>
</properties>
properties
标签用于定义属性,子标签的名称就是属性名称。
使用属性统一版本号:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
可以用这种方式将所有依赖的版本都集中管理:
<properties>
<spring.version>5.2.10.RELEASE</spring.version>
<mysql-connector-j.version>8.0.33</mysql-connector-j.version>
<javax.servlet-api.version>3.1.0</javax.servlet-api.version>
<druid.version>1.1.16</druid.version>
<mybatis-spring.version>2.1.1</mybatis-spring.version>
<lombok.version>1.18.26</lombok.version>
<fastjson.version>1.2.75</fastjson.version>
<jackson-databind.version>2.9.0</jackson-databind.version>
<mybatis.version>3.5.5</mybatis.version>
<junit.version>4.13</junit.version>
<aspectjweaver.version>1.9.4</aspectjweaver.version>
</properties>
Idea 中在依赖的版本号上右键,选择 Refactor -> proerty,可以自动将依赖的版本号设置为属性。
5.2.properties 加载 Maven 属性
如果需要,可以将 properties 文件中的属性用 Maven 管理和加载。
比如在 mvc-demo-parent 模块中添加自定义属性:
<properties>
<jdbc.username>root</jdbc.username>
<jdbc.password>mysql</jdbc.password>
<jdbc.driver>com.mysql.cj.jdbc.Driver</jdbc.driver>
<jdbc.url>jdbc:mysql:///mybatis?useSSL=false</jdbc.url>
</properties>
在子模块 mvc-demo 的 properties 文件中使用这些属性:
jdbc.username=${jdbc.username}
jdbc.password=${jdbc.password}
jdbc.driver=${jdbc.driver}
jdbc.url=${jdbc.url}
仅仅这样做还不够,还需要在 mvc-demo-parent 模块配置中告诉 Maven 相关插件,在处理 properties 文件所在目录时,使用 Maven 属性:
<build>
<resources>
<resource>
<directory>../mvc-demo/src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
filtering
标签为true
表示开启 Maven 的过滤器,用于解析指定目录下的资源文件,如果其中有${...}
,就用 Maven 属性进行替换。
现在 Idea 的 mvc-demo-parent 模块下会出现一个 resources 选项,它下面就是被 Maven 管理的目录:
可以对父模块 mvc-demo-parent 进行安装,并在生成的 mvc-demo 模块的 war 包中查看 properties 文件内容是否使用了 Maven 属性。
5.2.1.web.xml
实际上这里编译会出错,提示找不到 WEB-INF/web.xml 文件。我们的 mvc-demo 模块通过相关配置类设置 Servlet 配置,所以并不需要 web.xml 文件,但 Maven 生成 war 包时会强制检查是否存在这个文件,不存在就报错。
解决方式有两种,简单的方式为添加一个空的 web.xml 文件以欺骗 Maven 的相关插件。复杂方式为修改相关 Maven 插件的设置。
在 mvc-demo 的 POM 配置中添加:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
这里的作为插件属性的failOnMissingWebXml
标签设置为false
,Maven 生成 war 包时就不会再强制检查 web.xml 文件是否存在了。
现在对父模块执行安装,就能获得 mvc-demo 模块的 war 包,在该 war 包的/classes
目录下可以找到jdbc.properties
文件,可以看到该文件正常从 Maven 属性加载了值:
jdbc.username=root
jdbc.password=mysql
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql:///mybatis?useSSL=false
5.2.2.project.basedir
在现在的示例中,只有 mvc-demo 的 resources 目录下的文件可以使用 Maven 属性,因为:
<directory>../mvc-demo/src/main/resources</directory>
如果要让所有的子模块的 resources 目录都可以使用 Maven 属性,需要使用 Maven 的内置属性:
<directory>${project.basedir}/src/main/resources</directory>
内置属性project.basedir
表示当前项目的根目录,当 Maven 插件编译 maven-demo 模块时,project.basedir
的值就是 maven-demo 的根目录。
5.3.其它属性
除了自定义属性,Maven 中可以使用多种属性:
可以使用mvn help:system
命令查看 Maven 可以使用的 Java 系统属性和环境变量属性:
=============================================================================== System Properties =============================================================================== java.runtime.name=OpenJDK Runtime Environment sun.boot.library.path=D:\software\coding\eclipse_jdk1.8\jre\bin java.vm.version=25.332-b09 java.vm.vendor=Temurin maven.multiModuleProjectDirectory=C:\Users\70748\AppData\Local\Microsoft\WindowsApps java.vendor.url=https://adoptium.net/ path.separator=; guice.disable.misplaced.annotation.check=true java.vm.name=OpenJDK 64-Bit Server VM ... =============================================================================== Environment Variables =============================================================================== GATEWAY_VM_OPTIONS=D:\software\coding\IntelliJ IDEA 2021.3.2\ja-netfilter-all\vmoptions\gateway.vmoptions DATASPELL_VM_OPTIONS=D:\software\coding\IntelliJ IDEA 2021.3.2\ja-netfilter-all\vmoptions\dataspell.vmoptions COMSPEC=C:\WINDOWS\system32\cmd.exe NUMBER_OF_PROCESSORS=16 SYSTEMROOT=C:\WINDOWS
可以像自定义变量那样使用,比如${java.vm.version}
。
5.4.版本管理
在我们jar包的版本定义中,有两个工程版本用的比较多:
-
SNAPSHOT(快照版本)
-
项目开发过程中临时输出的版本,称为快照版本
-
快照版本会随着开发的进展不断更新
-
-
RELEASE(发布版本)
-
项目开发到一定阶段里程碑后,向团队外部发布较为稳定的版本,这种版本所对应的构件文件是稳定的
-
即便进行功能的后续开发,也不会改变当前发布版本内容,这种版本称为发布版本
-
除了上面的工程版本,我们还经常能看到一些发布版本:
-
alpha版:内测版,bug多不稳定内部版本不断添加新功能
-
beta版:公测版,不稳定(比alpha稳定些),bug相对较多不断添加新功能
-
纯数字版
The End,谢谢阅读。
本文的完整示例可以从获取。
文章评论