🛠️ Testing, Tools & Ecosystem · Advanced

Dependencies & versions in Java

Transitive dependencies, conflicts, semantic versioning, vulnerability scanning.

🧩 The mysteryDecember 2021: teams worldwide rushed to patch Log4Shell. Many were shocked to learn they used log4j at all; it appeared nowhere in their build files. How?

You ship a whole tree

Each dependency brings its own dependencies, and those bring theirs. These transitive dependencies are part of what you ship. See the full tree with **mvn dependency:tree or gradle dependencies**.

When versions collide

Two paths may ask for different versions of the same library; the build must pick one. Maven: nearest wins, the declaration closest to your project in the tree. Gradle: highest wins, it upgrades to the highest requested version unless you add constraints.

🔮 Predict it

Maven picks one

Your app depends on A → C 1.0 and on B → D → C 2.0. Which C does Maven put on the classpath?

  1. C 1.0
  2. C 2.0
  3. Both versions
Show the answer

C 1.0: it's nearer (depth 2 beats depth 3), even though it's older. To get C 2.0, declare it directly or pin it in **dependencyManagement**. Gradle would pick 2.0.

Semantic versioning

SemVer is MAJOR.MINOR.PATCH. 2.4.1 → 3.0.0: breaking changes allowed. 2.4.1 → 2.5.0: new features, backward compatible. 2.4.1 → 2.4.2: bug fixes only. It's a promise from the author that helps you judge upgrade risk; your tests still have the last word.

⚠️ The trap

NoSuchMethodError at runtime

Everything compiles, but a request fails with **NoSuchMethodError inside a Jackson class. Two libraries need different Jackson versions, and the one that won lacks that method: code compiled against one version is running with another. Check the tree and align versions, for example with a BOM**.

Keeping dependencies safe

✗ Open range
<version>[1.0,)</version>

Always 'latest': builds aren't reproducible and surprises arrive silently.

✓ Pinned + scanned
<version>2.17.1</version>
<!-- + Dependabot / OWASP
     Dependency-Check in CI -->

Exact versions, automated vulnerability scans and regular small upgrades.

💼 In the real world

Log4Shell, transitively

A scanner flags log4j-core with Log4Shell (CVE-2021-44228), but your build never mentions log4j. It arrived transitively through another dependency. mvn dependency:tree shows which library pulled it in, so you know what to upgrade or override.

Key takeaways

  1. Transitive dependencies are part of what you ship
  2. Maven: nearest wins; Gradle: highest wins
  3. SemVer MAJOR.MINOR.PATCH: MAJOR may break
  4. Inspect with mvn dependency:tree / gradle dependencies
🤯 Did you know?

Log4Shell received the maximum CVSS severity score of 10.0.

Practice questions

Your app depends on A -> C 1.0 and on B -> D -> C 2.0. Which C does Maven put on the classpath?

  1. C 2.0, because the highest version wins
  2. C 1.0, because the nearest declaration wins
  3. Both versions
  4. None: the build fails
Check your answer

C 1.0, because the nearest declaration wins. Maven's 'nearest wins' picks the version closest to your project in the tree (depth 2 beats depth 3), even if it is older.

A scanner flags log4j-core with Log4Shell (CVE-2021-44228), but your build file never mentions log4j. How is that possible?

  1. Scanners guess at random
  2. It arrives transitively through another dependency; inspect the dependency tree
  3. The JDK bundles log4j-core
  4. It's in your IDE, not your app
Check your answer

It arrives transitively through another dependency; inspect the dependency tree. Most of your shipped code comes in transitively. mvn dependency:tree or gradle dependencies shows which library pulled it in.

Dependencies resolved, code compiled. Now how do you ship it? Next: JARs, executable JARs, jlink and jpackage.