1

I have a maven project where in I included a jar that I created using gradle as a pom dependency. In that included Jar's code, I am referencing log4j logmanager. When I try to access a method in the external jar, it throws java.lang.NoClassDefFoundError on logmanager that the class inside the exernal jar is referring to.

build.gradle for exernal jar is:

plugins {
    id 'java'
}

group 'com.somecompany.somethingelse'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {

    implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.13.0'
    implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.13.0'

}

I build the jar using gradle clean assemble

I install this jar locally in to .m2 using mvn install:install-file and then have a dependency of it in the pom for the consuming app.

I am not really sure what is going on here.

External Jar class code

package com.company.something;

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

public abstract class MyClass{

     private static Logger logger = LogManager.getLogger();

     public static String myMethod(String someInput){
         logger.info("entered myMethod");
         ......some code goes here.....

     }

}

Jar Consuming class code

import com.company.something.MyClass;

public class consumingClass{

   public String consumingMethod(){
      MyClass.myMethod("someinput");
      return "something";
    }
}
Ray S
  • 569
  • 1
  • 8
  • 18
  • Could you post a simple Java code example which, together with the Gradle build script you already provided, would demonstrate the issue you're having? – ysakhno Feb 06 '20 at 20:49
  • java code from the consuming app you mean ? – Ray S Feb 06 '20 at 20:52
  • Well, actually it should better be both (the library and the consuming application). Also please show the Gradle build script of the consuming application too. BTW, you do not need to manually install your library in the local Maven repo each time you modify it, because you can use the Composite Build feature of Gradle: https://docs.gradle.org/current/userguide/composite_builds.html – ysakhno Feb 06 '20 at 21:04
  • added code. Also, consuming app is a maven project not gradle. I build it using simple mvn clean install – Ray S Feb 06 '20 at 21:14

3 Answers3

1

When you use mvn install:install-file, you are installing the jar file and creating a default pom for it using plain Maven. If you don't do anything else, the pom will not contain any of the transitive dependencies. After all, Maven just sees a jar file and knows nothing about the surrounding Gradle scripts. This is why it fails at runtime as the Log4J dependencies are missing from the library ("external") pom.

What you should do instead is to use the Maven Publish Plugin for Gradle to create a proper pom for your library. Do this by adding:

plugins {
    id 'maven-publish'
}

publishing {
    publications {
        myLibrary(MavenPublication) {
            from components.java
        }
    }
}

You can then upload the jar file to your local .m2 repository with a full pom using gradle publishToMavenLocal.

Also, ysakhno's answer is correct in the part about not needing log4j-core on your compilation classpath - it's simply bad practice. Instead, you should either remove it and make the consuming project add it as an explicit dependency, or change the configuration from implementation to runtimeOnly. Both approaches are fine, depending on how tight you want to couple Log4j with your library.

I also think it is perfectly fine to use the Log4J2 API in a library, even if it could be consumed in projects using many different logging implementations. After all, it is just as easy to bind the Log4J2 API to SLF4J, as it is the other way around. And both are popular and very good choices.

Bjørn Vester
  • 6,851
  • 1
  • 20
  • 20
  • This is a better explanation (than mine) for the core problem, although it still does not explain why he was able to make an uber-jar that _did_ include Log4J's classes. Also: he was not using Log4J2, just Log4J. – ysakhno Feb 08 '20 at 15:49
0

You may want to add this compile group: 'org.apache.logging.log4j', name: 'log4j-1.2-api', version: '2.2' too Idea is that you just don't have enough dependencies to satisfy your needs. It happens often with such things as loggers

Refer: unexpected exception: java.lang.NoClassDefFoundError: org/apache/log4j/LogManager

Andry Shutka
  • 283
  • 1
  • 4
  • The usage of `compile` and `runtime` configurations has been [discouraged since Gradle 3.4](https://docs.gradle.org/3.4/userguide/java_library_plugin.html#sec:java_library_separation), and has been [deprecated since Gradle 6.0](https://docs.gradle.org/current/userguide/upgrading_version_5.html#dependencies_should_no_longer_be_declared_using_the_compile_and_runtime_configurations). These configurations might not work with some future version of Gradle. – ysakhno Feb 06 '20 at 22:49
0

In general, the reason why logging libraries have 2 JARs (like in the sample you're presenting), is to let libraries compile against just the API JAR of the library, and then work (execute) at runtime with the actual implementation of the logging library (that would be log4j-core in your case) present somewhere on the classpath of the consuming application.

With the above in mind, you have to separate the dependencies between the library and the app, i.e. in the library's build.gradle you should have this:

dependencies {

    implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.13.0'
    // Note: you do not need the 'actual' implementation of Log4j in your library
    // at all!  It should compile very well with just the API, you'll then have
    // to put an 'implementation' dependency on log4j-core in your consuming
    // application's build.gradle (or pom.xml for that matter)

}

And then in your application's pom.xml you will have to put this:

    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.13.0</version>
    </dependency>

If you do want to leave everything as is (strongly not recommended) and have the dependencies from the library to be included in the consuming application, then use the api dependency configuration instead of the implementation one. Here is a good StackOverflow answer explaining the difference between the two.

On a side-note, I would suggest making your library dependent not on Log4j's API, but on Simple Logging Facade for Java one, because then the consuming application could choose which of the logging libraries implementations to use. As specified in SLF4J's FAQ:

[...] libraries and other embedded components should consider SLF4J for their logging needs because libraries cannot afford to impose their choice of logging framework on the end-user.

Note: Whatever you choose to do, do not use compile dependency configuration in Gradle build script, because it has been deprecated some time ago, and might not work with future versions of Gradle. The configuration roughly equivalent to compile is api.

ysakhno
  • 834
  • 1
  • 7
  • 17
  • That sounds very counter intuitive to me. The jar that is being consumed should be self sufficient and the code consuming it should be agnostic about it. Why should consuming code include log4j related libraries if it doesn't even want to log anything by itself? – Ray S Feb 06 '20 at 21:38
  • Your library should only include dependent libraries that it needs in order to be compiled. Your library by itself will not be run, so it really does not need any of the actual implementation dependencies, let alone should it impose any of those on the consuming application. Consider the SLF4J case I was talking about. It also has and API JAR, but guess what — it has several different implementation JARs. Should you pick a specific JAR in your library, the consuming application would need to make an effort to select something different. – ysakhno Feb 06 '20 at 22:00
  • consuming application knows nothing about logging. I can't use slf4j due to some project limitations. But regardless, I am not sure why are you suggesting any changes in the consuming application in lieu of whatever the logging needs of an external jar are. That just defeats the whole purpose of consuming 3rd party external jars. I have had issues with other external jars like fasterxml.jackson as well. I am on the verge of just creating an uber jar and call it a day for now – Ray S Feb 06 '20 at 22:05
  • Even though your application does not use any of the logging classes directly, it still needs to provide logging configuration. So in the end, your application is not so agnostic about chosen libraries as you might think. If nothing of the above persuaded you, and you still want to have it your way, just slap `api` instead of `implementation` in your library's build.gradle, as already suggested in the answer. That might work. If the JAR with the class is not present on the classpath, creating an uber-jar will likely not help either. – ysakhno Feb 06 '20 at 22:08
  • creating an uber jar worked but I am still bummed. It's an unnecessary fat jar and I really don't want to include anything that's not even being used in the jar. I get your point. Logging is just a typical use case. What about jackson dependencies? I don't see them either when I use this jar in the consuming app. How do you reason jackon? would you include jackson dependencies in the consuming app that has nothing to do with jackson? – Ray S Feb 06 '20 at 22:23
  • Probably not (in respect to Jackson library). I guess in the general sense, having `implementation` dependency configuration on the library side should have been enough for your application to run fine. What threw it off, I guess, was having Gradle-built library consumed in a Maven project. Did you try using the `api` dependency configuration instead of the `implementation` one, without making an uber-jar? – ysakhno Feb 06 '20 at 22:39