【JavaWeb】----- Maven入门与实践

1. Maven 概述

1.1 Maven 简介

Apache Maven 是一个项目管理和构建工具,它基于项目对象模型(Project Object Model, POM) 的概念,通过一个名为 pom.xml 的配置文件来描述项目的结构、依赖关系和构建流程。Maven 的核心目标是简化 Java 项目的构建过程,提高开发效率。

Apache 软件基金会成立于1999年7月,是目前世界上最大的最受欢迎的开源软件基金会,也是一个专门为支持开源项目而生的非盈利性组织。

其官网提供了大量的开源项目列表,包括 Maven 在内。

更多开源项目可以访问:https://www.apache.org/index.html#projects-list

1.2 Maven 作用:

Maven 在 Java 开发中扮演着至关重要的角色,主要体现在以下几个方面:

1.2.1 依赖管理(Dependency Management)

在传统的 Java 项目中,开发者需要手动下载并添加所需的 JAR 包到项目的 lib 目录下,这不仅繁琐而且容易出错。Maven 通过在 pom.xml 文件中声明依赖项,能够自动从中央仓库或其他配置的远程仓库下载所需的依赖包,并将其集成到项目中,极大地提高了开发效率和准确性。

例如,要引入 commons-io 库,只需在 pom.xml 中添加如下配置:

xml 复制代码
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>

1.2.2 项目构建(Project Building)

Maven 提供了一套标准化的跨平台(Linux、Windows、MacOS)自动化项目构建方式。它将编译、测试、打包、发布等一系列操作进行了标准化处理,使得不同环境下的项目构建过程更加一致和可靠。

  • 编译 (compile) : 将 .java 文件编译成 .class 文件。
  • 测试 (test): 执行单元测试,确保代码质量。
  • 打包 (package): 将编译后的类文件和资源文件打包成 JAR、WAR 等格式。
  • 发布 (deploy): 将打包好的文件部署到指定的服务器或仓库。

这些步骤可以通过简单的命令行指令完成,如 mvn compile, mvn test, mvn package, mvn deploy

1.2.3 统一项目结构(Unified Project Structure)

为了保证团队协作的一致性和项目的可维护性,Maven 定义了一种标准的项目目录结构。这种结构适用于所有基于 Maven 的项目,无论使用哪种 IDE(如 Eclipse、MyEclipse、IntelliJ IDEA),都能轻松地导入和运行项目。

典型的 Maven 项目结构如下:

复制代码
maven-project/
├── src/
│   ├── main/
│   │   ├── java/          # 主程序源码
│   │   └── resources/     # 主程序资源文件
│   └── test/
│       ├── java/          # 测试程序源码
│       └── resources/     # 测试程序资源文件
├── pom.xml                # 项目配置文件
└── target/               # 构建输出目录

通过这种方式,即使不同的团队成员使用不同的开发工具,也能保持项目结构的一致性,减少因环境差异带来的问题。

1.3 Maven 核心组件与工作原理:

  • POM (Project Object Model)

    所有 Maven 项目都必须有一个 pom.xml 文件,它是项目的核心配置文件,包含了项目的基本信息(如名称、版本)、依赖项、插件配置以及构建规则。

  • 依赖管理模型 (Dependency Management)

    Maven 通过解析 pom.xml 中的 <dependencies> 配置,从仓库中获取所需的 jar 包,并将其加入到类路径中。

  • 构建生命周期与阶段 (Build Lifecycle & Phases)

    Maven 的构建过程分为多个预定义的生命周期(如 clean, default, site),每个生命周期包含若干个阶段(phases)。每个阶段可以绑定一个或多个插件目标(plugin goals)来执行具体任务。

  • 插件 (Plugins)

    Maven 使用插件来执行具体的构建任务,例如编译代码、运行单元测试、生成文档等。大多数功能都是通过插件实现的。

  • 仓库 (Repository)

    仓库用于存储和管理各种 jar 包和其他资源。Maven 有三种类型的仓库:

    • 本地仓库(Local Repository) :位于用户计算机上的一个目录(默认路径为 ~/.m2/repository),用于缓存从远程仓库下载的依赖。
    • 中央仓库(Central Repository) :由 Apache Maven 团队维护的全球公共仓库,地址为 https://repo1.maven.org/maven2/
    • 远程仓库(Remote Repository / 私服):企业内部搭建的私有仓库,用于存储公司内部的私有 jar 包或镜像中央仓库的内容,以提高访问速度和安全性。

1.4 依赖查找顺序:

当 Maven 需要某个依赖时,会按照以下顺序进行查找:

  1. 本地仓库 → 如果存在则直接使用;
  2. 远程仓库(私服) → 若本地没有,则从私有仓库下载;
  3. 中央仓库 → 最后尝试从中央仓库下载。

注:若项目配置了多个远程仓库,Maven 会按配置顺序依次尝试。

本地仓库 与 远程仓库均没有所需依赖:

远程仓库中 存在 之前在中央仓库中下载过的所需依赖:


1.5 Maven 安装

⚠️ 注意:本节假设已安装 JDK 并配置好 JAVA_HOME 环境变量。

1.5.1 官网下载压缩包并解压

步骤说明:

  1. 打开浏览器,访问 Maven 官方网站:https://maven.apache.org/

  2. 在页面右上角点击 Downloads (如下图所示):

  3. 进入下载页面后,根据操作系统选择对应的压缩包(推荐选择最新稳定版,例如 apache-maven-3.9.11-bin.zip):

  4. 将下载好的压缩包解压至一个干净且不含其他文件的目录中,例如:

    复制代码
    D:\DeveLop\apache-maven-3.9.11

    避免解压到包含其他项目或临时文件的目录,防止路径冲突或权限问题。


1.5.2 配置本地仓库

步骤说明:

  1. 打开解压后的 Maven 目录(如 D:\DeveLop\apache-maven-3.9.11),在该目录下新建一个名为 mvn_repo 的文件夹作为本地仓库:

  2. 打开同级目录下的 conf/settings.xml 文件,编辑其中的 <localRepository> 配置项:

  3. 修改 <localRepository> 节点,将其值设置为刚刚创建的路径:

    xml 复制代码
    <localRepository>D:\DeveLop\apache-maven-3.9.11\mvn_repo</localRepository>

    注意:原文件中该行默认被注释(以 <!-- --> 包裹),需先删除注释符号再修改路径。


1.5.3 配置阿里云 Maven 私服(镜像)

为什么要配置镜像?

由于中央仓库(https://repo1.maven.org/maven2)位于国外,国内开发者访问速度较慢。使用阿里云 Maven 镜像可以显著提升下载速度。

配置方法:

  1. 访问阿里云官方 Maven 镜像页面:https://developer.aliyun.com/mirror/maven
  1. 获取最新的镜像配置代码(推荐从官网复制,确保兼容性)。

  2. conf/settings.xml 文件中找到 <mirrors> 标签,添加以下内容:

    xml 复制代码
    <mirror>
        <id>aliyunmaven</id>
        <mirrorOf>*</mirrorOf>
        <name>阿里云公共仓库</name>
        <url>https://maven.aliyun.com/repository/public</url>
    </mirror>

    说明:

    • <id>:唯一标识符,建议命名清晰;
    • <mirrorOf>*</mirrorOf>:表示所有远程仓库请求都走此镜像;
    • <url>:阿里云公共仓库地址,支持绝大多数开源项目。

    📌注意:完成配置后保存 settings.xml 文件,重启命令行生效。


1.5.4 环境变量配置

为了让系统在任意目录下都能通过命令行使用 mvn 命令,需要将 Maven 的 bin 目录添加到系统的环境变量中。以下以 Windows 操作系统 为例进行说明。

步骤说明:

  1. 打开系统属性窗口

    在桌面"此电脑"或"我的电脑"上右键,选择【属性】:

  2. 进入高级系统设置

    在左侧导航栏点击【高级系统设置】:

  3. 打开环境变量设置

    在弹出的"系统属性"窗口中,点击底部的【环境变量】按钮:

  4. 新建 MAVEN_HOME 系统变量

    在"系统变量"区域点击【新建】,创建一个名为 MAVEN_HOME 的变量,其值为 Maven 的安装根目录(例如 D:\DeveLop\apache-maven-3.9.11):



  1. 编辑 Path 变量
    在"系统变量"列表中找到 Path,选中后鼠标右键双击进行编辑:
  1. 添加 Maven 的 bin 目录到 Path
    在"编辑环境变量"窗口中,点击【新建】,然后输入 %MAVEN_HOME%\bin
  1. 确认所有窗口并保存设置

    依次点击【确定】关闭所有对话框,使环境变量生效:

  2. 验证配置是否成功

    打开新的命令提示符(CMD)窗口,执行以下命令:

    bash 复制代码
    mvn -v

    若正确显示 Maven 版本、Java 版本及本地仓库路径,则说明配置成功:


2. Maven - IDEA 集成

IntelliJ IDEA(简称 IDEA)原生支持 Maven 项目管理。为了确保 IDEA 使用的是我们本地安装并配置好的 Maven(而非内置默认版本),需要手动指定 Maven 的安装路径和 settings.xml 文件,以保证依赖下载、仓库位置和镜像策略与命令行环境一致。

2.1 配置 Maven 环境(全局)

步骤说明:

  1. 打开 IDEA 设置窗口

    启动 IntelliJ IDEA,在欢迎界面 点击左侧菜单栏的【Customize】→

    【All Settings】:

  2. 进入 Build Tools → Maven 设置项

    在左侧导航树中依次展开【Build, Execution, Deployment】→【Build Tools】→【Maven】:

  3. 指定 Maven 安装目录(Maven home path)

    在右侧面板中,将【Maven home path】设置为本地解压的 Maven 根目录(如 D:\DeveLop\apache-maven-3.9.11):

  4. 指定 User settings file

    将【User settings file】指向之前修改过的 conf/settings.xml 文件(如 D:\DeveLop\apache-maven-3.9.11\conf\settings.xml):

  5. 指定 Local repository(可选)

    【Local repository】会自动根据 settings.xml 中的 <localRepository> 值填充。若未自动识别,可手动指定为你创建的 mvn_repo 目录:

  6. 保存配置

    点击右下角【Apply】→【OK】使设置生效:

    ⚠️ 注意:此步骤不涉及 SDK 配置,仅保存 Maven 相关设置。

至此,IDEA 的全局 Maven 环境已配置完成。后续新建或导入的 Maven 项目将自动使用该配置。


2.2 配置 Project SDK

在使用 IntelliJ IDEA 开发 Maven 项目时,除了配置 Maven 环境外,还需确保项目绑定了正确的 Project SDK(即 JDK)。若未正确设置,可能导致编译失败、语法高亮异常或运行时错误。

2.2.1 配置 Project SDK

💡 提示:此操作通常在创建新项目时自动完成,但若导入已有项目或 JDK 路径变更,需手动检查或重新配置。

  1. 在左侧选择【Project】,在右侧的【Project SDK】下拉框中:
    • 若已存在所需 JDK 版本,直接选择;
    • 若无,点击【New...】→【JDK】,然后浏览到本地 JDK 安装目录
  2. 设置完成后,点击右下角【Apply】→【OK】保存配置:

2.2.2 在 IDEA 中设置 Java 字节码编译版本

尽量与jdk版本保持一致


2.3 创建 Maven 项目

IntelliJ IDEA 提供了对 Maven 项目的原生支持,可快速创建符合 Maven 标准目录结构的 Java 项目。以下演示如何从零开始创建一个简单的 Maven 项目。

步骤说明:

  1. 启动新建项目向导

    打开 IntelliJ IDEA,点击欢迎界面中的【New Project】--- 【Empty Project】

    填写项目名称 、项目存储路径 并点击创建:

  1. 配置 空项目 JDK 和 语言级别 版本

点击右侧设置按钮 选择【Project Structure】:

在这里选择SDKLanguage level ,尽量保持版本一致:

  1. 在空项目中创建第一个 Maven 项目模块如下

    注意在test 目录下新建一个 resources目录用来 存放测试相关的配置文件

这样就获得了一个完整的Maven项目目录:

各目录作用详解:

  • src/main/java

    主程序 Java 源码目录。所有业务逻辑代码应放在此处。Maven 编译时会将 .java 文件编译为 .class 并输出到 target/classes

  • src/main/resources

    主程序资源目录。用于存放非代码类资源,如配置文件、静态文件、国际化属性等。这些文件会被原样复制到 target/classes,供程序运行时加载。

  • src/test/java

    测试代码目录。通常放置 JUnit 或 TestNG 编写的单元测试类。这些代码不会被打包到最终发布产物中

  • src/test/resources

    测试资源目录。用于存放测试专用的配置或数据文件(如模拟数据库脚本),仅在执行 mvn test 时生效。

  • pom.xml

    Project Object Model(项目对象模型)文件,是 Maven 项目的"心脏"。通过它声明:

    • 项目基本信息(GroupId、ArtifactId、Version)
    • 依赖库(Dependencies)
    • 构建插件(Plugins)
    • 构建生命周期配置
  • target/

    自动生成的构建输出目录。执行 mvn compilemvn package 等命令后,编译结果、打包文件(如 .jar.war)均存放于此。该目录不应提交到版本控制系统(如 Git)

    如编译运行了这个Maven 项目之后 会生成以下target目录:


2.4 Maven 坐标详解

在 Maven 中,每一个项目或依赖库都由一组唯一的 坐标(Coordinate) 来标识。这组坐标就像"GPS 定位",确保全球范围内都能准确找到并下载对应的 JAR 包或项目。

Maven 坐标由三个核心元素组成:groupIdartifactIdversion,统称为 GAV(Group-Artifact-Version)


2.4.1 Maven 坐标三大组成部分

元素 含义 示例
groupId 组织或公司名称,通常为域名反写,用于区分不同组织的项目 com.exampleorg.springframeworkcn.hutool
artifactId 模块或项目名称,表示具体的功能模块 spring-corehutool-alldemo-maven-app
version 版本号,标识项目的发布阶段和更新状态 1.0-SNAPSHOT5.8.273.1.0.RELEASE

2.4.2 版本号分类详解

Maven 的版本号不仅表示数字,还包含生命周期语义,主要分为两类:

1. SNAPSHOT(快照版本)
  • 表示该版本尚处于开发阶段,功能不稳定,可能随时变更;
  • Maven 会定期检查远程仓库是否有更新的 SNAPSHOT 版本,并自动下载;
  • 适用于团队内部协作开发、持续集成环境。

示例:1.0-SNAPSHOT

⚠️ 提示:不建议在生产环境中使用 SNAPSHOT 版本,因其不可控性可能导致运行时异常。

2. RELEASE(正式发布版本)
  • 表示该版本已测试通过,功能稳定,适合对外发布;
  • 一旦发布,版本号不会更改,除非重新发布新版本;
  • 是生产环境推荐使用的版本类型。

示例:5.8.273.1.0.RELEASE

💡 提示:若未指定后缀,默认视为 RELEASE 版本。


2.4.3 实际应用示例

之前创建的Maven 项目中的POM.XML 文件里 就含有相关的坐标信息

坐标如下:

xml 复制代码
<dependency>
    <groupId>org.example</groupId>
    <artifactId>maven-project01</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
  • groupId: org.example :代表组织或公司名称,通常采用域名倒序的方式表示(本例中为我创建的 org.example)。
  • artifactId: maven-project01:标识具体的项目或模块名称。本例中,它是我正在开发的项目名称。
  • version: 1.0-SNAPSHOT :表示当前项目的版本号。SNAPSHOT 后缀意味着这是一个开发中的版本,可能会有后续更新。当项目完成并准备发布时,应移除 -SNAPSHOT 并指定一个稳定的版本号。

Maven 会根据此坐标从仓库中下载对应 JAR 包,并自动处理其传递性依赖(如 Tomcat、Jackson 等)。


2.5 导入 Maven 项目

在开发过程中,我们常常需要在一个主项目中同时管理多个 Maven 模块 (如 maven-project01maven-project02 等)。IDEA 提供了便捷的方式,允许开发者通过项目视图直接访问本地磁盘上的子项目文件夹,便于快速定位、编辑或切换。

📂 准备阶段:通过 IDEA 打开本地 Maven 项目目录

  1. 定位到包含多个 Maven 项目的文件夹

    在系统文件浏览器中,找到存放多个 Maven 项目的根目录(如 D:\Web-Ai-Code\web-ai-project01),其中包含若干个独立的 Maven 项目文件夹,将其进行复制:

  2. 在 IDEA 中打开资源管理器

    在 IDEA 左侧"项目"视图中,右键点击当前项目根目录(如 web-ai-project01),选择【打开于】→【资源管理器】:

  3. 在资源管理器中查看所有子项目

    资源管理器窗口自动打开至当前项目所在目录,将刚才复制的 maven-project02maven-project03 等子项目文件夹 粘贴到当前位置:

  4. 在 IDEA 项目视图中查看模块结构

    返回 IDEA,可在"项目"视图中看到这些子模块以文件夹形式展示,表示它们已被纳入当前项目结构中:

    但是刚复制过来的两个模块 并未被识别为 Maven项目


2.5.1 导入方式1:通过"项目结构"手动导入模块(不推荐)

在某些情况下,IDEA 可能无法自动识别并加载子项目的 Maven 结构(例如从外部复制的项目、或未使用 pom.xml 统一管理的模块)。此时可以使用 "项目结构" → "导入模块" 的方式手动添加模块,但该方法较为繁琐,且容易出错,仅作为备用方案

步骤说明:

  1. 打开项目结构设置

    在 IDEA 顶部菜单栏点击【文件】→【项目结构...】(快捷键 Ctrl+Alt+Shift+S):

  2. 选择"导入模块"

    在左侧导航栏选择【模块】,然后点击右上角的【+】按钮 → 【导入模块】:

  3. 选择要导入的 Maven 项目目录

    在弹出的文件选择对话框中,定位到目标 Maven 项目的根目录,并选择其 pom.xml 文件(如 maven-project02/pom.xml):

  4. 确认导入结果

    点击【确定】后,IDEA 会将该模块添加到当前项目中。返回项目视图,可在主项目下看到新导入的模块已列示:


2.5.2 导入方式2:通过 Maven 工具窗口导入模块(推荐)

相比于手动配置项目结构的方式,IDEA 提供了更智能、更便捷的 Maven 模块管理功能 。我们可以通过 Maven 工具窗口中的"+"按钮 直接添加新的 Maven 模块,实现一键导入,并自动解析依赖和构建路径。此方法是官方推荐的标准操作流程。

步骤说明:

  1. 打开 Maven 工具窗口并点击"+"按钮

    在 IDEA 右侧找到【Maven】工具窗口(若未显示,可通过【View】→【Tool Windows】→【Maven】打开),点击顶部的【+】按钮:

  2. 选择目标 Maven 项目的 pom.xml 文件

    在弹出的文件浏览器中,定位到要导入的子项目根目录(如 maven-project03),并选中其 pom.xml 文件,然后点击【确定】:

  3. 确认导入结果

    导入成功后,返回项目视图,可在主项目下看到新模块已列示为子目录:


3. Maven 依赖管理

3.1 配置依赖

在 Maven 项目中,依赖(Dependency) 是指当前项目运行所必需的外部 JAR 包。通过在 pom.xml 文件中声明依赖坐标(GAV),Maven 能够自动从仓库下载并管理这些库,避免手动复制 JAR 文件带来的混乱与版本冲突。

3.1.1 依赖配置核心流程

配置一个 Maven 依赖通常包括以下四个步骤:

  1. 查找依赖坐标
  2. pom.xml 中添加 <dependency> 标签
  3. 刷新 Maven 项目以下载依赖
  4. 验证依赖是否成功引入

我们以引入 Spring 框架中的 spring-context 模块为例,演示整个过程。


步骤一:使用 Maven Central 查找依赖坐标

当不知道某个库的 groupIdartifactIdversion 时,可访问 https://mvnrepository.com 进行搜索。

  1. 打开浏览器,进入 https://mvnrepository.com,在顶部搜索框输入目标模块名称(如 spring-context):

  2. 在搜索结果中找到匹配项,点击第一个结果 Spring Context

  3. 进入详情页后,查看其版本信息。选择最新稳定版本(如 7.0.1):

  4. 复制该版本标准的 Maven 依赖配置代码:

注意:若需兼容旧版本 JDK,可选择 6.x5.x 版本;生产环境建议使用 RELEASE 版本 ,避免使用 -SNAPSHOT


步骤二:在 pom.xml 中添加依赖

将复制的依赖配置粘贴到项目的 pom.xml 文件中 <dependencies> 标签内。

  1. 打开项目根目录下的 pom.xml 文件;
  2. <properties> 后添加如下配置:
xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>7.0.1</version>
    </dependency>
</dependencies>

步骤三:刷新 Maven 项目以下载依赖

添加依赖后,需通知 Maven 重新解析配置并下载 JAR 包。

  1. 在 IDEA 右侧的 Maven 工具窗口 中,点击顶部的 刷新按钮(🔄)
  2. Maven 将自动从本地仓库或远程镜像(如阿里云)下载 spring-context-7.0.1.jar 及其传递性依赖。

步骤四:验证依赖是否成功引入

成功导入后,可在以下位置确认依赖已生效:

  • Maven 工具窗口:查看"依赖项"树形结构;
  • target/classes 目录:编译后的类路径包含相关包;
  • 代码中使用类 :尝试导入 org.springframework.context.ApplicationContext 类,无报错即表示加载成功。

💡 提示:若未看到依赖,检查网络连接、本地仓库路径是否正确,或尝试重启 IDEA。


3.1.2 补充:依赖的传递性(Transitive Dependencies)

Maven 不仅会下载你显式声明的依赖,还会自动解析其传递性依赖(Transitive Dependencies)。例如:

xml 复制代码
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>7.0.1</version>
</dependency>

此依赖会自动拉取以下模块:

  • spring-core
  • spring-beans
  • spring-expression
  • spring-aop

这些依赖无需手动配置,Maven 会根据 spring-contextpom.xml 自动完成。


3.2 排除依赖

在 Maven 项目中,某些依赖会自动引入其传递性依赖(Transitive Dependencies) ,这些依赖可能与项目中其他模块存在版本冲突或不必要地增加包体积。为了解决此类问题,Maven 提供了 <exclusions> 配置机制,允许开发者主动排除特定的传递性依赖

✅ 例如:spring-context 依赖 micrometer-observation,但你不想使用该监控组件,即可通过 <exclusion> 将其排除。

3.2.1 依赖排除的核心原理

  • 排除依赖:指主动断开某个依赖项的资源链,阻止其被下载和加载;
  • 无需指定版本 :被排除的依赖仅需声明 groupIdartifactId,不需要写 version
  • 作用范围:仅影响当前依赖项的传递性关系,不影响其他依赖对它的引用。

步骤一:定位需要排除的依赖

spring-context 为例,它默认依赖多个子模块,包括:

  • spring-core
  • spring-beans
  • spring-expression
  • io.micrometer:micrometer-observation

其中,micrometer-observation 是一个用于指标收集的库,若项目未使用相关功能,可将其排除以减少冗余。

  1. 打开 IDEA 的 Maven 工具窗口 ,展开 maven-project01 的"依赖项"树形结构:

步骤二:在 pom.xml 中添加 <exclusions> 配置

在目标依赖的 <dependency> 标签内,添加 <exclusions> 子标签,并定义要排除的依赖坐标。

xml 复制代码
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>7.0.1</version>
    
    <exclusions>
        <exclusion>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-observation</artifactId>
        </exclusion>
    </exclusions>
    
</dependency>

步骤三:刷新项目并验证排除结果

保存 pom.xml 后,点击 Maven 工具窗口顶部的 刷新按钮(🔄),Maven 会重新解析依赖关系。

  1. 刷新完成后,再次查看"依赖项"列表;
  2. 发现 io.micrometer:micrometer-observation 已不再出现:

步骤四:理解排除机制的工作方式

  • 排除 ≠ 删除 :Maven 仍会保留原始依赖(如 spring-context),只是不再拉取其指定的子依赖;
  • 只影响当前路径 :如果另一个依赖也引入了 micrometer-observation,它仍然会被下载;
  • 推荐场景
    • 避免版本冲突(如不同模块引入不同版本的同一个库);
    • 减少最终 JAR 包大小;
    • 满足安全审计要求(如禁用第三方监控组件)。

💡 提示:可通过 mvn dependency:tree 命令查看完整的依赖树,快速识别哪些依赖被引入。


3.2 生命周期

3.2.1 生命周期概述

Maven 的构建过程是高度标准化和自动化的,其核心机制之一就是 生命周期(Lifecycle)。生命周期将项目的构建流程划分为一系列有序的阶段(Phase),确保不同项目在不同环境下都能以统一的方式完成编译、测试、打包和部署等操作。

核心目标:对所有 Maven 项目构建过程进行抽象与统一,提升开发效率和可维护性。


3.2.1.1 三大独立生命周期

Maven 定义了三套相互独立的生命周期,每套生命周期包含多个按顺序执行的阶段(Phase)。这些生命周期分别服务于不同的构建目的:

生命周期 用途
clean 清理工作,移除上一次构建生成的文件(如 target/ 目录)
default 核心构建任务,包括编译、测试、打包、安装、部署等
site 生成项目站点报告,如 API 文档、测试结果统计等

3.2.1.2 每个生命周期包含多个阶段(Phase)

每个生命周期由若干个有顺序的阶段 组成,后一个阶段依赖于前一个阶段的成功执行。例如,在 default 生命周期中,必须先完成 compile 阶段才能进入 test 阶段。

以下五个生命周期是最为核心的:


3.2.1.3 关键特性:阶段的继承性

💡 在同一套生命周期中,当执行某个阶段时,该阶段之前的所有阶段都会自动执行

例如:

  • 执行 mvn package 会自动触发以下阶段:

    bash 复制代码
    validate → initialize → ... → compile → test → package
  • 执行 mvn install 会自动执行至 install 阶段,包括前面所有步骤。

这保证了构建过程的完整性,避免因跳过关键步骤导致错误。


3.2.1.4 执行生命周期的两种方式

Maven 生命周期可通过以下两种方式进行执行:

在 IntelliJ IDEA 中执行
  • 打开右侧的 Maven 工具窗口
  • 展开项目下的 Lifecycle 节点;
  • 双击任意阶段(如 compiletestpackage),即可触发该阶段及其前置阶段的执行。
在命令行中执行
  • 打开终端(CMD 或 PowerShell);
  • 进入项目根目录;
  • 输入 mvn <phase> 命令执行指定阶段。
bash 复制代码
mvn compile        # 编译源码
mvn test           # 编译并运行测试
mvn package        # 编译、测试、打包
mvn install        # 打包并安装到本地仓库
mvn deploy         # 发布到远程仓库

3.2.2 执行指定生命周期的方式

3.2.2.1 clean 周期

clean 是 Maven 三大生命周期之一,主要用于清理上一次构建生成的文件 ,确保后续构建在干净的环境中进行。最常见的操作就是删除 target/ 目录下的所有内容,避免旧文件干扰新构建。

✅ 核心作用:移除构建产物,防止版本冲突或残留问题。


3.2.2.2 实战演示:执行 clean 生命周期

步骤一:查看当前项目状态

在执行 clean 之前,先观察项目目录结构:

  • maven-project01 项目根目录下已存在一个名为 target/ 的文件夹;
  • 该文件夹包含编译后的 .class 文件、打包生成的 JAR 包等构建产物;
  • 若不清理,可能导致重复构建时出现"已存在"错误或版本混乱。

步骤二:在 IDEA 中执行 clean
  1. 打开 IntelliJ IDEA 右侧的 Maven 工具窗口
  2. 展开 maven-project01 项目的 Lifecycle 节点;
  3. 找到并双击 clean 阶段(或右键选择"运行");

⚠️ 注意:点击 clean 后,Maven 会自动触发 pre-clean → clean → post-clean 整个流程。


步骤三:观察执行结果

1.执行完成后,IDEA 的底部输出面板将显示日志信息:

2.同时,项目视图中的 target/ 目录将被彻底删除:


3.2.2.3 compile 周期

compile 是 Maven default 生命周期 中的一个核心阶段,用于将项目主程序的 Java 源代码(位于 src/main/java)编译为字节码文件(.class),并输出到 target/classes 目录中。这是构建可运行程序的第一步,也是后续测试、打包等操作的基础。

✅ 核心作用:将人类可读的 .java 文件转换为 JVM 可执行的 .class 文件。


3.2.2.4 实战演示:执行 compile 阶段

步骤一:在 IDEA 中执行 compile
  1. 打开 IntelliJ IDEA 右侧的 Maven 工具窗口
  2. 展开 maven-project01 项目的 Lifecycle 节点;
  3. 找到并双击 compile 阶段(或右键选择"运行");

⚠️ 注意:执行 compile 时,Maven 会自动触发其前置阶段:

  • validateinitializegenerate-sourcesprocess-sourcesgenerate-resourcesprocess-resourcescompile

步骤二:观察编译结果

执行完成后,IDEA 的底部输出面板将显示日志信息:

同时,在项目视图中可以观察到以下变化:

  • target/ 目录下新增了 classes/ 子目录;
  • classes/org/example/Main.class 文件被成功生成;
  • 可通过双击 .class 文件查看反编译后的字节码内容。

3.2.2.5 package 周期

package 是 Maven default 生命周期 中的一个关键阶段,用于将编译后的类文件(.class)以及资源文件打包成标准的可分发格式,如 JARWAREAR 等。这是项目构建流程中的重要一步,标志着代码已经具备了"可发布"或"可部署"的能力。

✅ 核心作用:将 target/classes 目录下的所有内容打包为一个可运行的归档文件。


3.2.2.6 实战演示:执行 package 阶段

步骤一:确认前置条件

在执行 package 之前,必须确保:

  • 源码已成功编译(即 compile 阶段已完成);
  • target/classes 目录下存在编译后的 .class 文件;
  • pom.xml 中配置了正确的 <packaging> 类型(默认为 jar);

步骤二:在 IDEA 中执行 package
  1. 打开 IntelliJ IDEA 右侧的 Maven 工具窗口
  2. 展开 maven-project01 项目的 Lifecycle 节点;
  3. 找到并双击 package 阶段(或右键选择"运行");

⚠️ 注意:执行 package 时,Maven 会自动触发其前置阶段:

  • validate → compile → test → prepare-package → package

因此,即使你只点击 package,也会自动执行编译和测试等步骤。


步骤三:观察打包结果

执行完成后,IDEA 的底部输出面板将显示日志信息:

同时,在项目视图中可以观察到以下变化:

  • target/ 目录下新增了一个名为 maven-project01-1.0-SNAPSHOT.jar 的 JAR 包;
  • 该 JAR 包包含了 classes/ 下的所有 .class 文件及 resources/ 中的配置文件;
  • 可直接双击运行或通过命令行启动。

3.2.2.7 install 周期

install 是 Maven default 生命周期 中的一个重要阶段,用于将项目构建生成的可分发包(如 JAR、WAR)安装到本地 Maven 仓库中,以便其他项目可以作为依赖来引用它。

✅ 核心作用:将当前项目的输出文件(如 JAR 包)复制到本地仓库,供本机其他项目使用。


3.2.2.8 实战演示:执行 install 阶段

步骤一:确认前置条件

在执行 install 之前,必须确保:

  • 项目已成功编译(compile 已完成);
  • 项目已打包(package 已完成),即 target/ 目录下存在 *.jar 文件;
  • pom.xml 中配置了正确的 <groupId><artifactId><version>

步骤二:在 IDEA 中执行 install
  1. 打开 IntelliJ IDEA 右侧的 Maven 工具窗口
  2. 展开 maven-project01 项目的 Lifecycle 节点;
  3. 找到并双击 install 阶段(或右键选择"运行");

⚠️ 注意:执行 install 时,Maven 会自动触发其前置阶段:

  • validate → compile → test → package → install

因此,即使你只点击 install,也会自动执行编译、测试和打包等步骤。


步骤三:观察日志输出

执行完成后,IDEA 的底部输出面板将显示日志信息:

关键信息说明:

  • JAR 文件被安装到本地仓库;
  • POM 文件也被同步安装;
  • 安装路径由 <groupId><artifactId><version> 共同决定。

步骤四:验证本地仓库内容

打开本地 Maven 仓库路径(通常为 ~/.m2/repository 或自定义路径),按照以下结构查找:

复制代码
mvn_repo/
└── org/
    └── example/
        └── maven-project01/
            └── 1.0-SNAPSHOT/
                ├── maven-project01-1.0-SNAPSHOT.jar
                ├── maven-project01-1.0-SNAPSHOT.pom
                └── maven-metadata-local.xml

🔍 说明:

  • maven-project01-1.0-SNAPSHOT.jar:即项目打包后的 JAR 文件;
  • maven-project01-1.0-SNAPSHOT.pom:项目 pom.xml 的副本;
  • maven-metadata-local.xml:元数据文件,用于版本管理。

📌 描述:文件浏览器中显示 org 目录,对应 <groupId>org.example</groupId>

📌 描述:进入 org/example 目录,对应 org.example 组名。

📌 描述:进入 maven-project01 目录,对应 <artifactId>maven-project01</artifactId>

📌 描述:进入 1.0-SNAPSHOT 目录,对应 <version>1.0-SNAPSHOT</version>

📌 描述:最终目录中包含 maven-project01-1.0-SNAPSHOT.jar.pom 文件,表明安装成功。


3.2.2.9 使用命令行进行上述生命周期的操作

在实际开发中,虽然可以通过 IDE(如 IntelliJ IDEA)图形化界面执行 Maven 生命周期阶段,但掌握 命令行方式 是开发者必备技能。它不仅适用于自动化脚本、CI/CD 流水线,也便于理解 Maven 的底层执行逻辑。

本节将详细介绍如何使用 mvn 命令行工具执行 compilepackageinstall 等核心生命周期阶段。


3.2.2.9.1 前置准备

在使用命令行执行 Maven 操作前,需要确保:

  1. Maven 已正确安装并配置环境变量MAVEN_HOMEPATH);
  2. 项目根目录下存在 pom.xml 文件
  3. 当前工作目录为项目根目录 (即包含 pom.xml 的目录);

✅ 推荐做法:在 IDEA 中直接打开项目根目录的终端,避免路径错误。

步骤一:右键项目根目录 → 打开于 → 资源管理器

在 IntelliJ IDEA 中,右键点击项目根目录(如 maven-project01),选择菜单项:

  • "打开于" → "资源管理器"

这一步会自动打开系统文件资源管理器,并定位到项目根目录。


步骤二:在资源管理器中打开命令行窗口

在资源管理器中,地址栏输入 cmd 并回车,即可在当前目录下启动命令提示符(Command Prompt)。

⚠️ 注意:此方法会自动将 cmd 的当前工作目录设置为项目根目录,无需手动切换路径。


3.2.2.9.2 整体操作一览

在完成前置准备后,我们可以在命令行中逐步执行 Maven 的核心生命周期阶段。本节将通过实际命令演示从清理项目到安装至本地仓库的全过程,并结合日志分析每个阶段的行为。

✅ 操作顺序:clean → compile → package → install

这些命令是构建 Java 项目的标准流程,适用于单模块和多模块项目。


步骤一:执行 mvn clean ------ 清理构建产物
bash 复制代码
mvn clean
步骤二:执行 mvn compile ------ 编译源码
bash 复制代码
mvn compile

步骤三:执行 mvn package ------ 打包为 JAR
bash 复制代码
mvn package

步骤四:执行 mvn install ------ 安装到本地仓库
bash 复制代码
mvn install

4. 测试概述

软件测试是软件开发过程中不可或缺的关键环节,旨在通过系统化的验证手段,确保软件在功能、性能、安全性等方面满足预期要求。


4.1 测试的定义

测试(Testing) 是一种用来促进和鉴定软件的正确性、完整性、安全性和质量的过程。

  • 它通过对软件行为的观察与验证,发现潜在缺陷;
  • 提高软件可靠性,降低上线后出现故障的风险;
  • 是保障软件交付质量的重要手段。

4.2 测试阶段划分

软件测试通常按照开发流程划分为四个主要阶段,形成一个由细到粗、逐步推进的测试体系:

🔹 单元测试(Unit Testing)

  • 介绍:对软件的基本组成单位(如类、方法、函数)进行测试,是最小粒度的测试单元。
  • 目的:验证每个独立模块的功能是否正确,确保代码逻辑无误。
  • 测试人员:开发人员
  • 典型工具:JUnit、TestNG

🔹 集成测试(Integration Testing)

  • 介绍:将已通过单元测试的各个模块,按照设计要求组合成子系统或完整系统后进行测试。
  • 目的:检查模块之间的接口交互是否正常,数据传递是否准确。
  • 测试人员:开发人员
  • 常见场景:服务调用、数据库连接、API 接口联调

🔹 系统测试(System Testing)

  • 介绍:对已经集成好的软件系统进行全面、彻底的测试,验证整体功能与非功能性需求。
  • 目的:检验系统的正确性、稳定性、性能、安全性等是否满足指定要求。
  • 测试人员:专业测试人员(QA)
  • 测试范围:功能测试、性能测试、安全测试、兼容性测试等

🔹 验收测试(Acceptance Testing)

  • 介绍:也称交付测试,是针对用户需求和业务流程进行的正式测试,用于确认软件是否可以交付使用。
  • 目的:验证软件系统是否满足用户的验收标准,是否具备上线条件。
  • 测试人员:客户 / 需求方 / 产品经理
  • 常见形式:UAT(用户验收测试)

4.3 测试方法分类

根据测试过程中对软件内部结构的了解程度,测试方法可分为三大类:白盒测试、黑盒测试、灰盒测试

🔹 白盒测试(White-box Testing)

  • 特点:测试人员清楚软件内部结构、代码逻辑。
  • 关注点:代码路径、分支覆盖、循环逻辑、异常处理等。
  • 用途:验证代码实现的正确性,常用于单元测试和集成测试。
  • 适用对象:开发人员

✅ 工具支持:Junit + Mockito、SonarQube、JaCoCo(代码覆盖)


🔹 黑盒测试(Black-box Testing)

  • 特点:不关心软件内部实现,只关注输入输出行为。
  • 关注点:功能是否符合需求、界面是否友好、操作是否流畅。
  • 用途:验证软件的功能、兼容性、用户体验等。
  • 适用对象:测试人员、最终用户

✅ 常见方式:功能测试、回归测试、UI 自动化测试(Selenium)

🔹 灰盒测试(Gray-box Testing)

  • 特点:结合了白盒测试与黑盒测试的优点,既关注内部结构,又考虑外部表现。
  • 关注点:接口调用、数据流、权限控制等。
  • 用途:适用于集成测试和系统测试,尤其适合 Web 应用和服务端接口测试。
  • 适用对象:开发与测试人员协同

✅ 示例:测试 API 接口时,知道其内部参数校验逻辑,但以客户端视角发送请求并验证返回结果。



4.4 单元测试

单元测试(Unit Testing)是软件开发中最基础、最关键的测试环节之一,旨在对程序中的最小可测试单元(如类、方法)进行独立验证,确保其行为符合预期。通过编写自动化测试用例,开发者可以在代码修改后快速发现潜在问题,提升代码质量和开发效率。


4.4.1 快速入门案例

我们将以一个简单的 UserService 类为例,演示如何编写并运行单元测试。

4.4.1.1 业务逻辑如下:
java 复制代码
package org.example;

import java.time.LocalDate;
import java.time.Period;
import java.time.format.DateTimeFormatter;

public class UserService {

    /**
     * 给定一个身份证号, 计算出该用户的年龄
     * @param idCard 身份证号
     */
    public Integer getAge(String idCard){
        if (idCard == null || idCard.length() != 18) {
            throw new IllegalArgumentException("无效的身份证号码");
        }
        String birthday = idCard.substring(6, 14);
        LocalDate parse = LocalDate.parse(birthday, DateTimeFormatter.ofPattern("yyyyMMdd"));
        return Period.between(parse, LocalDate.now()).getYears();
    }


    /**
     * 给定一个身份证号, 计算出该用户的性别
     * @param idCard 身份证号
     */
    public String getGender(String idCard){
        if (idCard == null || idCard.length() != 18) {
            throw new IllegalArgumentException("无效的身份证号码");
        }
        return Integer.parseInt(idCard.substring(16,17)) % 2 == 1 ? "男" : "女";
    }

}

接下来,我们分三步完成单元测试配置与执行。


4.4.1.1 引入依赖

要在 Maven 项目中使用 JUnit 5 进行测试,需在 pom.xml 文件中添加对应的依赖。

依赖信息:

xml 复制代码
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.9.1</version>
</dependency>

📌 描述:右侧 Maven 工具栏显示 org.junit.jupiter:junit-jupiter:5.9.1 已成功下载并加入项目依赖。


4.4.1.2 创建测试类

测试类应位于 src/test/java 目录下,并遵循以下规范:

  • 包名与主代码一致(如 org.example);
  • 类名以 Test 结尾(如 UserServiceTest);
  • 使用 @Test 注解标记测试方法。
步骤一:创建测试包

右键点击 src/test/java → 新建 → 软件包 → 输入 org.example

步骤二:创建测试类

org.example 包下新建 Java 类 UserServiceTest

步骤三:编写测试代码
java 复制代码
// UserServiceTest.java
package org.example;

import org.junit.jupiter.api.Test;

/**
 * 测试类
 */
public class UserServiceTest {

    @Test
    public void testGetAge() {
        UserService userService = new UserService();
        Integer age = userService.getAge("100000200610011011");
        System.out.println(age);
    }
}

说明:

  • @Test 注解表示该方法是一个测试用例;
  • 方法命名建议以 test 开头,体现其用途;
  • 测试逻辑:传入身份证号,调用 getAge() 方法获取年龄并打印。

4.4.1.3 运行单元测试

在 IntelliJ IDEA 中,可以直接右键点击测试方法或整个类来运行测试。

操作方式:
  1. 将光标放在 testGetAge() 方法上;
  2. 右键选择 "运行 'UserServiceTest.testGetAge'"
  3. 或点击编辑器左侧绿色小三角图标。

📌 描述:IDEA 成功执行测试,输出结果为 25,表示身份证号对应出生年份为 2006 年,年龄为 25 岁(假设当前年份为 2025)。
🟢 结果解读:

  • 绿色勾 表示测试通过;
  • 若出现红色叉,则表示测试失败或异常;
  • 执行时间显示为 35毫秒,表明测试响应迅速。

4.5 断言

在单元测试中,仅仅让测试方法"运行不报错"是远远不够的。我们需要一种机制来验证被测方法的实际输出是否符合预期结果 ,这种机制就是------断言(Assertion)

断言的作用

通过比较实际结果与期望值,判断业务逻辑是否正确执行。如果不符合预期,则测试失败并抛出异常,帮助开发者快速定位问题。

JUnit 提供了 org.junit.jupiter.api.Assertions 类,封装了一系列常用的断言方法,用于验证各种条件是否成立。


4.5.1 为什么要使用断言?

虽然测试方法可以正常运行,但这并不意味着业务逻辑一定正确。例如:

java 复制代码
@Test
public void testGetAge() {
    UserService userService = new UserService();
    Integer age = userService.getAge("100000200610011011");
    System.out.println(age); // 输出: 25
}

上述代码虽然能打印出结果,但无法确认这个结果是否正确 。如果方法返回的是 null 或错误值,程序也不会报错,导致测试"误以为成功"。

🚨 因此,必须引入断言来主动验证结果是否符合预期!

使用断言的好处:
  • 明确表达测试意图;
  • 自动化判断测试是否通过;
  • 提高测试的可读性和可靠性。

4.5.2 常用断言方法

下表列出了 JUnit 5 中最常用的断言方法及其用途:

断言方法 描述
Assertions.assertEquals(Object exp, Object act, String msg) 检查两个值是否相等,若不相等则报错。
Assertions.assertNotEquals(Object unexp, Object act, String msg) 检查两个值是否不相等,若相等则报错。
Assertions.assertNull(Object act, String msg) 检查对象是否为 null,若不为 null 则报错。
Assertions.assertNotNull(Object act, String msg) 检查对象是否不为 null,若为 null 则报错。
Assertions.assertTrue(boolean condition, String msg) 检查条件是否为 true,若为 false 则报错。
Assertions.assertFalse(boolean condition, String msg) 检查条件是否为 false,若为 true 则报错。
Assertions.assertThrows(Class<Throwable> expType, Executable exec, String msg) 检查程序运行时是否抛出了预期的异常类型。

🔍 提示:

  • 所有断言方法的最后一个参数 msg 表示错误提示信息,可用于调试;
  • 若不指定该参数,系统会自动提供默认信息;
  • 每个方法都有多个重载版本,可省略 msg 参数。

4.5.3 实战示例:使用断言验证业务逻辑

4.5.3.1 测试正常逻辑代码

我们继续以 UserService 类中的 getGender() 方法为例,演示如何通过 断言 来验证其是否按预期工作,并利用测试结果反向推动代码优化。

✅ 被测方法说明:

java 复制代码
/**
 * 根据身份证号计算用户的性别
 * @param idCard 身份证号
 */
public String getGender(String idCard) {
    if (idCard == null || idCard.length() != 18) {
        throw new IllegalArgumentException("无效的身份证号码");
    }
    return Integer.parseInt(idCard.substring(16, 17)) % 2 == 0 ? "男" : "女";
}

🔍 规则说明

  • 身份证第17位为奇数 → 男性;偶数 → 女性;
  • 例如:100000200610011011 的第17位是 1(奇数),应返回 "男"
    注意:此时代码逻辑是错误逻辑

步骤一:编写带断言的测试用例

我们在 UserServiceTest.java 中添加一个测试方法,使用 Assertions.assertEquals() 验证性别判断逻辑:

java 复制代码
@Test
public void testGetGenderWithAssert() {
    UserService userService = new UserService();
    String gender = userService.getGender("100000200610011011");
    // 断言:期望返回 "男"
    Assertions.assertEquals("男", gender, "性别获取信息有问题");
}

✅ 说明:

  • expected: 预期值为 "男"
  • actual: 实际返回值;
  • message: 错误提示信息,便于调试。

步骤二:运行测试 → 发现问题

执行测试后,IDEA 显示测试失败:

🔍 分析:

  • 测试用例传入身份证号 100000200610011011,第17位是 1(奇数),应为男性;
  • 但实际返回 "女",说明逻辑有误。

步骤三:定位代码问题

打开 UserService.java,检查 getGender() 方法实现:

java 复制代码
return Integer.parseInt(idCard.substring(16, 17)) % 2 == 0 ? "男" : "女";

⚠️ 问题所在:

  • % 2 == 0 时返回 "男",即偶数为男;
  • 但根据国家标准:奇数为男,偶数为女
  • 所以判断条件写反了!
    💡 正确逻辑应为:
java 复制代码
return Integer.parseInt(idCard.substring(16, 17)) % 2 == 1 ? "男" : "女";

步骤四:修改代码

将判断条件改为 == 1

java 复制代码
return Integer.parseInt(idCard.substring(16, 17)) % 2 == 1 ? "男" : "女";

步骤五: 再次运行测试,最终效果对比
状态 代码 测试结果
❌ 初始版本 % 2 == 0 ? "男" : "女" 失败:预期"男",实际"女"
✅ 修复后 % 2 == 1 ? "男" : "女" 成功:返回"男"

📌 描述:测试完全通过,证明业务逻辑已修复。


4.5.3.2 测试异常逻辑代码

我们继续以 UserService.getGender(String idCard) 方法为例,该方法在传入 null 或长度不为 18 的身份证号时,应抛出 IllegalArgumentException 异常。

✅ 被测代码:

java 复制代码
public String getGender(String idCard) {
    if (idCard == null || idCard.length() != 18) {
        throw new IllegalArgumentException("无效的身份证号码");
    }
    return Integer.parseInt(idCard.substring(16, 17)) % 2 == 1 ? "男" : "女"; }

注意:此时代码逻辑是正确逻辑


步骤一:编写异常测试用例

我们使用 Assertions.assertThrows() 方法来验证当传入 null 时,是否抛出了预期的异常。

java 复制代码
@Test
public void testGetGenderWithAssert1() {
    UserService userService = new UserService();
    // 断言:期望抛出 NullPointerException
    Assertions.assertThrows(NullPointerException.class, () -> {
        userService.getGender(null);
    }, "传入 null 时应抛出空指针异常");
}

⚠️ 注意:此处我们故意写错异常类型,目的是演示测试失败时的反馈机制。


步骤二:运行测试 → 发现问题

执行测试后,IDEA 显示测试失败:

🔍 分析:

  • 方法内部抛出的是 IllegalArgumentException,而非 NullPointerException
  • 说明我们的断言类型写错了!

步骤三:定位被测代码中的异常类型

打开 UserService.java,查看 getGender() 方法实现:

java 复制代码
if (idCard == null || idCard.length() != 18) {
    throw new IllegalArgumentException("无效的身份证号码");
}

✅ 确认:确实抛出的是 IllegalArgumentException,而不是 NullPointerException


步骤四:修改断言

将断言中的异常类型改为 IllegalArgumentException.class

java 复制代码
@Test
public void testGetGenderWithAssert1() {
    UserService userService = new UserService();
    // 断言:期望抛出 IllegalArgumentException
    Assertions.assertThrows(IllegalArgumentException.class, () -> {
        userService.getGender(null);
    }, "传入 null 时应抛出非法参数异常");
}
再次运行测试 与 最终效果对比:
状态 断言类型 测试结果
❌ 错误版本 NullPointerException.class 失败:预期与实际异常类型不匹配
✅ 正确版本 IllegalArgumentException.class 成功:捕获到正确异常

4.6 常见注解

JUnit 5 提供了丰富的注解(Annotations),不仅简化了单元测试的编写流程,还增强了测试的灵活性和可维护性。这些注解帮助开发者更高效地组织测试逻辑、管理资源生命周期,并实现复杂的测试场景。

核心理念

"通过注解驱动测试行为",无需手动控制执行顺序,即可完成初始化、运行、清理等全流程。

注解 说明 备注
@Test 标记一个方法为测试方法,只有被此注解修饰的方法才会被执行 单元测试
@ParameterizedTest 支持参数化测试,允许同一个测试方法多次运行,每次传入不同参数 使用该注解后,无需再使用 @Test
@ValueSource @ParameterizedTest 提供测试参数来源,支持基本类型数组 与参数化测试配合使用
@DisplayName 自定义测试类或测试方法在运行时显示的名称,提升可读性 默认显示类名/方法名
@BeforeEach 修饰实例方法,在每个测试方法执行前运行一次 用于初始化资源(如数据库连接、对象实例)
@AfterEach 修饰实例方法,在每个测试方法执行后运行一次 用于释放资源(如关闭文件、清理缓存)
@BeforeAll 修饰静态方法,在所有测试方法之前只执行一次 通常用于全局初始化(如加载配置)
@AfterAll 修饰静态方法,在所有测试方法之后只执行一次 通常用于全局清理(如关闭服务)

📌 注意:

  • @BeforeAll@AfterAll 必须修饰 静态方法
  • @BeforeEach@AfterEach 修饰 实例方法
  • 所有方法都应声明为 public void,无返回值。

4.6.1 初始化操作:在测试前准备环境

@BeforeEach:每个测试前执行一次

适用于需要为每个测试方法创建独立环境的情况。

java 复制代码
@BeforeEach
public void setUp() {
    userService = new UserService(); // 每次测试前重新实例化
}

✅ 优点:

  • 避免测试之间相互影响;
  • 确保每个测试都在干净环境中运行。
@BeforeAll:所有测试前只执行一次

适用于一次性加载公共资源(如数据库连接池、配置文件)。

java 复制代码
@BeforeAll
static void beforeAll() {
    System.out.println("=== 开始所有测试 ===");
    // 加载全局配置、启动服务等
}

⚠️ 注意:

  • 必须是 静态方法
  • 仅执行一次,适合昂贵的初始化操作。

4.6.2 资源释放:在测试后清理环境

@AfterEach:每个测试后执行一次

适用于释放每个测试产生的临时资源。

java 复制代码
@AfterEach
public void tearDown() {
    if (userService != null) {
        userService = null; // 清理引用
    }
    System.out.println("单个测试结束,清理资源...");
}

✅ 优点:

  • 防止内存泄漏;
  • 保证测试隔离性。
@AfterAll:所有测试后只执行一次

适用于关闭全局资源(如数据库连接、服务器)。

java 复制代码
@AfterAll
static void afterAll() {
    System.out.println("=== 所有测试结束,关闭服务 ===");
    // 关闭数据库连接、停止线程池等
}

⚠️ 注意:

  • 必须是 静态方法
  • 通常与 @BeforeAll 成对使用。

4.6.2 参数化测试:支持方法形参

我们可以通过 @ParameterizedTest + @ValueSource 实现对同一测试逻辑的不同输入进行验证。

整体示例:测试 UserService.getGender() 方法的多种身份证号
java 复制代码
package org.example;

import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

/**
 * 用户服务测试类(UserService1Test)
 * <p>
 * 本类用于对 {@link UserService} 中的业务方法进行单元测试,
 * 主要验证身份证号码解析性别功能的正确性。
 * 使用 JUnit 5 的参数化测试特性,通过多组身份证数据批量验证逻辑。
 * </p>
 */
@DisplayName("用户服务测试类")
public class UserService1Test {

    /**
     * 全局初始化方法,在所有测试方法执行前仅运行一次。
     * 适用于一次性资源准备(如数据库连接、配置加载等)。
     * 注意:必须声明为 static 方法。
     */
    @BeforeAll
    public static void beforeAll() {
        System.out.println("before All");
    }

    /**
     * 全局清理方法,在所有测试方法执行完毕后仅运行一次。
     * 适用于释放全局资源(如关闭连接、清理临时文件等)。
     * 注意:必须声明为 static 方法。
     */
    @AfterAll
    public static void afterAll() {
        System.out.println("after All");
    }

    /**
     * 每个测试方法执行前都会调用此方法。
     * 用于初始化单个测试所需的对象或状态(例如新建被测对象实例)。
     */
    @BeforeEach
    public void beforeEach() {
        System.out.println("before Each");
    }

    /**
     * 每个测试方法执行后都会调用此方法。
     * 用于清理单个测试产生的副作用(如重置状态、关闭流等)。
     */
    @AfterEach
    public void afterEach() {
        System.out.println("after Each");
    }

    /**
     * 参数化测试方法:验证不同身份证号码对应的性别是否正确。
     * <p>
     * 通过 {@link CsvSource} 提供多组测试数据(仅身份证号),
     * 每组数据会独立运行一次该测试方法。
     * 假设规则:身份证第17位为奇数表示"男",偶数表示"女"。
     * </p>
     *
     * @param idCard 待测试的18位身份证号码(字符串形式)
     */
    @DisplayName("测试用户性别")
    @ParameterizedTest(name = "身份证 {0} 应返回性别: 男")
    @CsvSource({
            "100000200010011011",  // 第17位是 '1' → 男
            "100000200010011012",  // 第17位是 '1' → 男(注意:此处实际应为 '2'?请确认数据合理性)
            "100000200010011013"   // 第17位是 '1' → 男
    })
    public void testGetGender(String idCard) {
        // 创建被测对象实例
        UserService userService = new UserService();

        // 调用待测方法获取性别
        String gender = userService.getGender(idCard);

        // 断言:期望性别为"男",若不匹配则输出自定义错误信息
        Assertions.assertEquals("男", gender, "性别获取信息有问题");
    }
}

🔍 说明:

  • @ParameterizedTest 表示这是一个参数化测试;

  • @ValueSource(strings = {...}) 提供多个字符串参数;

  • 测试方法会自动运行两次,分别传入三个身份证号;

  • 每次运行时,idCard 参数值不同。
    💡 提示:

  • @ValueSource 仅支持基本类型(如 int, String, boolean);

  • 对于复杂对象,可使用 @CsvSource 或自定义参数提供器。

运行结果如下:


明白了!你希望不改变排版 ,也不修改已提供的代码内容 ,仅基于你刚刚给出的 4.7 节内容进行板块优化(如语言润色、逻辑更清晰、结构更规范),同时保留原有代码和格式不变。

以下是优化后的版本 ------ 完全保留你的原始代码与排版结构,仅对说明性文字进行精炼与规范化处理:


4.7 单元测试 ------ 企业开发规范

4.7.1 核心原则:尽可能覆盖所有可能情况(尤其是边界值)

原则 :编写测试方法时,应尽可能覆盖业务方法中所有可能的执行路径,尤其要关注边界值非法输入异常场景,以确保代码在各种情况下均能正确、稳定地运行。

4.7.2 示例:根据身份证号计算年龄 与 判断性别
java 复制代码
	/**
     * 给定一个身份证号, 计算出该用户的年龄
     * @param idCard 身份证号
     */
    public Integer getAge(String idCard){
        if (idCard == null || idCard.length() != 18) {
            throw new IllegalArgumentException("无效的身份证号码");
        }
        String birthday = idCard.substring(6, 14);
        LocalDate parse = LocalDate.parse(birthday, DateTimeFormatter.ofPattern("yyyyMMdd"));
        return Period.between(parse, LocalDate.now()).getYears();
    }


    /**
     * 给定一个身份证号, 计算出该用户的性别
     * @param idCard 身份证号
     */
    public String getGender(String idCard){
        if (idCard == null || idCard.length() != 18) {
            throw new IllegalArgumentException("无效的身份证号码");
        }
        return Integer.parseInt(idCard.substring(16,17)) % 2 == 1 ? "男" : "女";
    }

4.7.2.1 测试用例设计:全面覆盖各种输入情况

为保障 getGender()getAge() 方法的健壮性与可靠性,需设计全面的测试用例,覆盖以下场景:

  • 正常合法输入(验证核心逻辑)
  • 边界值(如长度刚好为18位)
  • 非法输入(如空值、长度错误)
  • 异常路径(验证是否按预期抛出异常)
4.7.2.2 标准测试用例列表:
测试条件 描述
idCard = null 检查空指针异常处理
idCard = "" 空字符串输入
idCard = "110" 长度不足的字符串
idCard = "1100002000100100111100000" 长度为19位的非法身份证
idCard = "110000200010010011" 正常长度(18位)且第17位为奇数 → 应返回"男"
idCard = "110000200010010021" 正常长度(18位)且第17位为偶数 → 应返回"女"

4.7.2.3 示例单元测试代码(JUnit + Assertions)
java 复制代码
package org.example;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/**
 * 用户信息测试类
 * 用于对 UserService 中的 getGender() 和 getAge() 方法进行单元测试。
 */
@DisplayName("用户信息测试类")
public class UserService2Test {

    private UserService userService;

    /**
     * 在每个测试方法执行前初始化 UserService 实例。
     */
    @BeforeEach
    public void setUp(){
        userService = new UserService();
    }

    /* ========================= 性别获取测试 ========================= */

    /**
     * 测试 getGender 方法传入 null 值时是否抛出 IllegalArgumentException。
     */
    @Test
    @DisplayName("获取性别-null值")
    public void testGetGender1(){
        Assertions.assertThrows(IllegalArgumentException.class, ()->{
            userService.getGender(null);
        });
    }

    /**
     * 测试 getGender 方法传入空字符串时是否抛出 IllegalArgumentException。
     */
    @Test
    @DisplayName("测试获取性别-空串")
    public void testGetGender2(){
        Assertions.assertThrows(IllegalArgumentException.class, ()->{
            userService.getGender("");
        });
    }

    /**
     * 测试 getGender 方法传入长度不足的身份证号(少于18位)时是否抛出异常。
     */
    @Test
    @DisplayName("测试获取性别-长度不足")
    public void testGetGender3(){
        Assertions.assertThrows(IllegalArgumentException.class, ()->{
            userService.getGender("110");
        });
    }

    /**
     * 测试 getGender 方法传入长度超过18位的身份证号时是否抛出异常。
     */
    @Test
    @DisplayName("测试获取性别-超出长度")
    public void testGetGender4(){
        Assertions.assertThrows(IllegalArgumentException.class, ()->{
            userService.getGender("10000020001001101111");
        });
    }

    /**
     * 测试 getGender 方法传入合法18位身份证号且第17位为奇数(表示男性)时,返回"男"。
     */
    @Test
    @DisplayName("测试获取性别- 正常:男")
    public void testGetGender5(){
        String gender = userService.getGender("100000200010011011");
        Assertions.assertEquals("男", gender);
    }

    /**
     * 测试 getGender 方法传入合法18位身份证号且第17位为偶数(表示女性)时,返回"女"。
     */
    @Test
    @DisplayName("测试获取性别- 正常:女")
    public void testGetGender6(){
        String gender = userService.getGender("100000200010011002");
        Assertions.assertEquals("女", gender);
    }

    /* ========================= 年龄获取测试 ========================= */

    /**
     * 测试 getAge 方法传入合法18位身份证号时能否正常返回年龄(注意:当前测试未断言结果,仅调用)。
     */
    @Test
    @DisplayName("获取年龄-正常身份证")
    public void testGetAge() {
        Integer age = userService.getAge("100000200010011011");
        Assertions.assertEquals(25, age);
    }

    /**
     * 测试 getAge 方法传入 null 值时是否抛出 IllegalArgumentException。
     */
    @Test
    @DisplayName("获取年龄-null值")
    public void testGetAge1(){
        Assertions.assertThrows(IllegalArgumentException.class, ()->{
            userService.getAge(null);
        });
    }

    /**
     * 测试 getAge 方法传入空字符串时是否抛出 IllegalArgumentException。
     */
    @Test
    @DisplayName("获取年龄-空串")
    public void testGetAge2(){
        Assertions.assertThrows(IllegalArgumentException.class, ()->{
            userService.getAge("");
        });
    }

    /**
     * 测试 getAge 方法传入长度不足的身份证号(少于18位)时是否抛出异常。
     */
    @Test
    @DisplayName("获取年龄-长度不足")
    public void testGetAge3(){
        Assertions.assertThrows(IllegalArgumentException.class, ()->{
            userService.getAge("110");
        });
    }

    /**
     * 测试 getAge 方法传入长度超过18位的身份证号时是否抛出异常。
     */
    @Test
    @DisplayName("获取年龄-超出长度")
    public void testGetAge4(){
        Assertions.assertThrows(IllegalArgumentException.class, ()->{
            userService.getAge("10000020001001101111");
        });
    }
}

4.7.2.4 代码覆盖

现代 IDE(如 IntelliJ IDEA)支持直接运行测试并生成代码覆盖报告,帮助开发者直观了解哪些代码已被测试覆盖。

  1. 在测试类上右键点击 → 选择: Run 'UserService2Test' with Coverage
  2. IDE 将自动执行所有测试,并高亮显示:
    • ✅ 已执行的代码(绿色)
    • ⚠️ 未执行的代码(红色或黄色)
  3. 可视化反馈有助于发现漏测的分支 ,例如:
    • null 输入是否抛出异常?
    • 长度不为18的字符串是否被捕获?

💡 建议 :每次提交前,确保关键方法的测试覆盖率达到 80%以上,尤其关注异常处理路径。

📌 说明:在 IntelliJ IDEA 中,右键点击测试类后选择"使用覆盖率运行"选项(Run 'UserService2Test' with Coverage),即可启动带有代码覆盖分析的测试执行。

📌 说明:测试完成后,IDE 显示代码覆盖统计信息。图中显示 UserService 类的方法覆盖率为 100%行覆盖率为 100%,表明所有逻辑路径均被测试用例覆盖,符合企业级开发规范要求。


4.7.2.5 修改覆盖率作用对象的方法

在实际开发中,有时我们希望只对特定类进行代码覆盖分析 ,而不是整个项目或所有被测试类。例如:只想查看 UserService 类的覆盖情况,而忽略其他测试类或辅助类。

IntelliJ IDEA 提供了灵活的配置选项,允许我们自定义 "代码覆盖率的作用对象"(即哪些类参与覆盖统计)。

操作步骤如下:
  1. 在顶部运行配置下拉菜单中,点击当前测试类名称(如 UserService2Test),然后选择:

    复制代码
    编辑配置...
  2. 在弹出的"运行/调试配置"窗口中,找到下方的 "代码覆盖率" 区域,点击 "+" 号按钮,选择:

    复制代码
    添加类...
  3. 在弹出的"选择类"对话框中,输入要包含的类名(如 UserService),从列表中选中目标类(org.example.UserService),然后点击:

    复制代码
    确定
  4. 返回配置页面后,确认已成功添加 org.example.UserService 到覆盖率列表中,点击:

    复制代码
    确定
  5. 重新运行带覆盖率的测试(Run with Coverage),即可看到仅显示 UserService 类的覆盖数据


4.8 使用AI辅助单元测试的撰写(谨慎使用)

随着 AI 编程助手(如 IntelliJ 的 Lingma、GitHub Copilot 等)在开发中的普及,越来越多开发者尝试通过 AI 自动生成单元测试代码。虽然这可以提升编码效率,但必须保持警惕 :AI 生成的代码可能存在逻辑缺陷、覆盖不足、性能问题等风险。

⚠️ 核心提醒:AI 是辅助工具,不能替代人工审核与质量把控。

操作示例:使用 AI 生成单元测试

  1. UserService.java 中右键点击目标方法(如 getGender()),选择:

    复制代码
    生成单元测试
  2. AI 会自动生成一个测试类(如 UserServiceAITest.java),包含基于当前方法的测试用例,例如:

    • 正常输入测试
    • 异常输入处理(如 null 值)
    • 参数边界验证
  3. 运行带覆盖率的测试后,发现实际覆盖情况并不理想:

说明:尽管测试通过了,但 UserService 类的方法覆盖率为 50%行覆盖率为 44% ,表明 AI 只生成了针对 getGender() 方法的测试,而忽略了 getAge() 方法的测试,导致关键业务逻辑未被覆盖。


5. 依赖范围

5.1 junit 范例

在 Maven 项目中,依赖项的作用范围(Scope)决定了该依赖在哪些生命周期可用,以及是否参与打包运行。

关键点junit 作为测试框架,只应在测试阶段使用,不应出现在主程序中。

5.1.1 为什么需要设置 <scope>test</scope>

  • 默认情况下,Maven 会将所有依赖引入到 compile 阶段,即主程序也可以使用。
  • 如果 junit 被错误地编译进主程序,会导致:
    • 编译报错(如无法解析 @Test 注解)
    • 打包后 jar 文件体积增大
    • 运行时引入不必要的测试类

5.1.2 示例:正确配置 JUnit 的依赖范围

xml 复制代码
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.9.3</version>
    <scope>test</scope>
</dependency>

📌 说明:在 pom.xml 中为 junit-jupiter 设置 <scope>test</scope>,表示该依赖仅用于测试阶段。

5.1.3实际效果验证

junit 的 scope 设置为 test 后:

  1. src/main/java 中使用 @Test 注解会提示错误:

    ❌ 无法解析符号 "Test"

  2. src/test/java 中可以正常编译和运行测试用例。

  3. 使用 mvn package 打包时,不会包含 JUnit 类,避免污染最终产物。

结论 :通过设置 <scope>test</scope>,实现了"测试依赖隔离",保证了主程序的纯净性与可部署性。


5.2 生命周期补充---Test生命周期

Maven 的生命周期分为多个阶段,其中 test 是一个独立的生命周期目标,专门用于执行单元测试。

5.2.1 Maven 生命周期中的 test 阶段

  • 作用 :编译并运行 src/test/java 下的所有测试类。
  • 触发方式
    • 执行命令:mvn test
    • 或在 IDE 中点击运行按钮(如 IntelliJ IDEA 的绿色三角)

📌 说明:Maven 生命周期中,test 阶段位于 compile 之后、package 之前,负责执行测试任务。

5.2.2 跳过 Test 生命周期

在某些场景下(如仅需编译或打包),可以跳过测试执行:

方法一:使用 -DskipTests 参数
bash 复制代码
mvn clean compile package -DskipTests

⚠️ 注意:此参数仅跳过测试执行,但仍会编译测试代码。

方法二:使用 -Dmaven.test.skip=true
bash 复制代码
mvn clean compile package -Dmaven.test.skip=true

✅ 此参数会完全跳过测试编译和执行,适合快速构建发布包。

方法三:使用 Maven 图形化界面工具

](https://i-blog.csdnimg.cn/direct/9f2058fc185a40bea443a84d6d58f0e0.png)

📌 说明:此时在 Maven 工具栏中点击 package 按钮即可跳过test阶段,直接进行打包


6. Maven常见问题 与 解决方法


相关推荐
阿宁又菜又爱玩2 小时前
Maven基础知识
java·maven
冷雨夜中漫步2 小时前
Maven BOM(Bill of Materials)使用指南与常见错误
java·数据库·maven
BUTCHER52 小时前
maven插件
java·maven
Hui Baby4 小时前
maven自动构建到镜像仓库
java·maven
永不停歇的蜗牛19 小时前
Maven的POM文件相关标签作用
服务器·前端·maven
洛克大航海1 天前
Maven 的下载安装配置教程
java·maven
Mr.朱鹏1 天前
RocketMQ可视化监控与管理
java·spring boot·spring·spring cloud·maven·intellij-idea·rocketmq
咖啡不甜不好喝1 天前
IDEA Maven设置所有项目生效
maven·idea
0和1的舞者1 天前
《Maven 核心功能与仓库体系详解》
ide·maven·项目管理·仓库·依赖