Maven
了解Maven
Maven(Apache Maven)是一个用于项目管理和构建的开源工具。
Maven 的作用:
- 项目构建:Maven 可以自动执行项目的构建过程,包括编译源代码、运行单元测试、打包生成可部署的产物等。
- 依赖管理: Maven 负责管理项目的依赖关系,通过在项目的 POM 文件中声明依赖,Maven 可以自动下载并配置所需的库或框架。这简化了项目的配置和部署过程,并确保了项目的依赖关系是一致的。
- 多模块项目支持 : Maven 支持多模块项目,允许将一个大型项目划分为多个模块,每个模块可以独立构建和测试。这种模块化的结构有助于提高项目的可维护性和可扩展性。
- 插件体系结构: Maven 的插件体系结构允许开发人员编写自定义的插件,以满足特定项目的需求。
- 仓库管理: Maven 使用本地仓库和远程仓库来管理项目的构建产物和依赖库。本地仓库用于存储本机构建的产物,而远程仓库用于获取和发布构建产物。
其他的比如版本管理、项目报告等一般用的不多,版本管理一般都会选择Git。
苹果(macOS)安装Maven 比较简单,使用brew 安装管理就好。
brew install maven
一、依赖
1.1 依赖管理
Maven 使用 pom.xml 文件进行依赖配置和管理
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.example</groupId>
<artifactId>my-project</artifactId>
<version>1.0.0</version>
<!-- 项目依赖 -->
<dependencies>
<!-- 例:JUnit 依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
1.2.1 Maven 坐标
Maven 坐标(Coordinates)是用于唯一标识 Maven 项目的元数据,它包含三个关键信息:groupId、artifactId 和 version。
- groupId(组织或包名):代表项目所属的组织或包名,用于组织 Maven 仓库中的项目。
命名规则通常是反转的域名(或者组织的标识符),例如 com.example。
建议使用全球唯一的标识符,以避免命名冲突。 - artifactId(项目名称):代表项目的名称,用于唯一标识 Maven 仓库中的构建产物(例如 JAR 文件)。
命名规则通常是项目的简短名称,例如 my-project。 - version(版本号):代表项目的版本号,用于标识项目构建的不同版本。
命名规则通常是 X.Y.Z 形式,例如 1.0.0。其中,X 是主版本号,Y 是次版本号,Z 是修订版本号。
备注: 个人建议 groupId 的命名规则是 反转的域名+项目名称
, artifactId 的命名规则是 公司名称 + 项目名称
。
举个例子:
<dependency>
<groupId>com.olym.jdbc</groupId>
<artifactId>olym-jdbc</artifactId>
<version>1.8.0</version>
</dependency>
打包后的依赖名称是 olym-jdbc-1.8.0.jar,更加清晰。
1.2.2 Maven 依赖的作用域 (Scope)
Maven 依赖的作用域(Scope)定义了依赖在不同阶段的生命周期中的可见性和有效性。不同的作用域用于不同的情景,以满足项目的需求。以下是 Maven 中常见的依赖作用域:
作用域 | 描述 | 用途 |
---|---|---|
compile | 默认作用域,适用于所有阶段。 | 依赖在编译、测试、运行时都可见。 |
provided | 适用于编译和测试阶段,但不会包含在最终打包的产物中。 | 依赖在运行时由容器或环境提供。 |
runtime | 适用于运行时和测试阶段,但不参与编译。 | 依赖在运行时可见,但在编译时不需要。 |
test | 仅适用于测试阶段,不参与编译和运行时。 | 依赖仅在测试代码中可见。 |
system | 与 provided 类似,需要显式提供路径指定本地 JAR 文件的位置。 | 不推荐使用,容易导致构建不可移植性。 |
import | 仅适用于 <dependencyManagement> 部分,用于管理依赖的版本。 | 不参与实际的构建,仅用于管理版本。 |
重点提示:
system
作用域的要格外注意,system
作用域的依赖在打包(mvn package)时不会包含在项目的构建产物中。这是因为 system 作用域通常用于引入本地系统中的 JAR 文件,而不是从 Maven 仓库中解析。
举个例子:
<dependency>
<groupId>com.olym.jdbc</groupId>
<artifactId>olym-jdbc</artifactId>
<version>1.8.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/libs/olym-jdbc-1.8.0-GA-pg.jar</systemPath>
</dependency>
这个依赖是 system
,在IDEA运行时没有任何异常,但是当打包后,运行时就会抛出 ClassNotFoundException
,查看程序包时,里面没有包含该依赖。
如何解决 system 打包问题?
- 安装依赖到本地库 (不太推荐)
mvn install:install-file \
-Dfile=path/to/your/artifact.jar \
-DgroupId=your.groupId \
-DartifactId=your-artifactId \
-Dversion=1.0 \
-Dpackaging=jar
- -Dfile: 指定要安装的 JAR 文件的路径。
- -DgroupId: 指定项目的 Group ID。
- -DartifactId: 指定项目的 Artifact ID。
- -Dversion: 指定项目的版本号。
- -Dpackaging: 指定 JAR 文件的打包类型,这里是 jar。
- 将依赖放到私库进行管理 (推荐) 使用Nexus搭建个人仓库
1.2.3 传递性依赖 (概念性很强)
传递性依赖指的是项目依赖关系中的间接依赖。当项目 A 依赖于项目 B,而项目 B 依赖于项目 C,那么项目 A 与项目 C 之间就存在一种传递性依赖关系。
Maven 在处理传递性依赖时会自动解析和管理这些依赖关系,确保项目构建时所需的所有依赖都被正确地引入。当你在项目 A 中运行 Maven 构建时,Maven 会自动下载项目 B 和项目 C 的相关 JAR 文件,并将它们添加到项目 A 的类路径中。
1.2.4 依赖调解
因为 传递性依赖
的存在,大多数情况下,开发者不需要关心依赖,但可能存在问题,传递性依赖可能存在冲突,也就是说Maven没有选对传递性依赖,造成依赖冲突,某些功能无法使用。
Maven 依赖调解的规则:
- 第一原则: 路径最近者优先。 比如 A -> B -> C -> X(1.0)、 A -> D -> X(2.0),那么会使用X(2.0)。
- 第二原则: 如果路径相同,那么在 pom.xml 文件中依赖顺序决定了谁被使用。
可以通过排除依赖,解决依赖冲突,例如:
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>projectB</artifactId>
<version>1.0</version>
<!-- 排除传递性依赖项 org.unwanted:unwanted-artifact -->
<exclusions>
<exclusion>
<groupId>org.unwanted</groupId>
<artifactId>unwanted-artifact</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
备注: 不需要声明版本信息。
二、项目构建和插件
Maven 的核心仅仅定义了抽象的生命周期,具体的任务是交由插件完成的,插件以独立的构建形式存在。
Maven插件官网链接
2.1 生命周期
Maven 生命周期定义了一个项目的构建过程,并将它划分为不同的阶段(phase)。 简单的一个例子,命令mvn clean
就是一个生命周期。
-
Clean Lifecycle(清理生命周期):
- clean: 清理项目,删除
target
目录及其内容。
- clean: 清理项目,删除
-
Default Lifecycle(默认生命周期):
- validate: 验证项目是否正确且所有必要的信息都可用。
- compile: 编译项目的源代码。
- test: 使用适当的测试框架运行测试。这些测试不要求代码被打包或部署。
- package: 接受编译的代码,并将其打包成可分发的格式,如 JAR。
- verify: 运行所有检查,验证包是否有效且达到质量标准。
- install: 将包安装到本地存储库,以供其他项目本地使用。
- deploy: 将最终的包复制到远程存储库,以便共享和部署。
-
Site Lifecycle(站点生命周期):
- pre-site: 在生成站点文档之前执行的工作。
- site: 生成项目的站点文档。
- post-site: 在生成站点文档之后执行的工作,并且为部署准备。
- site-deploy: 将生成的站点文档部署到服务器上。
2.2 构建和插件
Maven 中大概使用了类似 Facade 模式,举个例子 mvn clean
运行可以理解为一个接口,实际是使用插件实现的。
生命周期和插件的关系,如下:
阶段 | 插件目标 | 执行任务 |
---|---|---|
validate | - | 验证项目是否正确且所有必要的信息可用 |
initialize | - | 初始化构建状态 |
generate-sources | - | 生成源代码 |
process-sources | - | 处理源代码,如过滤资源文件 |
generate-resources | - | 生成资源文件 |
process-resources | maven-resources-plugin:resources | 处理资源文件,如拷贝资源到输出目录 |
compile | maven-compiler-plugin:compile | 编译项目的源代码 |
process-classes | - | 处理编译后的类文件 |
generate-test-sources | - | 生成测试源代码 |
process-test-sources | - | 处理测试源代码 |
generate-test-resources | - | 生成测试资源文件 |
process-test-resources | maven-resources-plugin:testResources | 处理测试资源文件 |
test-compile | maven-compiler-plugin:testCompile | 编译测试源代码 |
process-test-classes | - | 处理编译后的测试类文件 |
test | maven-surefire-plugin:test | 运行单元测试 |
prepare-package | - | 准备打包阶段 |
package | maven-jar-plugin:jar | 将编译后的代码打包成可分发的格式 |
pre-integration-test | - | 在集成测试之前执行 |
integration-test | maven-surefire-plugin:integration-test | 运行集成测试 |
post-integration-test | - | 在集成测试之后执行 |
verify | - | 对集成测试的结果进行验证 |
install | maven-install-plugin:install | 将构建的项目副本安装到本地仓库 |
deploy | maven-deploy-plugin:deploy | 将构建的项目副本部署到远程仓库 |
2.2.1 实战-构建时依赖单独打包
如果默认
<build>
,执行mvn clean package -DskipTests
会将程序代码、依赖、配置文件同时打包进入到一个服务jar包内,依赖jar包基本上占用的空间会非常大,对于部署运行影响还是很大的,所以将依赖和代码分离,是很好的一个主意。
主要用到了 maven-dependency-plugin
插件和 maven-jar-plugin
插件,配置如下:
<build>
<!-- 打包时将依赖 jar 放到 lib目录下-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.2.0</version> <!-- 替换为你需要的版本号 -->
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<!-- 指定依赖项的输出目录 -->
<outputDirectory>${project.build.directory}/lib</outputDirectory>
<!-- 排除不需要的依赖项 -->
<!-- <excludeArtifactIds>unwanted-artifact</excludeArtifactIds>-->
</configuration>
</execution>
</executions>
</plugin>
<!-- 打包时,应用jar包内不包括依赖jar-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<!-- 类路径信息 -->
<classpathPrefix>lib/</classpathPrefix>
<mainClass>com.example.fancoding.web.FancodingWebApplication</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
上面的配置会有两个效果:
- 构建时,依赖jar包会统一放到
/lib
目录下 - 构建时,服务jar包中会有
MANIFEST.MF
文件,文件中设置类路径信息以及主类。
文件目录参考如下:
MANIFEST.MF
文件参考:
备注: 除了首次部署或依赖变更时,需要将/lib
上传到服务器,其他时候就可以单独部署服务jar包。如果使用上面的例子,那么lib
目录需要和服务jar包在同一目录,启动也不会有任何影响 java -jar fancoding.jar
。
扩展一下:
之前有个安全需求,由第三方提供JDBC替换换原来的驱动,在不需要修改代码的前提下,那么我直接上传依赖jar包到lib
目录,修改MANIFEST.MF
增加依赖就可以了,不需要重新打包,是不是很方便。
2.2.1 实战-构建时根据不同的环境配置文件进行打包
一般的服务都会有开发、测试、灰度和生产等环境,打包时需要不同的配置文件,如果使用
spring.profiles.active
去控制,那么每次打包前都需要修改配置;另一种方式将配置文件在服务器上部署/config
也是不错的主意。但是使用Maven 可以更好构建。
配置文件参考如下:
<!-- 多配置文件管理-->
<profiles>
<profile>
<id>dev</id>
<properties>
<environment>dev</environment>
</properties>
<activation>
<activeByDefault>true</activeByDefault> <!-- 默认激活的Profile -->
</activation>
</profile>
<profile>
<id>prod</id>
<properties>
<environment>prod</environment>
</properties>
</profile>
</profiles>
<build>
<!-- 定义打包的名称-->
<finalName>fancoding</finalName>
<!-- 定义打包的配置文件引入-->
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources-env/${environment}</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
构建时,执行 mvn clean install -P prod
- -P: 你配置的环境id
三、仓库 (了解即可)
如果需要搭建自己搭建私有仓库和使用,可以参考 使用Nexus搭建个人仓库
Maven 仓库是存储 Maven 构建项目所需依赖项(JAR 文件、插件、父项目等)的地方。它是一个目录结构,按照 Maven 的约定,有两种主要类型的仓库:本地仓库和远程仓库。
-
本地仓库(Local Repository):
本地仓库是位于开发者本地机器上的一个目录,用于存储下载的依赖项。Maven 在首次构建项目时,会从远程仓库下载所需的依赖项,并将它们缓存到本地仓库中。之后的构建过程将重复使用本地仓库中的缓存,避免重复下载相同的依赖项。
默认情况下,本地仓库位于用户的家目录下的
.m2/repository
目录(Windows 下为C:\Users\YourUsername\.m2\repository
,Linux 下为/home/YourUsername/.m2/repository
)。 -
远程仓库(Remote Repository):
远程仓库是位于网络上的一个仓库,用于存储 Maven 中央仓库或其他公共或私有仓库的依赖项。当 Maven 构建项目时,它会从远程仓库下载缺失的依赖项。Maven 中央仓库是 Maven 默认的远程仓库,包含了大量的开源项目的依赖项。
除了 Maven 中央仓库,还可以配置其他远程仓库,例如企业内部的私有仓库或其他公共的 Maven 仓库。这样,项目可以从这些远程仓库获取依赖项。
仓库的分类可以按照其可访问性和管理者的不同来进行划分:
-
本地仓库(Local Repository): 存储在本地开发者机器上的仓库。
-
远程仓库(Remote Repository):
- 中央仓库(Central Repository): Maven 默认的远程仓库,包含了大量的开源项目的依赖项。
- 其他公共仓库: 包括其他开源仓库,可以通过 Maven 的配置文件指定。
- 私有仓库(Private Repository): 企业内部搭建的私有仓库,用于存储自己的项目和依赖项。
四、多模块项目支持
虽然现在好像微服务十分流行,但是一般的中小公司或者项目规模级别并不大的前提下,项目模块化更常见,毕竟成本更低。
在 Maven 中,模块化通常指的是将一个大型项目拆分成多个独立的、可重用的子模块,每个子模块有自己的 pom.xml 文件和项目结构。
project-root/
├── pom.xml (父项目的 pom 文件)
├── module1/
│ └── pom.xml (子模块1的 pom 文件)
├── module2/
│ └── pom.xml (子模块2的 pom 文件)
└── ...
4.1 模块化管理
在 Maven 中,模块化是指将一个大型项目拆分成多个独立的、可重用的子模块,每个子模块有自己的 pom.xml 文件和项目结构。Maven 提供了一种机制来管理多模块项目,允许你将这些子模块组织在一起,以便更好地管理和构建项目。
4.1.1 父项目
一般的父项目没有
/src
目录,并且一般只有/doc
、和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>
<!-- 表示它是一个聚合项目 -->
<packaging>pom</packaging>
<!-- 子模块 -->
<modules>
<module>fancoding-web</module>
</modules>
<groupId>com.example</groupId>
<artifactId>fancodig</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>2.6.13</spring-boot.version>
<knife4j.version>4.0.0</knife4j.version>
</properties>
<!-- 集中管理项目中所有模块的依赖版本-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
- dependencyManagement :用于集中管理项目的依赖版本号。当你在 Maven 项目中有多个模块时,dependencyManagement 元素可以帮助你在一个父 POM 文件中集中定义和管理依赖的版本,而子模块只需引用这些依赖而无需指定版本号。
4.1.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>com.example</groupId>
<artifactId>fancodig</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<!-- 子项目的坐标 -->
<groupId>com.example.fancoding</groupId>
<artifactId>web</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>fancoding-web</name>
<description>fancoding-web</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
Maven的生命周期如下:
4.1.3 实战
网上做的比较好的开源项目,模块化一把都会拆分成
web
、service
、core
和api
,可能还会有ui
等。我个人是比较喜欢这种拆分方式的。
fancoding/
├── pom.xml (父项目的 pom 文件)
├── fancoding-web/
│ └── pom.xml (子模块1的 pom 文件)
├── fancoding-service/
│ └── pom.xml (子模块2的 pom 文件)
└── fancoding-core/
│ └── pom.xml (子模块3的 pom 文件)
└── fancoding-api/
│ └── pom.xml (子模块4的 pom 文件)
- web/:Controller层代码,过滤器(Filter)、拦截器(Interceptor)、监听器(Listener)等相关的代码我也会放到这里,比如 接口请求日志过滤。
- service/ :后端业务逻辑的核心代码,一般是开放服务接口给 web 使用,避免 web 层直接使用 dao层。
- core/:一些常用的配置信息、常量、工具类等代码。
- api/ : 一般放些VO、DTO、DO等代码。
可能刚入门时候,比较熟悉的名词是 MVC。
- MVC(Model-View-Controller)是一种经典的软件设计模式,用于组织和分离应用程序的各个组件。
后面可能有 分布式
、微服务
、中台
等各种高大上的名词。
其实本质还是一样的,使得代码更易于维护、扩展和测试
。
比如我工作过的项目,一个系统拆分成了 门户系统、沙盒系统、数据资产等多个系统和模块(很乱),前任选择了将一个系统拆分成了所谓的微服务,独立运行,但是启动门户系统时,其他服务没有启动就会报错,这。。。就很尴尬了。
五、常用的Maven指令
mvn clean package -DskipTests
: 跳过测试,清理打包mvn spring-boot:run
: 在 Spring Boot 项目中运行应用程序。mvn dependency:tree
: 打印项目的依赖关系树,显示所有直接和传递性依赖。mvn dependency:list
: 用于列出项目的所有直接依赖项及其版本信息。这个命令可以帮助开发者快速查看项目中使用的依赖项,以及它们的版本。mvn dependency:analyze
: 用于分析项目的依赖关系,检测潜在的问题,如过期的依赖项、未使用的依赖项等。mvn clean source:jar -Dmaven.compiler.skip=true
:打包源码(不进行编译)