There is a big difference between runtime and compile time when it comes down to such things.
compiling code (such as mvn package
), and writing code (as in, what IntelliJ is doing to ensure that your editing experience is nice; auto-complete dialogs for all the libraries you use, errors if you try to invoke non-existent methods, etcetera) are one thing (let's call it 'write time').
Running your code, as with java -jar xxx.jar
is something completely different.
maven and your dependency list is a write time thing. Thus, maven and intellij know where to look for your dependencies, but when you run java -jar xxx.jar
, that does not know where to look, and thus, your dependencies aren't found, and thus, NoClassDefFoundError
occurs.
That's because that jar file that maven makes just contains your code, it is completely disconnected from your maven file (your maven stuff is not looked up when you run your code at all), and it does not contain your dependencies.
You'd have to ship them separately. There are 3 solutions to this problem:
- Preferred solution: jar files contain a so-called manifest, which tells the JVM for example what the name of the main class is within that jar file. It can also contain the class-path (this is in fact the only class-path checked when using
java -jar
to run jar files; you can't specify a -classpath
parameter). This lets you deploy your app such that your app installation looks like:
/usr/local/projects/YourApp/main-app.jar
/usr/local/projects/YourApp/dep/guava.jar
/usr/local/projects/YourApp/dep/mysql-jdbc-connector.jar
/usr/local/projects/YourApp/dep/jdbi.jar
etcetera, and to run this application, just run main-app.jar
. This requires the manifest of main-app.jar to contain the entry:
Class-Path: dep/guava.jar dep/mysql-jdbc-connector.jar dep/jdbi.jar
When running the jar, the Class-Path
entry is split on spaces, and then each entry is looked up relative to the directory that contains main-app.jar
. By shipping the jars separately, it's easy to separately update them or replace them, and deploying a new version is much faster (you just ship the jar(s) that were changed, not all of them. Many apps have hundreds of MBs of deps, whereas their own jar is a few MB at most, makes a big difference for example when pushing deps from your dev machine to the test server!)
This leads to the question: How do you make maven put that Class-Path entry in the output jar file's manifest? The maven-jar-plugin
can do the job - see this answer for more details.
- Shade in your deps (also called striping, or fatjar, or bigjar)
This is the notion of taking all your deps, rewriting their name to avoid version conflicts, and then making one humongous jar file that contains everything. It has the considerable downside of being a much slower process, especially if you need to push this out to another system for testing. Use the shade plugin to do so.
java -jar x.jar
cannot work unless the jar file either has a Class-Path entry in its manifest, or contains every dep it needs. However, you can also run your java code like so: java -cp main-app.jar:dep/guava.jar:dep/jdbi.jar:/dep/other-deps-here com.foo.YourMainApp
. This is.. not convenient, but you could presumably write a shell script or some such. This isn't a very java-like solution, I don't recommend it.