rrrrrrrrrrrrrrrr
rrrrrrrrrrrrrrrr

Reputation: 342

Java doesn't load classes from manifest-defined classpath

I'm using Gradle to build a project. This is my build.gradle:

plugins { id 'application' }

group 'my.group'
version '1.0'

sourceCompatibility = 1.11

repositories { mavenCentral() }

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
    implementation group: 'org.xerial', name: 'sqlite-jdbc', version: '3.7.2'
}

mainClassName = 'my.group.App'

jar {
    manifest {
        attributes(
            'Main-Class': mainClassName,
            'Class-Path': configurations.runtimeClasspath.files.collect { it.name }.join(' ')
        )
    }

    from configurations.runtimeClasspath
    into ''
}

This correctly generates a jar file with the following META-INF/MANIFEST.MF:

Manifest-Version: 1.0
Main-Class: my.group.App
Class-Path: sqlite-jdbc-3.7.2.jar
[newline]

And in the root of this jar file, there is indeed the sqlite-jdbc-3.7.2.jar file referenced in the manifest; I manually verified that inside this jar file there is a class called org.sqlite.JDBC.

However, running the generated jar with java -jar jarfile.jar results in:

Exception in thread "main" java.lang.ExceptionInInitializerError
        at my.group.LEManagerSqlite.<init>(LEManagerSqlite.java:64)
        at my.group.LEManager.createLEManager(LEManager.java:80)
        at my.group.GuiFrame.<init>(GuiFrame.java:60)
        at my.group.App.openFromFile(App.java:23)
        at my.group.App.main(App.java:19)
Caused by: java.lang.RuntimeException: java.lang.ClassNotFoundException: org.sqlite.JDBC
        at my.group.SqliteConnectionManager.<clinit>(SqliteConnectionManager.java:17)
        ... 5 more
Caused by: java.lang.ClassNotFoundException: org.sqlite.JDBC
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
        at java.base/java.lang.Class.forName0(Native Method)
        at java.base/java.lang.Class.forName(Class.java:315)
        at my.group.SqliteConnectionManager.<clinit>(SqliteConnectionManager.java:14)
        ... 5 more

For reference, SqliteConnectionManager has a static initializer which loads org.sqlite.JDBC, which is what is referenced as SqliteConnectionManager.java:14 in the stack trace:

Class.forName("org.sqlite.JDBC");

I tested this with OpenJDK 11 and OpenJ9 11, with identical results. I conclude that I'm doing something wrong, but I cannot understand what.

Upvotes: 0

Views: 1078

Answers (1)

rrrrrrrrrrrrrrrr
rrrrrrrrrrrrrrrr

Reputation: 342

I found out what I was doing wrong: the documentation clearly says (doh!) that the Class-Path directive looks for additional classpath resources in the local filesystem or the network.

Loading additional classes from jar files included in the main jar file is not supported by the JVM, and thus requires custom loading code. A simpler alternative is to unpack the additional classes from the jar files and include them directly. A jar file built this way is referred to as a "fat jar" (or "uber jar").

There is a Gradle plugin called Shadow which can build such jars without any further configuration.

Upvotes: 1

Related Questions