Why do jar dependencies keep biting the hand that feeds them?
If you build Java apps for a living, you have met the broken classpath at 6 pm on a Friday. You drop a jar in a lib folder, Ant compiles, the app starts, then boom at runtime a missing class from yet another jar shows up. You chase that jar, then the next, then you ship a fat war with duplicates and mystery versions. It works on my machine becomes a lifestyle. There has to be a better way to say what we need and let the tool fetch it without drama.
I have the scars. You probably do too.
Enter Maven. Not as a silver bullet but as a boring friend who brings order. You write a pom.xml that declares groupId, artifactId, version, and Maven figures out transitive dependencies. You ask for Spring and it also gets the jars Spring needs. It uses a local cache in your home folder and a shared central repository. The big win is that the rules are written down so the build server can do the same thing you do.
It can also surprise you by pulling in the world if you are not specific.
My first rule is simple. Pin versions. Version ranges look fancy but they move under your feet. A minimal POM that stays put looks like this:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.demo</groupId>
<artifactId>catalog-service</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>2.0.4</version>
</dependency>
</dependencies>
</project>Pin versions. Future you will thank you.
Next rule. Control the tree. Transitive is helpful until a library drags a logging stack you do not want. Use exclusions to keep your house quiet and pick your logger or XML parser on purpose.
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate</artifactId>
<version>3.2.2.ga</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>Say no early. The tree stays clean.
Many of us build behind a proxy or we cache jars in house. Maven plays nice with both. Set your proxy and mirrors in settings.xml. If you run Apache Archiva or a simple file share mirror, point Maven at it. Your builds speed up and keep working when the internet blinks or Central gets slow.
<settings>
<proxies>
<proxy>
<id>corp-proxy</id>
<active>true</active>
<protocol>http</protocol>
<host>proxy.company.local</host>
<port>3128</port>
</proxy>
</proxies>
<mirrors>
<mirror>
<id>internal</id>
<name>Internal Mirror</name>
<url>http://repo.company.local/maven2</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>
</settings>Central is nice. Your build server behind the firewall is nicer.
Scopes are your friends. Use test for JUnit, provided for Servlet API when a container gives it to you, and runtime when you only need a jar at runtime. This keeps your compile classpath lean and your war files sane.
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.4</version>
<scope>provided</scope>
</dependency>Your war should not carry what the container already has.
When things feel weird, ask Maven what it thinks. The dependency plugin prints the tree and shows where a jar came from and which version won a conflict. The nearest one in the path wins, which is a simple rule once you see it.
mvn -q dependency:tree
mvn dependency:purge-local-repository
mvn -o package # offline after you cacheKeep trees small and boring.
We all like tools that fade into the background. Maven can do that if we respect a few basics. Be explicit about versions. Exclude what you do not want. Use scopes to match reality. Point Maven at a local cache or mirror and make the build server use the same settings. Then your commit is not a surprise party for the next person who types mvn package.
Dependency management should feel boring and repeatable, not heroic.