0

I'm trying to use log4j in my java core console application. In the documentation there is sample code on how to add Gradle dependency for log4j:

https://logging.apache.org/log4j/2.x/maven-artifacts.html

dependencies {
  compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.14.1'
  compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.14.1'
}

I pasted it to my build.gradle file and wrote sample code for running the code.

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Program {
    private static final Logger logger = LogManager.getLogger("HelloWorld");

    public static void main(String[] args) {
        logger.error("Just a test error entry");
    }
}

along with that I also added manifest in order to run the code from the console.

jar {
    manifest {
        attributes "Main-Class": "Program"
    }
}

If I build it with Gradle - everything is built successfully. But if I try to run java -jar from my build directory then I got

Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/logging/log4j/LogManager
        at Program.<clinit>(Program.java:5)
Caused by: java.lang.ClassNotFoundException: org.apache.logging.log4j.LogManager
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:636)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:182)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:519)
        ... 1 more

I found 1 workaround: modify to jar{} section:

jar {
    manifest {
        attributes "Main-Class": "Program"
    }

    from {
        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

and it started working, I even try to zip .jar file and saw that there is log4j dependency added.

But what is I don't want transitive dependency on log4j? If I set implementation group instead of compile group it doesn't work again. Is there any complete example of using log4j?

On top of that, one thing to point, while it is not working with java -jar <.jar>, for some reason it still works in Intellij idea. Could someone explain me why?

andrii_bui
  • 13
  • 1
  • 5
  • If a JAR file requires external dependencies you need to use the `-cp` argument to the `java` executable (or the `CLASSPATH` environment variable). Alternatively some implementations of Java can add additional JARs to the classpath by reading the manifest (cf. [this question](https://stackoverflow.com/q/22659463/11748454)) – Piotr P. Karwasz Aug 06 '21 at 09:04
  • @PiotrP.Karwasz, could you please share any documentation about this? I've read gradle docs, but didn't find that we should specify classpath, etc for using `implementation` – andrii_bui Aug 11 '21 at 12:36
  • see [What is a classpath and how do I set it?](https://stackoverflow.com/q/2396493/11748454) When Intellij runs a project, it does something similar to setting a classpath. – Piotr P. Karwasz Aug 11 '21 at 18:43
  • @PiotrP.Karwasz, sorry, I cannot find any documentation in Gradle about it. I saw the link, but what classpath should I tell my `build.gradle`? Because in my built .jar there are no log4j jars if I put `implementation`. Only if I put `compile`. I've read that the difference between them in transitive dependencies, but probably there is something else. And the thing which upsets me - is there is no documentation about it. – andrii_bui Aug 11 '21 at 19:36

1 Answers1

0

The problem you are encountering is caused by a problem in the application's classpath, when you launch the application (cf. What is a classpath and how do I set it?).

Gradle has many options to help you with setting the classpath right. Since you don't mention any Gradle version, I'll use version 7.1 in the examples. Assume you have this build.gradle file in a project named appName:

plugins {
  id 'application'
}

dependencies {
  implementation group: 'org.apache.logging.log4j', name: 'log4j-api',  version: '2.14.1'
  runtimeOnly    group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.14.1'
}

application {
  mainClass = 'com.example.App'
}

(log4j-core is not necessary to compile your code, so it is in the runtimeOnly dependency configuration, cf. dependency configurations)

Manually setting the classpath

If you generate a plain JAR file (execute the jar task and look in build/libs) you can execute the application with:

java -cp log4j-api-2.14.1.jar:log4j-core-2.14.1.jar:appName.jar com.example.App

(I assume all jars are in the current directory; the path separator is system specific, on Windows it is ;)

This becomes easily difficult to get right, therefore the application Gradle plugin generates a shell and a batch script to help you: execute the installDist task and look into build/install/appName (or you can use the distZip or distTar tasks for a compressed version of this folder). Setting up the correct command is reduced to calling:

bin/appName

Hardcoding the classpath in the Manifest

You can also hardcode the dependencies needed in the JARs manifest file. In Gradle you can do it with:

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

to obtain a manifest file with all the information needed to start the application:

Main-Class: pl.copernik.gradle.App
Class-Path: log4j-core-2.14.1.jar log4j-api-2.14.1.jar

Notice the usage of runtimeClasspath, which is a configuration that contains both the runtimeOnly and implementation dependency configurations needed to start the application.

If the dependency jars are in the same folder as appName.jar, you can run the application with:

java -jar appName.jar

Spring Boot loader

If you want to have all dependencies in a single JAR, you can use the Spring Boot Gradle plugin instead of the application plugin:

plugins {
  id 'java'
  id 'org.springframework.boot' version '2.5.3'
}

The bootJar task will produce an appName.jar resembling in its structure the appName.zip file produced by the distZip file. However the shell/batch scripts are replaced with Java code, so you can call:

java -jar appName.jar

Fat Jar

What you did in your question with:

jar {
    manifest {
        attributes "Main-Class": "com.example.App"
    }
    from {
        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

is usually called fat JAR. Basically it is produced by unzipping your jar file and all dependencies into a single folder and zipping it back together.

This works, but has some disadvantages: e.g. you might lose licence information on the dependencies (which might be a licence violation) and you will not be able to replace the dependencies with new versions without recompiling. See this question for an example of an absurdly large fat jar.

Piotr P. Karwasz
  • 12,857
  • 3
  • 20
  • 43