Maven from Scratch: Coordinates, Dependency Mediation and Build Lifecycles

bee2026-10-0846 min read0 views
Mirrors via settings.xml, GAV coordinates and repositories, dependency scopes and mediation, the three build lifecycles and multi-module builds — every core Maven concept in one pass.
1 / 154
Section
0. The 30-second version
2 / 154

Maven does exactly one job: you write down what you need in a shopping list (pom.xml), and it fetches, processes and packs accordingly. You never need to know which corner of the disk a jar lives in, nor hand-assemble a compile command — you just write a coordinate (organization + module + version) and let it handle the rest. This article covers four things: who you are (coordinates) → what you need (dependencies and scopes) → where to fetch (repositories and mirrors) → how to process (lifecycles and multi-module builds).

3 / 154
类比|Analogy

Maven is a parcel sorting centre. The address you type when ordering is the coordinate (groupId:artifactId:version); the sorter scans the label and knows which conveyor belt (which lifecycle phase) the parcel belongs on, without any idea which floor your living room is on. And scope is the sticker on the box — "open for inspection only" / "must be signed for" / "do not deliver to this address". Stick it wrong and the parcel shows up in the wrong place, or simply never arrives.

4 / 154
Diagram
Figure · Chapter map: Maven answers just four questions
Figure · Chapter map: Maven answers just four questions
5 / 154

After this article you should be able to answer:

6 / 154
  • Why does one pom.xml rebuild the project on another machine, while a hand-copied javac -cp line cannot?
  • If two paths drag in two versions of the same library, which one wins — and can I overrule it?
  • What does each of the five words in mvn clean package -DskipTests actually trigger? Is test code still compiled?
7 / 154
Section
1. Why do we need a build tool at all?
8 / 154

Start with the world before build tools. You write three classes and compile them with one line:

9 / 154
bash
javac -d out src/main/java/com/example/OrderService.java
10 / 154

javac follows the imports and compiles the classes it depends on. But a real project has two hundred source files and a dozen third-party libraries, and the command balloons into this:

11 / 154
Code
Codebash
javac -d out \  -cp "lib/spring-core.jar:lib/spring-context.jar:lib/jackson-databind.jar:lib/mysql-connector-j.jar" \  $(find src/main/java -name "*.java")
Notes
  • Every new dependency means hand-adding another jar path to -cp; miss one and the build fails
  • Dependencies have their own dependencies (transitive ones), and completing that list by memory is nearly impossible
  • Move to another machine and the paths and versions all change — what blocks you is the environment, never the code
12 / 154

And that is only "compile". "Run the tests", "build the jar", "deploy to the server" each repeat this manual work. This is exactly what a build tool fixes: turning resolve, compile, test, package and deploy into one repeatable, declarative, machine-independent pipeline.

13 / 154

Maven is the most classic and most widely used tool on that pipeline. The fundamental difference from raw javac is what it trusts:

14 / 154
  • Raw javac trusts a local path — lib/xxx.jar must physically exist on disk
  • Maven trusts a coordinate — you declare org.springframework:spring-core:6.1.0 and it fetches the file from a repository itself
  • So moving machines needs only one pom.xml, with no dependence on anyone's local setup
15 / 154
Diagram
Figure · Maven core concepts
Figure · Maven core concepts
16 / 154

That diagram is the skeleton of this article: top-left is the coordinate (GAV) used to identify one artifact; bottom-left is the repository system that decides where to fetch it; the two columns on the right are lifecycles and plugins, which decide what happens to it afterwards. Every section below just zooms into one of those boxes.

17 / 154
类比|Analogy

how Maven looks for a jar is exactly how you look for a book at university — main library first. You check your own shelf (the local repository ~/.m2/repository); if it is not there you ask the campus loan desk (a private Nexus repo); only then do you go to the national library (Maven Central). Each level keeps a photocopy once found, so borrowing the same book a second time takes almost no time at all.

18 / 154
Note

remember Maven as two things — a "dependency butler" plus a "standardized pipeline". And pom.xml is its spec sheet, declaratively describing what the project needs and produces.

19 / 154
Section
2. Installing Maven and getting settings.xml right
20 / 154

Maven itself is a zip: unzip it and add bin to PATH; it also needs a JAVA_HOME pointing at a JDK:

21 / 154
powershell
# Windows: unzip to D:\dev\apache-maven-3.9.6, then set the environment variablesMAVEN_HOME = D:\dev\apache-maven-3.9.6PATH      += %MAVEN_HOME%\binmvn -v# Apache Maven 3.9.6 ... Java version: 17.0.10 ... OS name: "windows 11"
22 / 154

mvn -v prints the Maven version, the JDK it actually uses, and the OS — the first thing to check when "Maven is using the wrong JDK".

23 / 154
Section
2.1 settings.xml: the key to fast mirrors
24 / 154

By default Maven downloads from the overseas central repository (repo.maven.apache.org), which is often slow enough to time out inside China. The fix is a mirror. Here is the point beginners confuse most: settings.xml has two locations, with completely different scopes.

25 / 154
Table
LocationScopeFollows the machine?Recommendation
MAVEN_HOME/conf/settings.xmlAll usersNo (lost on reinstall)Read it for defaults, don't edit
~/.m2/settings.xml (user level)Current userYes (follows the person)Edit this one
26 / 154

Here is a complete user-level ~/.m2/settings.xml, ready to copy:

27 / 154
Code
Codexml
<?xml version="1.0" encoding="UTF-8"?><settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0                              http://maven.apache.org/xsd/settings-1.0.0.xsd">  <!-- 1. Local repository: where downloaded dependencies are stored -->  <localRepository>D:/dev/.m2/repository</localRepository>  <mirrors>    <!-- 2. Aliyun mirror: mirrors the central repo for much faster downloads -->    <mirror>      <id>aliyun-central</id>      <name>Aliyun Central</name>      <url>https://maven.aliyun.com/repository/public</url>      <mirrorOf>central</mirrorOf>    </mirror>  </mirrors>  <profiles>    <profile>      <id>jdk-17</id>      <activation>        <activeByDefault>true</activeByDefault>      </activation>      <properties>        <maven.compiler.source>17</maven.compiler.source>        <maven.compiler.target>17</maven.compiler.target>      </properties>    </profile>  </profiles></settings>
Notes
  • <localRepository>: tells Maven where to cache dependencies, ~/.m2/repository by default; a separate drive is easier to back up
  • <mirror>: rewrites requests for central to the Aliyun URL; mirrorOf is the matching rule
  • mirrorOf as central mirrors only the central repo; as it mirrors every repo (including your private one) — *never use when a private repo is involved*

Trap: setting mirrorOf to hijacks your company's private repository requests to Aliyun, so internal artifacts can never be resolved and you get Could not find artifact com.yourcorp:xxx. The correct setup puts the private repo under <repositories> and mirrors only central, or writes ,!your-private-repo.

28 / 154
Section
2.2 Offline mode and forced refresh
29 / 154

Once downloaded, dependencies are cached locally, so builds work offline. Three switches are worth memorizing:

30 / 154
  • -o (offline): force offline, using only the local repo — good for air-gapped machines or checking cache completeness
  • -U (update-snapshots): force a check for SNAPSHOT updates, essential for team work
  • -X (debug): print full debug logs; turn it on and you see exactly which URL Maven is requesting
31 / 154
Tip

when a dependency "was just published but cannot be resolved", try mvn -U clean package first. SNAPSHOT versions refresh only once a day by default, and -U forces an immediate pull.

32 / 154

That "once a day" is the only number here you can actually drag, and it is the most common invisible time lag in a team. Treat it as a knob: on the left you change the repository's <updatePolicy> (expressed in minutes), on the right you get the symptoms that setting produces in a real project:

33 / 154
Tuner
TunerHow often does Maven ask for a newer snapshot
0 minutes = never, 1440 = daily (Maven's default), 60 = interval:60. Start at the default 1440 to get a baseline, then drag both ways
<updatePolicy> · snapshot check interval
1440minutesNow 0 – 2880
daily (the default): it asks once a day
  • The first build of the day compares timestamps, the rest of the day reads the cache
  • Most cases of 'published yesterday, still not resolving today' stop right here
  • This is Maven's factory setting and a reasonable choice for most single-module projects
  • When debugging, ask first: what time did my cache last refresh?
Build speed80%
Chance of getting the new snapshot55%
Want fresh snapshots, shorten the interval; want stable builds, pin a release version — do not settle for a state that is both slow and uncertain.
34 / 154
Note

-U and <updatePolicy> are two entrances to the same thing — the flag applies to one build, the setting lives in settings.xml or the repository definition and applies forever. The mvn -U clean deploy you see in CI scripts is exactly a temporary override of daily.

35 / 154
Section
3. GAV coordinates and the repository system
36 / 154

Every artifact in Maven is uniquely located by a coordinate set, GAV for short:

37 / 154
Code
Codexml
<dependency>    <groupId>org.springframework</groupId>       <!-- organization / namespace -->    <artifactId>spring-context</artifactId>       <!-- module name -->    <version>6.1.0</version>                      <!-- version -->    <packaging>jar</packaging>                    <!-- packaging (optional, jar by default) --></dependency>
Notes
  • groupId: usually a reversed domain name, e.g. org.springframework
  • artifactId: the module name, e.g. spring-context
  • version: the version string; a trailing SNAPSHOT means work in progress
  • Together they map to a path in the repository: org/springframework/spring-context/6.1.0/spring-context-6.1.0.jar
38 / 154
Section
3.1 Local, remote and private repositories
39 / 154
Table
RepositoryLocationRoleMaintained by
LocalDisk, ~/.m2/repositoryCaches downloaded dependencies; checked first during a buildYou (Maven fills it)
CentralPublic repo.maven.apache.orgThe single official source for open-source librariesThe Maven community
PrivateCompany intranet (Nexus / Artifactory)Proxies the public repo and hosts internal artifactsCompany ops
40 / 154

Resolution order is: local repository → private repo (if configured) → central. Each hit back-fills the cache layer by layer: the private repo caches the public artifact, then your local repo caches the private one. The second build barely touches the network.

41 / 154
Key point

a private repo exists for two reasons — speed (an intranet proxy of the public repo, sharing one cache across the team) and isolation (internal artifacts never leave the intranet, external ones stay auditable). That is the whole answer to "why a private repo?" in an interview.

42 / 154
Section
4. The complete guide to dependency scope
43 / 154

The easiest field to misuse, and the one that plants the nastiest mines, is scope. It determines whether a dependency is visible when compiling main code, compiling tests, and at runtime.

44 / 154
xml
<dependency>    <groupId>org.projectlombok</groupId>    <artifactId>lombok</artifactId>    <version>1.18.30</version>    <scope>provided</scope>   <!-- compile-time only, never packaged --></dependency>
45 / 154

Here is the full six-scope table — worth bookmarking:

46 / 154
Table
scopeMain codeTestsPackaged at runtimeTypical use
compile (default)yesyesyesSpring, Jackson, ordinary deps
providedyesyesnoservlet-api, lombok (provided by container/JDK)
runtimenoyesyesJDBC drivers (no impl needed at compile time)
testnoyesnoJUnit, Mockito
systemyesyesnolocal-path jar (deprecated, avoid)
import———only for importing a BOM in dependencyManagement
47 / 154

This sandbox links all three dimensions live — switch the scope to see the difference:

48 / 154
Sandbox
Sandbox依赖范围沙盘
Result
Main code can use it: yes
Test code can use it: yes
Packaged at runtime: yes, into the final artifact
The default scope: visible for compile, test and runtime alike.
49 / 154
Warning

setting JUnit's scope to compile (or omitting it) ships your test framework into production; setting a JDBC driver to provided makes production throw ClassNotFoundException: com.mysql.cj.jdbc.Driver. Get scope wrong and it works 100% locally and breaks 100% in production.

50 / 154

That table has six rows, and the one thing everybody forgets while copying a pom is the scope itself. Do not copy — tick it out. Every checkbox in this generator emits the scope its dependency actually belongs to, and explains below why that one:

51 / 154
Generator
GeneratorTick them out and see which scope each dependency getspom.xml2 / 6
Start with Web alone (it has no scope, because compile is the default), then add MySQL, Lombok and Test: the output gains the three lines runtime, provided and test — those three are this section's table in real form
Output
<?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>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.4</version> <!-- 版本由 BOM 统管,子依赖不写 version -->
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>demo-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <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>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
Why each choice matters
parentInheriting 3.3.4 starter-parent means no spring-boot-starter-* needs a version; the moment someone adds an explicit version to one starter, that one wins — the most common source of dependency drift.
WebAnything that serves HTTP needs it: DispatcherServlet, embedded Tomcat and JSON mapping come inside this starter.
Testscope=test: @SpringBootTest, MockMvc and AssertJ live here; without it @Test is unresolved.
52 / 154
Section
5. Transitive dependencies and mediation
53 / 154

What you declare directly is a direct dependency; what it declares gets passed on to you as a transitive dependency. The trouble is that several paths can pull in different versions of the same library, and Maven must pick one — this is dependency mediation.

54 / 154
Animation
Animation · Dependency mediation
Animation · Dependency mediation
55 / 154
类比|Analogy

students with the same name must state their full ID. A class has three people called "张伟", so a roll call only works with "college - class - student number". Maven's groupId:artifactId:version is exactly that ID system. The trouble: when the same "college + name" (the same groupId:artifactId) shows up with two different numbers (1.0 and 1.1), the JVM only recognises the college-and-name part — so only one may stay. Which one, is the rule below.

56 / 154
Kernel lab
TeaVMHow the tree grows: nearest-wins and exclusions liveidle
Press all four buttons; the last one shows an exclusion pruning a whole subtree
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
57 / 154

The "only one may stay" clause bites hardest when both candidates sit at the same depth. Switch to "version conflict" and you can swap the order in which two dependencies are written in pom.xml and watch the winner flip — which is why "I added a dependency, touched no code, and everything broke":

58 / 154
Kernel lab
TeaVMTwo versions fighting for one slot: who wins?idle
Swap the declaration order and rerun — note that first-declared only matters at equal depth
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
59 / 154

For example, the project declares A and C:

60 / 154
Code
Codetext
my-app├── A:1.0│   └── commons:1.0      ← path: my-app → A → commons (depth 2)└── C:1.0    └── B:1.0        └── commons:1.1  ← path: my-app → C → B → commons (depth 3)
Notes
  • commons appears twice: 1.0 and 1.1
  • Rule one, "nearest wins": 1.0 is at depth 2 versus 1.1's depth 3, so 1.0 wins
  • If both paths have the same depth, rule two kicks in, "first declared wins": the dependency written earlier in pom.xml wins
61 / 154

That is the entire heart of Maven mediation. Understand it and you can predict the result of mvn dependency:tree before running it.

62 / 154
Section
5.1 Changing the outcome: exclusions and optional
63 / 154

To knock a transitive version out, use <exclusions>:

64 / 154
Code
Codexml
<dependency>    <groupId>com.example</groupId>    <artifactId>C</artifactId>    <version>1.0</version>    <exclusions>        <!-- exclude the commons that C brings in; use our own declared version -->        <exclusion>            <groupId>commons-io</groupId>            <artifactId>commons-io</artifactId>        </exclusion>    </exclusions></dependency>
Notes
  • An <exclusion> lists only groupId and artifactId, never a version (its meaning is "drop this whole subtree")
  • After excluding, declare the version you want directly, so that "my version is the one that counts"
65 / 154

Conversely, a library author who wants to stop one of its dependencies from propagating uses <optional>true</optional>:

66 / 154
Code
Codexml
<dependency>    <groupId>com.example</groupId>    <artifactId>redis-client</artifactId>    <version>2.0</version>    <optional>true</optional>   <!-- stays here, does not propagate downstream --></dependency>
Notes

Note: optional is the library author's tool; exclusions is the consumer's tool. The former lives in the library's pom, the latter in your project's pom — don't mix them up.

67 / 154
Section
6. Debugging dependency conflicts
68 / 154

Pick the wrong version and the code usually blows up at runtime, not compile time. Two commands are the main tools:

69 / 154
bash
# 1. Print the full dependency tree to find who dragged in the stale versionmvn dependency:tree# 2. Report declared-but-unused / overridden deps with cleanup suggestionsmvn dependency:analyze
70 / 154

A typical dependency:tree output, where indentation is path depth:

71 / 154
text
[INFO] com.example:my-app:jar:1.0-SNAPSHOT[INFO] +- org.springframework:spring-webmvc:jar:6.1.0:compile[INFO] \- com.fasterxml.jackson.core:jackson-databind:jar:2.16.0:compile[INFO]    \- com.fasterxml.jackson.core:jackson-core:jar:2.15.0:compile[INFO]       (version managed from 2.16.0)   # ← downgraded by some dependencyManagement
72 / 154

Here is a real NoSuchMethodError: you call a writeValueAsBytes overload that only Jackson 2.16 has, but the runtime classpath actually holds 2.15:

73 / 154
Code
Codetext
java.lang.NoSuchMethodError: 'byte[] com.fasterxml.jackson.databind.ObjectMapper.writeValueAsBytes(java.lang.Object)'    at com.example.api.OrderController.toJson(OrderController.java:42)
Notes
  • NoSuchMethodError / NoSuchMethodException are almost always "compiled against A, ran against B"
  • The first move is always mvn dependency:tree -Dincludes=com.fasterxml.jackson.core to see which version finally won
  • The right way to pin versions is to declare them centrally in the parent pom's <dependencyManagement> (see Section 8)

Trap: staring at the class name will not find the root cause — NoSuchMethodError means "this class has no such method", and a missing method usually means a version mismatch, not "a coding mistake". Before touching code, confirm the version with dependency:tree.

74 / 154

"Confirm the version first" is far too easy to turn into a slogan. Spread that dependency tree out as a single-step run: on the left, the candidates Maven collects; on the right, the depth table as it fills in and who brought whom. Step through it and watch step 4 — Maven picks the closest, not the newest:

75 / 154
Stepper
StepperWalking mediation with Maven: how 1.0 beat 1.11 / 6
Six steps down one tree. From step 3 on, keep your eyes on the depth table on the right — it is what decides the fight
Code under debug
1<dependency>A:1.0</dependency> # your pom has exactly two direct dependencies
2<dependency>C:1.0</dependency> # commons is never mentioned
3read A's pom → finds commons:1.0 # depth 2: my-app → A → commons
4read C's pom → finds B → commons:1.1 # depth 3: my-app → C → B → commons
5mediation: one survivor per groupId:artifactId
6the compile classpath gets commons:1.0; 1.1 is never even downloaded
Variables now
direct dependencies2
commons declarednowhere
candidates collected0
Call stack
1read pom.xml
1Mediation's input is not the version you believe in but this dependency graph. Your pom never mentions commons and it lands on the classpath anyway — that is a transitive dependency.
76 / 154
Section
7. The three lifecycles and their phases
77 / 154

Maven has three independent lifecycles, each made of ordered phases; running a later phase automatically runs the earlier ones:

78 / 154
Table
LifecycleMain phases (in order)Purpose
cleanpre-clean → clean → post-cleanRemove the previous build output
defaultvalidate → compile → test → package → verify → install → deployCompile, test, package, install, deploy
sitepre-site → site → post-site → site-deployGenerate the project documentation site
79 / 154

A phase itself is an empty shell; the work is done by plugin goals bound to it — Maven's single most important design:

80 / 154
Table
PhaseDefault bound goalOutput
compilemaven-compiler-plugin:compile.class files under target/classes
testmaven-surefire-plugin:testtest reports
packagemaven-jar-plugin:jartarget/*.jar
installmaven-install-plugin:installinstalled into the local repo
deploymaven-deploy-plugin:deploypushed to the private/remote repo
81 / 154
Diagram
Figure · Three lifecycles, and the goals that do the work
Figure · Three lifecycles, and the goals that do the work
82 / 154

Those two tables add up to eleven rows, but reading and knowing are different things: you have to click. This diagram splits one mvn package into five boxes — each one says who is doing the work and what breaks if you skip it:

83 / 154
Diagram
FlowOne mvn clean package, box by box (who does the work)1 / 5
Click from ① to ⑤; box ② is the one people get wrong — clean and package belong to two unrelated chains
→
→
→
→
① The command is split into phases
mvn clean package is not one action but two: walk the clean chain, then walk the default chain as far as package. The lifecycles are independent; writing them on one line only makes them sequential.
All clearIn one line: phases are a duty roster, goals are the workers; you call the roster, the workers do the job.
84 / 154

Those two facts — "a later phase drags the earlier ones" and "calling a goal directly skips steps" — are the parts Maven interviews get wrong most often. This lab is built for exactly that; cycle through the four modes in order:

85 / 154
Kernel lab
TeaVMThree lifecycles: who drags whom, who skips whatidle
Start with the three lifecycles to see that clean and default never touch, then open default bindings to see which goal hangs on which phase
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
86 / 154

A common command is really a combination of phases; unpack it one flag at a time:

87 / 154
Code
Codebash
mvn clean package -DskipTests -U -X
Notes
  • clean: run the clean lifecycle first, deleting target
  • package: run the default lifecycle up to package, producing the jar/war
  • -DskipTests: skip test execution (test code is still compiled); -Dmaven.test.skip=true skips compiling test code too
  • -U: force snapshot updates; -X: print debug logs

Tip: -DskipTests and -Dmaven.test.skip=true are often used interchangeably, but the former compiles without running, the latter neither compiles nor runs. On CI, pick the latter to save time and the former to keep compile verification.

88 / 154
Section
8. Multi-module builds: aggregation and inheritance
89 / 154

As a project grows, a single pom becomes thousands of lines. Maven splits it with two orthogonal ideas: aggregation builds everything together, inheritance shares configuration.

90 / 154
text
spring-demo/                 ← parent POM (a parent pom always uses packaging=pom)├── pom.xml├── demo-common/             ← shared module│   └── pom.xml├── demo-service/            ← business module, depends on common│   └── pom.xml└── demo-web/                ← web module, depends on service    └── pom.xml
91 / 154

The parent POM does both aggregation and inheritance:

92 / 154
xml
<project>    <groupId>com.example</groupId>    <artifactId>spring-demo</artifactId>    <version>1.0.0</version>    <packaging>pom</packaging>   <!-- a parent POM must be pom, never jar -->    <!-- Aggregation: list the modules; mvn install builds them in dependency order -->    <modules>        <module>demo-common</module>        <module>demo-service</module>        <module>demo-web</module>    </modules>    <!-- Inheritance: unify versions here; children declare only groupId and artifactId -->    <dependencyManagement>        <dependencies>            <dependency>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-dependencies</artifactId>                <version>3.2.0</version>                <type>pom</type>                <scope>import</scope>   <!-- import pulls in a BOM -->            </dependency>        </dependencies>    </dependencyManagement></project>
93 / 154

A child module simply inherits the parent and declares its dependencies (versions come from the parent):

94 / 154
Code
Codexml
<project>    <parent>        <groupId>com.example</groupId>        <artifactId>spring-demo</artifactId>        <version>1.0.0</version>    </parent>    <artifactId>demo-service</artifactId>   <!-- groupId / version inherited -->    <dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>            <!-- no version: decided by the parent's dependencyManagement -->        </dependency>    </dependencies></project>
Notes
  • <modules> is aggregation: one mvn install builds every module, and Maven topologically sorts them for you
  • <dependencyManagement> is inheritance: it manages versions only and does not actually pull anything in; children omit the version
  • scope=import with type=pom: lifts an entire BOM (such as Spring Boot's dependency list) into your version-management center

Key point: the difference between dependencyManagement and dependencies is a frequent interview question — the former is a "price list" that only takes effect when a child declares the dependency; the latter is "placing the order" and pulls the dependency immediately.

95 / 154

Aggregation is often mistaken for "build in directory order"; in reality Maven sorts by the dependency graph. This animation splits one mvn install into six frames — look at frames 5 and 6: downstream modules read what was just installed into the local repository, so "I changed common but web still behaves the old way" is never cache superstition. It simply never read your new class:

96 / 154
Animation
Animation · Multi-module: the build order of one mvn install
Animation · Multi-module: the build order of one mvn install
97 / 154
Section
9. Ten frequent errors and traps
98 / 154
Table
SymptomRoot causeFix
Could not resolve dependencies or download timeoutsSlow network or no mirror configuredConfigure the Aliyun mirror; retry with mvn -U
Could not find artifact com.corp:xxxmirrorOf set to *, hijacking the private repoMirror only central; configure the private repo under <repositories>
NoSuchMethodError / NoClassDefFoundErrorMediation picked a different versionLocate with mvn dependency:tree; pin with dependencyManagement
Runtime ClassNotFoundException: driver classJDBC driver scope set to provided/testChange to runtime (or default compile)
Test framework shipped to productionJUnit missing test scopeAdd <scope>test</scope> explicitly
Works locally, CI reports JAVA_HOME invalidDifferent JDKs across machinesPin with toolchains or maven.compiler.release
SNAPSHOT changes not picked upSnapshots refresh once a day by defaultForce a refresh with mvn -U
Code changed but old behavior persistsStale target not cleanedRun mvn clean before building
Downstream misses a module changeNot installed into the local repoRun mvn clean install at the root
duplicate dependency warningThe same module declared twiceMerge duplicates; clean up with dependency:analyze
99 / 154
Key point

sorting errors into two buckets is far more efficient — "cannot fetch" (network, mirror, private repo) and "got it wrong" (mediation, scope, stale cache). The first points you to settings.xml, the second to dependency:tree.

100 / 154
Section
10. Decision card and summary
101 / 154
Decision
Decisiona colleague has a "legacy" dependency that can only be brought in via a local file path, so he wrote `<scope>system</scope>` with `<systemPath>`. His machine happens to have that jar, so everything works — but after deployment the jar is missing from the package. What is the right move?
102 / 154
Section
11. Hands-on labs: scopes and packaging
103 / 154

Section 7 listed the three lifecycles, but "phases" stay abstract. This animation walks one dependency from being written into pom.xml all the way to sitting on the runtime classpath. Watch step 4 (mediation) and step 6 (scope decides whether it ships) — they correspond to Sections 5 and 4:

104 / 154
Animation
Animation · The life of a dependency
Animation · The life of a dependency
105 / 154

The second lab here answers "what actually changes when I edit a scope". Press the four buttons and watch the same jar's visibility across "compiling main code / compiling tests / packaging", including why a JDBC driver marked provided looks perfectly fine locally and collapses the moment it reaches production:

106 / 154
Kernel lab
TeaVMScopes decide visibility: one edit, three places reactidle
Start with compile as the baseline, then flip through provided / runtime / test
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
107 / 154

The third lab answers a question every beginner asks: why does the plain jar produced by mvn package refuse to run with java -jar. A fatjar puts dependencies under BOOT-INF/lib and reads them with a custom class loader — which ties straight back to the classpath lesson in article #1:

108 / 154
Kernel lab
TeaVMInside an executable jar: why the thin jar cannot runidle
Look at layout first to learn the folders, then loader to see how BOOT-INF/lib jars get loaded
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
109 / 154

The fourth lab goes back to the two most treacherous rules of Section 7. Calling a goal directly skips every earlier phase (so you package stale classes), and several goals bound to one phase queue up in the order they are written in the pom. Press all four modes, but especially the third:

110 / 154
Kernel lab
TeaVMCalling a phase or a goal directly: which steps get skippedidle
Compare the execution list of mvn package with mvn jar:jar — the lines missing from the second one are the accident scene
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
111 / 154
Kernel lab
TeaVMSeveral goals on one phase: who runs firstidle
Watch how the order of <plugin> blocks in the pom decides execution order; this explains the classic 'I added a plugin and the build changed'
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
112 / 154

One last cross-article trap: the JDK Maven uses is not necessarily the java on your terminal. The Java version printed by mvn -v is the one your build really runs on, and this mode shows how it gets picked on a machine with several JDKs:

113 / 154
Kernel lab
TeaVMWhich JDK does Maven pickidle
Then reread the opening of Section 2: when mvn -v and java -version disagree, you now know where to look
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
114 / 154

Enough buttons — time to type. This console is wired to the same in-browser kernel and every reply is computed there. Start with lab mvnlife phases, then work down the list:

115 / 154
Console
116 / 154
Trap

run lab mvnlife bind immediately followed by lab mvnlife direct — the lines the second one lacks are exactly the goals listed by the first. Once that contrast clicks, the question "why did it run my tests when I only said package" disappears for good.

117 / 154
Section
12. Sandbox: which version should win a conflict
118 / 154

You already know the rules (nearest wins, then first declared). What is hard is what to do after the verdict. Pick the shape of the conflict on the left, pick your remedy on the right, and the panel shows the resulting dependency:tree plus a risk rating:

119 / 154
Sandbox
SandboxWhich version wins a dependency conflict
Result
No matching result
120 / 154
Tip

the least intuitive cell is "BOM overrides + declare directly" — you write a version, believe it won, and dependencyManagement silently rewrites it. There is only one way to be sure: run mvn dependency:tree and look for the (version managed from ...) note.

121 / 154
Section
13. Common errors, searchable by exact wording
122 / 154
Table
Error text (excerpt)Real cause30-second fixDig deeper in
'mvn' 不是内部或外部命令 / 'mvn' is not recognized as an internal or external commandMaven's bin is not on PATH, or you edited PATH without reopening the terminalAdd %MAVEN_HOME%\bin to PATH, open a new window, verify with mvn -v#1 environment variables
The JAVA_HOME environment variable is not defined correctlyMaven found no JDK (this article is about Maven, but Maven still needs JAVA_HOME)Point it at the JDK root, never at \bin#1 Section 3
Could not resolve dependencies for project ...: Failed to collect dependencies at com.example:A:jar:1.0That coordinate does not exist in any reachable repo, or the network/mirror is brokenSearch the coordinate on the repository website first, then retry with mvn -USection 2
Could not find artifact com.yourcorp:xxx:jar:1.0 in aliyun-centralmirrorOf set to *, hijacking even private-repo trafficMirror only central, or write *,!your-private-repoSection 2.1
java.lang.NoSuchMethodError: 'byte[] com.fasterxml.jackson.databind.ObjectMapper.writeValueAsBytes(java.lang.Object)'Compiled against one version, ran against another — textbook mediation accidentmvn dependency:tree -Dincludes=com.fasterxml.jackson.coreSection 6
java.lang.NoClassDefFoundError: com/mysql/cj/jdbc/Driver or a runtime ClassNotFoundExceptionThe JDBC driver was scoped provided/test, so it never joined the runtime classpathChange it to runtime (or leave the default compile)Section 4 scope sandbox
[WARNING] The POM for xxx is missing, no dependency information available, repeated failures for one artifactA previous failed download left .lastUpdated markers in the local repoDelete that artifact folder in ~/.m2/repository, or retry with -USection 2.2
duplicate dependency warning / the same GA appearing twiceThe same dependency declared twice across parent and child pomsClean up with dependency:analyze; centralise in dependencyManagementSection 8
123 / 154
Trap

on an air-gapped machine the confusing one is "it is cached, yet still reported missing". The failed download left *.lastUpdated files behind, and Maven then believes it already tried that coordinate and will not go online again. Delete the artifact directory and re-resolve, or force it with -U.

124 / 154

That NoSuchMethodError row is the chapter's signature "Maven did nothing wrong and you still broke" scene. The stack below is real — do not read the answer, just click the frame you believe is guilty:

125 / 154
Triage
Error triageNoSuchMethodError: ObjectMapper.writeValueAsBytes(Object)
Compiled against 2.16, running on 2.15 — and the call site is in your code

Everything is fine locally. The fatjar CI produced goes to production and the first export endpoint returns 500 with two lines of log.

Exception in thread "http-nio-8080-exec-5" java.lang.NoSuchMethodError: 'byte[] com.fasterxml.jackson.databind.ObjectMapper.writeValueAsBytes(java.lang.Object)'
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:897)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:255)
at com.example.api.OrderController.list(OrderController.java:29)
at com.example.api.OrderController.toJson(OrderController.java:42)
at java.base/java.lang.invoke.DirectMethodHandle$Holder.invokeVirtual(DirectMethodHandle$Holder.java)
Click the frame you blame — guessing is allowed
No pressure: guess the exception first, then which line actually made the call.
126 / 154
Section
14. Check yourself
127 / 154
Quiz
Check yourselfA brings in commons:1.0 at depth 2, B brings commons:1.1 at depth 3, and nowhere in your pom do you declare commons directly. Which version ends up on the classpath?
Pick one — you get feedback right away
128 / 154
Quiz
Check yourselfWhat is the difference between `mvn clean package -DskipTests` and `mvn clean package -Dmaven.test.skip=true`?
Pick one — you get feedback right away
129 / 154
Section
15. Practice in three levels
130 / 154
Section
Level 1 · Follow along
131 / 154

Goal: manufacture a version conflict deliberately, then resolve it three different ways and feel the difference.

132 / 154
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>dep-lab</artifactId>    <version>1.0.0</version>    <properties>        <maven.compiler.release>17</maven.compiler.release>        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>    </properties>    <dependencies>        <!-- drags in commons-logging 1.2 -->        <dependency>            <groupId>commons-cli</groupId>            <artifactId>commons-cli</artifactId>            <version>1.5.0</version>        </dependency>        <!-- drags in commons-logging 1.0 -->        <dependency>            <groupId>commons-attributes</groupId>            <artifactId>commons-attributes-compiler</artifactId>            <version>2.1</version>        </dependency>    </dependencies></project>
133 / 154

Run it and record the output:

134 / 154
bash
mvn -q dependency:tree -Dincludes=commons-logging
135 / 154

Expected shape (indentation is depth; the loser is marked omitted):

136 / 154
text
[INFO] com.example:dep-lab:jar:1.0.0[INFO] +- commons-cli:commons-cli:jar:1.5.0:compile[INFO] |  \- commons-logging:commons-logging:jar:1.2:compile[INFO] \- commons-attributes:commons-attributes-compiler:jar:2.1:compile[INFO]    \- (commons-logging:commons-logging:jar:1.0:compile - omitted for duplicate)
137 / 154

Now add code using an API that only 1.2 has, and observe what breaks at compile time versus runtime. Then try each remedy in turn: declare the version directly, add an <exclusion> to the first dependency, pin it in dependencyManagement — verifying every attempt with the same dependency:tree command.

138 / 154

Done when: you can explain in your own words what each of the three fixes actually changed, and why the third one suits a team.

139 / 154
Section
Level 2 · Variants
140 / 154
  1. Swap the declaration order of the two dependencies and arrange them at equal depth (add one more intermediate dependency). You will observe the winning version flipping with the order — the fragility of letting line order decide runtime behaviour.
  2. Change commons-logging's scope to provided, run mvn package, then java -jar. You will observe NoClassDefFoundError at startup while the build stays completely green.
  3. In ~/.m2/settings.xml, change mirrorOf from central to * and build a project that needs a private repo. You will observe Could not find artifact ... (without a private repo, reproduce the request-path change with an obscure Central-only coordinate).
141 / 154

Tip: after variant 1, revisit the "equal depth" column of the Section 12 sandbox — the conclusion becomes obvious.

142 / 154
Section
Level 3 · Build something
143 / 154

Create a three-module project that exercises every concept in this article:

144 / 154
text
multi-lab/            <- parent pom: packaging=pom + <modules> + <dependencyManagement>├── lab-common/       <- one utility class, no third-party deps├── lab-service/      <- depends on lab-common, pulls a library with a conflicting transitive dep└── lab-web/          <- depends on lab-service, packaged as an executable fatjar
145 / 154

Requirements: ① the parent manages versions in dependencyManagement and children never write <version>; ② at least one child uses <exclusions> with a comment saying why; ③ mvn clean install at the root passes; ④ mvn dependency:tree contains no omitted for conflict; ⑤ lab-web configures spring-boot-maven-plugin and produces a jar that java -jar runs directly.

146 / 154

Acceptance checklist: on a fresh machine (or after deleting the entire ~/.m2/repository), a single mvn clean install rebuilds everything identically — that, and only that, is a reproducible build.

147 / 154
Section
16. Self-check
148 / 154
Self-check

what does each of groupId:artifactId:version denote, and why can these three strings replace a local jar path?

149 / 154
Self-check

in what order do the two mediation rules apply? Does Maven ever prefer the newer version?

150 / 154
Self-check

among compile / provided / runtime / test, which two most often cause "works locally, dies in production", and at which moment does each fail?

151 / 154
Self-check

what separates dependencyManagement from dependencies, and why is the first called a "price list"?

152 / 154
Self-check

which lifecycle phase does each word of mvn clean package -DskipTests trigger?

153 / 154
Mnemonic

coordinates identify, repositories supply, nearest wins, scope gates, phases bind goals — GAV is the ID card, local then private then central are the shelves, the shortest path wins, scope decides where it is visible, and every piece of work is a plugin goal bound to a phase.

154 / 154
Summary

going from manual javac to Maven, what you gain is not "shorter commands" but a "declarative, reproducible build". Every concept in this article — coordinates, repositories, scope, mediation, lifecycles, aggregation and inheritance — serves one goal: on any machine, mvn clean package produces exactly the same result.