前言
为什么要学习 Maven ?
没有 Maven 之前的开发环境:
<1> 会出现大量的重复文件,大量的 jar 包,导致项目体积增大,团队协作效率降低。
<2> 环境、版本冲突问题,比如 Windows 环境下能跑的项目在 Linux 就跑不起来。
<3> idea 虽然是自动保存自动编译的。但是打包、部署等操作还是需要我们自己完成的。自从引入 Maven 后,以上的问题差不多就已经解决了,那么 Maven 的作用是什么呢?
项目构建:提供标准的、跨平台的自动化项目构建方式 依赖管理:方便快捷的管理项目依赖的资源(iar包),避免资源间的版本冲突问题 统一开发结构:提供标准的、统一的项目结构
前期回顾:
目录
Maven 目录结构
Maven 的骨架
我们使用 idea 创建的 Maven项目,它有固定的骨架结构如下:
maven-project
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ └── resources
│ └── test
│ ├── java
│ └── resources
└── target
maven-project 是项目名称,其他的骨架的作用如下
骨架名称 | 作用 |
---|---|
pom.xml | 项目描述文件 |
src/main/java | 存放Java源码的目录 |
src/main/resources | 存放资源文件的目录 |
src/test/resources | 存放测试资源的目录 |
target | 所有编译、打包生成的文件都放在这里 |
pom.xml 文件
建立 Maven 项目的初始 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.thz</groupId>
<artifactId>maven-project</artifactId>
<version>1.0-SNAPSHOT</version>
...
<properties>
<maven.compiler.source>22</maven.compiler.source>
<maven.compiler.target>22</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
pom.xml 的三个组成部分
pom.xml 声明
<?xml version="1.0" encoding="UTF-8"?>
pom.xml 文档类型定义
<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">
pom.xml 正文
除了以上两个部分外剩下的都属于正文部分
坐标三要素:
groupId:组ID,一般使用公司名倒置 |
artifactID:项目ID,一般就是项目名称 |
version:版本号 |
Maven 坐标的作用
使用唯一标识,唯一性定位资源位置,通过该标识可以将资源的识别与下载工作交由机器完成 。
项目的属性设置:
maven.compiler.source:源代码的 JDK 版本 |
maven.compiler.target:测试代码的 JDK 版本 |
project.build.sourceEncoding:源代码的编码方式 |
Maven 工程构建
项目构建是指将源代码、依赖库和资源文件等转换成可执行或可部署的应用程序的过程,在这个过程中包括编译源代码、链接依赖库、打包和部署等多个步骤。
我们可以在 idea 中找到 Maven 的 Lifecycle,这是 Maven 命令的可视化按钮:
Maven 命令的作用
命令 | 描述 |
---|---|
compile | 编译项目,生成 target 文件 |
package | 打包项目,生成 jar 文件 |
clean | 清理编译或打包后的项目结构 |
install | 打包后上传到 maven 本地仓库 |
deploy | 只打包,上传到 maven 私服 |
site | 生成站点(报告) |
test | 执行测试源(测试) |
Maven 工程测试
我们可以写一些代码来测试一下:
Demo:
public class Demo {
public String Func(String name){
System.out.println("Hello "+name);
return "Hello "+name;
}
}
DemoTest:
public class DemoTest {
@Test
public void Test(){
Demo demo = new Demo();
String ret = demo.Func("Maven");
Assert.assertEquals(ret, "Hello Maven");
}
}
pom.xml:
导入以下测试需要的依赖
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
此时我们只需点击 Maven 自带的按钮程序即可运行:
test:
此时我们可以看见所有运行编译的文件都放在 target 中
package:
此时我们就可以看到打成 jar 的文件
但是注意此时这个 jar 包实际只有 3KB,它打包的只是项目的源文件,但是我们的依赖并没有被打包进 jar 包
通过执行一些指令,我们发现执行后面的指令时,会把前面的指令都执行一边。比如执行编译指令,首先会执行清理将之前的 target 文件给清理掉。
插入仓库的概念:
Maven 仓库
仓库的概念:用于存储资源,包含各种 jar 包
仓库分类 作用 远程仓库 非本机电脑上的仓库,为本地仓库提供资源 本地仓库 自己电脑上存储资源的仓库,连接远程仓库获取资源 中央仓库 Maven 团队维护,存储所有资源的仓库 私服 部门、公司范围内存储资源的仓库,从中央仓库获取资源
私服的作用:
保存具有版权的资源,包含购买或自主研发的jar 中央仓库中的iar都是开源的,不能存储具有版权的资源 一定范围内共享资源,仅对内部开放,不对外共享
举个例子,当导入一个依赖出现爆红的情况,说明我们本地仓库中没有这个依赖的 jar 包;当我们刷新 Maven 会发现右下角有一个进度条,这说明从 Maven 中央仓库下载依赖。当然你也可以自己从官网中下载需要依赖包:点击进入 Maven 中央仓库
注意 install 是比较常用的一个指令,当你写完项目代码都应该点一下,这样团队成员访问你的代码都是最新的。例如,你的项目源代码改了,但是你的本地仓库的代码没有改,这个时候团队的其他成员就访问不到你改好后的代码。
工程构建的好处:
项目构建是软件开发过程中至关重要的一部分,它能够大大提高软件开发效率,使得开发人员能够更加专注于应用程序的开发和维护,而不必关心应用程序的构建细节。
同时,项目构建还能够将多个开发人员的代码汇合到一起,并能够自动化项目的构建和部署,大大降低了项目的出错风险和提高开发效率。
依赖与依赖程度
依赖程度
隐藏的 scope 标签
我们之前添加依赖,都是直接添加坐标三要素即可,但是它还有一个隐藏的标签 - scope,决定这个依赖的依赖程度。
<dependencies>
<dependency>
<groupId>项目组织</groupId>
<artifactId>项目名称</artifactId>
<version>项目版本</version>
<scope>依赖的程度</scope>
</dependency>
</dependencies>
scope 常用的取值有四个:
取值 | 作用 |
---|---|
compile | 如果不设置 scope,默认值就是 compile 表示源代码环境需要,测试环境也需要,并且打包的时候包含 |
provided | 源代码环境需要,测试环境也需要,但是打包的时候不包含 |
test | 源代码环境不需要,打包的时候不包含,测试环境需要 |
runtime | 运行时需要,编译时不需要(源代码环境不需要,测试环境不需要),并且打包的时候包含 |
compile 就不在过多赘述了,比较好理解。我们来看看其他取值的含义:
provided (源代码环境需要,测试环境也需要,但是打包的时候不包含)
servlet-api 源代码与测试环境都需要,但是我们的 Tomcat 中已经存在,打包的时候就可以不需要了。
test (源代码环境不需要,打包的时候不包含,测试环境需要)
这里举个例子,比如,你的项目都已经测试好了,项目准备部署,我还需要测试代码做什么呢?测试只是检验源代码的正确性,而不涉及项目的部署,所以打包的时候不需要。
runtime (运行时需要,编译时不需要,并且打包的时候包含)
这里举个例子,比如,你可能在编译的时候只需要 API、JAR,而只有在运行的时候才需要 JDBC 驱动实现。
依赖版本统一提取和维护
先来说一下之前写法的弊端:
...
<groupId>com.thz</groupId>
<artifactId>maven-project</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
...
</dependencies>
...
例如:项目的依赖一多,假设某天需要修改一个依赖的版本号,我是不是还需要去那么多依赖中去找,这就显得很麻烦。有没有更直接的方法呢?可以将 version 这行提取出来,统一维护。
...
<groupId>com.thz</groupId>
<artifactId>maven-project01</artifactId>
<version>${project01.version}</version>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
...
</dependencies>
<!--声明版本-->
<properties>
<maven.compiler.source>22</maven.compiler.source>
<maven.compiler.target>22</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!--命名随便,内部制定版本号即可-->
<project01.version>1.0</project01.version>
<junit.version>4.12</junit.version>
<mysql.version>8.0.33</mysql.version>
...
</properties>
例如:<project01.version>1.0</project01.version> 相当与取别名,project01.version 对应的就是 1.0 这个版本号。而 ${project01.version} 的 ${} 操作就是提取这个别名的内容。
我们将依赖的版本号统一放在 properties 中维护,这样就可以很明确的知道什么依赖用了什么版本;当需要修改依赖的版本号时就可以在此修改,相对应的 ${...} 的值也会修改。
依赖传递和依赖冲突
依赖传递
像以上这种情况,B依赖于A,C依赖于B;那么C是否依赖于A呢?答案是肯定的。之前我们提到有个隐藏标签 scope ,没有明确的写,那么就是默认值 compile。三个环境都需要依赖A,那么C在打包的时候还是需要将A一起打包的。所以说,C依赖于A。
将 B 的 scope 标签换成 provided (源代码环境需要,测试环境也需要,但是打包的时候不包含)。
这个时候C是否依赖于A呢?因为B是provided标签修饰的,打包时不包含,也就是说C是看不见 A 的。所以此时 C 并不依赖于 A。
依赖冲突
举个例子:
项目B依赖于mysql5.7,项目A依赖于mysql8.0;此时C同时依赖于A、B。那么项目 A、B 必然会因为 mysql 的版本号冲突。那么怎么解决冲突呢?其实很简单就是排除掉一个。
<!--使用exclusions标签配置依赖的排除-->
<exclusions>
<!--在exclusion标签中配置一个具体的排除-->
<exclusion>
<!--指定要排除的依赖的坐标(不需要写version)-->
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</exclusion>
</exclusions>
为了更好的展示,这里新建了两个子模块
untitled1:
...
<artifactId>untitled1</artifactId>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
</dependencies>
...
untitled2:
...
<artifactId>untitled2</artifactId>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>
</dependencies>
...
untitled:
...
<modules>
<module>untitled1</module>
<module>untitled2</module>
</modules>
<dependencies>
<dependency>
<groupId>com.thz</groupId>
<artifactId>untitled2</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>com.thz</groupId>
<artifactId>untitled1</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
...
此时,我在 untitled 中导入了 untitled1 和 untitled2 依赖;但是会因为 untitled1 和 untitled2 中的数据库版本问题而冲突。
可以观察到 idea 帮我们检查到了冲突并且还做了版本屏蔽;但是 idea 不是万能的有些冲突问题还需要自己手动解决。以上屏蔽了 mysql8.0 的版本,那么我想要屏蔽 mysql5.0 的版本该怎么办呢?- 可以使用 exclusion 排除标签。
...
<dependencies>
<dependency>
<groupId>com.thz</groupId>
<artifactId>untitled2</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
...
此时使用的就是 mysql8.0 的版本。依赖冲突问题最好在导入依赖的时候先想会发生什么冲突,如果等到发生了冲突再来排查,这样排查的时间成本会很高,因为一个项目导入的依赖是非常多的。
父工程与子工程
maven 工程的继承
利用 Maven 可以对项目进行分模块开发。那么怎样把各个模块整合到一起呢?
这就利用了 Maven 继承的特性。一般是每个模块都继承一个父工程。
子类:声明父项目的地址
<!--定位父项目位置-->
<parent>
<groupId>com.thz</groupId>
<artifactId>maven-project01</artifactId>
<version>1.0</version>
</parent>
<!--子项目的名称-->
<artifactId>untitled1</artifactId>
我们在子工程中通过 parent 标签定位父工程的位置,这样的好处就是可以把一些共性放在父类里面。然后我们有多个子工程,这些子工程就没有必要去设置这个共性。因为它默认情况会继承。
父工程统一依赖管理
父项目声明依赖:
- 使用 dependencyManagement 标签配置对依赖的管理,被管理的依赖并没有真正被引入到工程,只有在子项目调用了才会被引入工程。
<!-- 使用 dependencyManagement 标签配置对依赖的管理 -->
<!-- 被管理的依赖并没有真正被引入到工程 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>6.0.10</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>6.0.10</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.10</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>6.0.10</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.0.10</version>
</dependency>
</dependencies>
</dependencyManagement>
子项目继承依赖:
- 子工程引用父工程中的依赖信息时,可以把版本号去掉,因为使用的是父工程默认的版本号。
<!-- 子工程引用父工程中的依赖信息时,可以把版本号去掉。 -->
<!-- 把版本号去掉就表示子工程中这个依赖的版本由父工程决定。 -->
<!-- 具体来说是由父工程的 dependencyManagement 来决定。 -->
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
</dependency>
</dependencies>
maven 工程的聚合
父类:声明子项目的模块
<modules>
<module>untitled1</module>
<module>untitled2</module>
</modules>
Maven 项目中的<modules>元素,它指定了当前项目的子模块列表。在这个例子中,项目有两个子模块,分别是untitled1和untitled2。这意味着这个项目是一个聚合项目,它管理着多个子模块的构建和依赖关系。当你运行 Maven 命令时,Maven会自动构建所有的子模块。