Java工具篇之Maven:从pom.xml到测试通过的JAR

昨天的Spring Boot项目用一条命令就能编译、测试和打包。今天把这条命令拆开:Maven依据pom.xml组织构建,并由插件执行具体工作。理解这层分工,才能看懂"IDE里能运行,命令行却失败"究竟是哪一步出了问题。

本文用Maven3.9.9、JDK21和一个完整的小项目讲清坐标、目录、依赖、生命周期、插件与排错。我们让测试真正守住打包入口,再观察产物里到底有什么。

一、先认识项目的三个坐标

一个制品通常用groupId、artifactId、version识别。例如demo:build-lab:1.0.0中,demo是组织命名空间,build-lab是制品名,1.0.0是版本。默认packaging是jar;它决定默认绑定哪些构建任务。

POM并非只是一张下载清单。它同时描述项目身份、依赖和构建配置。dependencies供项目代码使用,build/plugins参与构建过程。JUnit是测试依赖;编译器插件负责调用编译器,Surefire插件运行单元测试,JAR插件打包。

二、建立一个可验证的最小项目

目录如下,测试文件名称以Test结尾,能被本例Surefire默认规则发现:

text 复制代码
build-lab/
├─ pom.xml
└─ src/
   ├─ main/java/demo/App.java
   └─ test/java/demo/AppTest.java

完整pom.xml:

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>demo</groupId><artifactId>build-lab</artifactId><version>1.0.0</version>
  <properties>
    <maven.compiler.release>21</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId>
      <version>5.11.4</version><scope>test</scope>
    </dependency>
  </dependencies>
  <build><plugins>
    <plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.13.0</version></plugin>
    <plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.2</version></plugin>
    <plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>3.4.2</version>
      <configuration><archive><manifest><mainClass>demo.App</mainClass></manifest></archive></configuration>
    </plugin>
  </plugins></build>
</project>

release设为21,要求编译环境支持该目标,并约束使用的Java平台API。它不负责替你安装JDK。先运行mvn -v,检查Maven实际使用的Java版本与Java home;IDE项目SDK和终端JAVA_HOME可能不同。

显式固定插件版本,使团队采用相同的插件行为。JUnit只在测试阶段需要,所以配置test作用域。主程序不依赖它,运行时也不需要携带它。

App.java:

java 复制代码
package demo;

public class App {
    public static String label(String title) {
        if (title == null || title.isBlank()) {
            throw new IllegalArgumentException("title is blank");
        }
        return "TODO: " + title.strip();
    }
    public static void main(String[] args) {
        System.out.println(label(args.length == 0 ? "learn Maven" : args[0]));
    }
}

AppTest.java:

java 复制代码
package demo;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class AppTest {
    @Test void trimsTitle() {
        assertEquals("TODO: learn Maven", App.label("  learn Maven  "));
    }
    @Test void rejectsBlank() {
        assertThrows(IllegalArgumentException.class, () -> App.label("  "));
    }
    @Test void rejectsNull() {
        assertThrows(IllegalArgumentException.class, () -> App.label(null));
    }
}

测试覆盖正常标题、空白和null三条路径。这个例子虽小,失败条件却明确:如果以后删除输入检查,测试应当阻止构建继续成功。

三、阶段与目标如何配合

运行mvn verify时,Maven沿default生命周期执行到verify;不是只执行一个叫verify的动作。主要阶段可以这样读:

阶段 关心的结果
compile 主源码编译成功
test 单元测试通过
package 生成JAR等制品
verify 执行已绑定的后续质量检查
install 把制品放入本地仓库,供其他本地项目引用
deploy 将制品发布到远程制品仓库

阶段规定顺序,插件目标负责动作。 例如compiler:compile执行编译,surefire:test执行测试。直接调用插件目标不等于自动执行它之前的全部生命周期阶段。verify是否执行集成测试或静态检查,取决于项目是否配置了对应插件和绑定,阶段名称本身不会自动生成这些检查。生命周期说明

clean属于另一条生命周期。mvn clean verify先清理再构建;单独mvn verify不会自动清空旧产物。日常增量构建可以不clean,排除遗留文件或做交付验收时使用clean更直接。

四、运行、检查产物,再故意让测试失败

在pom.xml所在目录执行:

bash 复制代码
mvn -v
mvn clean verify
java -jar target/build-lab-1.0.0.jar
jar tf target/build-lab-1.0.0.jar

本次实际验收:3个测试全部通过,JAR启动输出TODO: learn Maven;故意改错断言后构建失败且未生成JAR,恢复后重新构建成功。JAR目录中包含App.class,不包含测试类和JUnit。

本例构建通过后,测试报告在target/surefire-reports,主类在target/classes,JAR在target/build-lab-1.0.0.jar。我们配置Main-Class清单,因此可以用java -jar启动。

普通JAR插件不会自动把所有第三方依赖打进JAR。这里主程序只用JDK,所以能独立运行。若加入运行时库,需要明确classpath、分发依赖,或选择适合项目的打包插件;Spring Boot可执行包也有自己的打包方式。

将测试期望临时改为错误字符串,再执行mvn clean verify:测试阶段失败,命令返回非零退出码,后续打包不会执行。恢复期望后再次构建。通过一次可控失败,可以确认CI执行的确实是测试,而非只编译源码或跳过测试。

不要把跳过测试当作修复。 -DskipTests通常跳过测试执行但仍编译测试代码;maven.test.skip会被相关插件用于跳过测试编译和执行。交付流水线应保留真实验收步骤。

五、依赖怎样进入classpath

作用域 主要用途
compile 默认值,主编译与运行都需要
provided 编译需要,运行环境提供
runtime 主编译不需要,运行需要
test 测试编译和执行需要

传递依赖把依赖的依赖带进解析图,实际传播还受作用域、optional和exclusions影响。同一个库出现多个版本时,Maven通常选择依赖路径更近的定义;同深度时先声明者优先。不要把"声明过一个新版本"理解为依赖图里所有关联库都自动升级。依赖机制

用依赖树找来源,比直接往pom.xml反复加版本更有效:

bash 复制代码
mvn dependency:tree
mvn help:effective-pom

dependencyManagement集中管理依赖版本和相关属性,本身不等于引入依赖。子项目仍在dependencies声明需要的库。pluginManagement也用于管理插件配置,是否执行仍要看插件实际声明和生命周期绑定;不要把管理区当作执行清单。

六、仓库、重复构建与排错顺序

本地仓库存放已下载依赖和本地install产物,远程仓库提供可下载制品。install成功只说明当前机器的其他项目可以解析这个版本;让同事和CI使用,应通过团队远程制品仓库发布,不能依靠复制个人仓库目录。

项目配置提交到版本库,仓库凭据放在受控的settings.xml或CI凭据系统中。固定依赖与插件版本、使用Maven Wrapper统一Maven版本,再统一JDK环境,才能减少"同一份代码,不同机器构建不同"的情况。SNAPSHOT内容可以变化,缓存与更新策略也会影响解析结果。

遇到失败按日志定位:先看mvn -v确认环境,再看第一个失败插件;编译错误查源码与release,测试错误查surefire-reports,解析失败查坐标、网络和仓库配置。只有明确是某个缓存制品损坏,才针对它处理;删除整个本地仓库会造成大量无关重新下载。

七、🧠 思维导图

#mermaid-svg-Dzay11U1G9ILZ7Cu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Dzay11U1G9ILZ7Cu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Dzay11U1G9ILZ7Cu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Dzay11U1G9ILZ7Cu .error-icon{fill:#552222;}#mermaid-svg-Dzay11U1G9ILZ7Cu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Dzay11U1G9ILZ7Cu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Dzay11U1G9ILZ7Cu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Dzay11U1G9ILZ7Cu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Dzay11U1G9ILZ7Cu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Dzay11U1G9ILZ7Cu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Dzay11U1G9ILZ7Cu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Dzay11U1G9ILZ7Cu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Dzay11U1G9ILZ7Cu .marker.cross{stroke:#333333;}#mermaid-svg-Dzay11U1G9ILZ7Cu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Dzay11U1G9ILZ7Cu p{margin:0;}#mermaid-svg-Dzay11U1G9ILZ7Cu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Dzay11U1G9ILZ7Cu .cluster-label text{fill:#333;}#mermaid-svg-Dzay11U1G9ILZ7Cu .cluster-label span{color:#333;}#mermaid-svg-Dzay11U1G9ILZ7Cu .cluster-label span p{background-color:transparent;}#mermaid-svg-Dzay11U1G9ILZ7Cu .label text,#mermaid-svg-Dzay11U1G9ILZ7Cu span{fill:#333;color:#333;}#mermaid-svg-Dzay11U1G9ILZ7Cu .node rect,#mermaid-svg-Dzay11U1G9ILZ7Cu .node circle,#mermaid-svg-Dzay11U1G9ILZ7Cu .node ellipse,#mermaid-svg-Dzay11U1G9ILZ7Cu .node polygon,#mermaid-svg-Dzay11U1G9ILZ7Cu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Dzay11U1G9ILZ7Cu .rough-node .label text,#mermaid-svg-Dzay11U1G9ILZ7Cu .node .label text,#mermaid-svg-Dzay11U1G9ILZ7Cu .image-shape .label,#mermaid-svg-Dzay11U1G9ILZ7Cu .icon-shape .label{text-anchor:middle;}#mermaid-svg-Dzay11U1G9ILZ7Cu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Dzay11U1G9ILZ7Cu .rough-node .label,#mermaid-svg-Dzay11U1G9ILZ7Cu .node .label,#mermaid-svg-Dzay11U1G9ILZ7Cu .image-shape .label,#mermaid-svg-Dzay11U1G9ILZ7Cu .icon-shape .label{text-align:center;}#mermaid-svg-Dzay11U1G9ILZ7Cu .node.clickable{cursor:pointer;}#mermaid-svg-Dzay11U1G9ILZ7Cu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Dzay11U1G9ILZ7Cu .arrowheadPath{fill:#333333;}#mermaid-svg-Dzay11U1G9ILZ7Cu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Dzay11U1G9ILZ7Cu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Dzay11U1G9ILZ7Cu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Dzay11U1G9ILZ7Cu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Dzay11U1G9ILZ7Cu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Dzay11U1G9ILZ7Cu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Dzay11U1G9ILZ7Cu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Dzay11U1G9ILZ7Cu .cluster text{fill:#333;}#mermaid-svg-Dzay11U1G9ILZ7Cu .cluster span{color:#333;}#mermaid-svg-Dzay11U1G9ILZ7Cu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Dzay11U1G9ILZ7Cu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Dzay11U1G9ILZ7Cu rect.text{fill:none;stroke-width:0;}#mermaid-svg-Dzay11U1G9ILZ7Cu .icon-shape,#mermaid-svg-Dzay11U1G9ILZ7Cu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Dzay11U1G9ILZ7Cu .icon-shape p,#mermaid-svg-Dzay11U1G9ILZ7Cu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Dzay11U1G9ILZ7Cu .icon-shape .label rect,#mermaid-svg-Dzay11U1G9ILZ7Cu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Dzay11U1G9ILZ7Cu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Dzay11U1G9ILZ7Cu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Dzay11U1G9ILZ7Cu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Maven构建
POM与坐标
生命周期
依赖
验收
项目与插件配置
compile test package verify
install与deploy
作用域与版本仲裁
测试失败阻止打包
依赖树与有效POM

八、总结

总结要点

POM把构建要求写成共享约定。 坐标说明项目是谁,依赖说明代码需要什么,插件说明构建如何执行。

生命周期让步骤有序衔接。 选择需要到达的阶段,让测试与检查成为产物生成的前置条件,再用一次失败实验验证它们真的生效。

排错要看实际环境和解析结果。 mvn -v、依赖树、有效POM和测试报告,比反复点IDE重试更能解释问题。

下一篇继续工具篇,使用Gradle重建同一个小项目,对比任务图、依赖配置与Wrapper。

👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊

相关推荐
wuminyu8 小时前
JVM规范学习总结:解释器与JIT编译器如何处理JVM字节码
java·linux·c语言·jvm·c++
txxzjmzlh8 小时前
SpringBoot 配置⽂件
java·spring boot·spring
Wang's Blog8 小时前
Java 项目实战: 交友项目 - 搜索附近的功能概述和分析
java·开发语言·交友
骑着蜗牛撵大象3279 小时前
大模型 Agent 的沙箱护栏:gVisor 隔离、出口白名单与审计闭环
java·开发语言·jvm·安全·agent沙箱·gvisor·microvm
独泪了无痕9 小时前
Hutool之LocalDateTimeUtil:让时间间隔操作如虎添翼
java
三无少女指南10 小时前
从这里开始:Java 后端与 AI 工程的三条阅读路线
java·学习路线·ai工程
香蕉大香蕉(06敬畏市场)10 小时前
MySQL 单点详解(07)
java·数据库·mysql·ai·博客·八股
nice_lcj52011 小时前
深入理解 Java 反射:从 Class 对象到方法调用(含性能实测 + 迷你 IOC 实战 + 面试考点)
java·开发语言·后端·面试
骑着蜗牛撵大象32711 小时前
2026 后端 AI 工程化:Spring AI 管调用、MCP 管工具、Agent 内嵌管自治
java·人工智能·后端·spring·ai·可观测性·mcp