【问题标题】:Java code throws NoClassDefFoundError for a class inside external libraryJava 代码为外部库中的类抛出 NoClassDefFoundError
【发布时间】:2020-02-06 20:42:49
【问题描述】:

我有一个 maven 项目,其中包含一个使用 gradle 作为 pom 依赖项创建的 jar。 在包含 Jar 的代码中,我引用了 log4j logmanager。 当我尝试访问外部 jar 中的方法时,它会在外部 jar 中的类所引用的 logmanager 上抛出 java.lang.NoClassDefFoundError。

外部 jar 的 build.gradle 是:

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'

}

我使用 gradle clean assemble 构建 jar

我使用 mvn install:install-file 将这个 jar 本地安装到 .m2 中,然后在 pom 中为消费应用程序添加它的依赖项。

我不确定这里发生了什么。

外部Jar类代码

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 消费类代码

import com.company.something.MyClass;

public class consumingClass{

   public String consumingMethod(){
      MyClass.myMethod("someinput");
      return "something";
    }
}

【问题讨论】:

  • 您能否发布一个简单的 Java 代码示例,连同您已经提供的 Gradle 构建脚本,可以证明您遇到的问题?
  • 你的意思是消费应用程序的java代码?
  • 好吧,实际上最好是两者兼有(库和消费应用程序)。也请显示消费应用程序的 Gradle 构建脚本。顺便说一句,您不需要每次修改时手动将库安装在本地 Maven 仓库中,因为您可以使用 Gradle 的 Composite Build 功能:docs.gradle.org/current/userguide/composite_builds.html
  • 添加了代码。此外,使用应用程序是一个 Maven 项目,而不是 gradle。我使用简单的 mvn clean install 构建它

标签: java maven gradle


【解决方案1】:

当您使用mvn install:install-file 时,您正在安装 jar 文件并使用纯 Maven 为其创建默认 pom。如果你不做任何其他事情,pom 将不包含任何传递依赖项。毕竟,Maven 只看到一个 jar 文件,对周围的 Gradle 脚本一无所知。这就是它在运行时失败的原因,因为库(“外部”)pom 中缺少 Log4J 依赖项。

您应该改为使用Maven Publish Plugin for Gradle 为您的库创建一个合适的 pom。通过添加来做到这一点:

plugins {
    id 'maven-publish'
}

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

然后,您可以使用 gradle publishToMavenLocal 将 jar 文件上传到带有完整 pom 的本地 .m2 存储库。

另外,ysakhno's answer 在编译类路径中不需要log4j-core 的部分是正确的——这只是不好的做法。相反,您应该要么 删除它并让使用项目将其添加为显式依赖项, 将配置从implementation 更改为runtimeOnly。这两种方法都很好,具体取决于您希望将 Log4j 与库耦合的紧密程度。

我还认为在库中使用 Log4J2 API 非常好,即使它可以在使用许多不同日志记录实现的项目中使用。毕竟,将 Log4J2 API 绑定到 SLF4J 就像反过来一样容易。两者都是受欢迎且非常好的选择。

【讨论】:

  • 这是对核心问题的更好解释(比我的),尽管它仍然不能解释为什么他能够制作一个 确实 包含 Log4J 类的 uber-jar .另外:他没有使用 Log4J2,只是使用 Log4J。
【解决方案2】:

你可能想添加这个 compile group: 'org.apache.logging.log4j', name: 'log4j-1.2-api', version: '2.2' 太 想法是您没有足够的依赖项来满足您的需求。它经常发生在记录器之类的东西上

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

【讨论】:

【解决方案3】:

一般来说,日志库有 2 个 JAR(就像您展示的示例一样)的原因是让库编译 仅针对库的 API JAR,然后工作(执行)在运行时,日志库的实际实现(在你的情况下是log4j-core)出现在消费应用程序的类路径的某个地方。

考虑到上述情况,您必须分离库和应用程序之间的依赖关系,即在库的build.gradle 中,您应该有这样的:

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)

}

然后在你的应用程序的 pom.xml 中你必须把这个:

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

如果您确实希望保留所有内容(强烈推荐)并将库中的依赖项包含在消费应用程序中,请使用 api 依赖项配置而不是implementation 一。这是一个很好的StackOverflow answer explaining the difference between the two

在旁注中,我建议让您的库不依赖于 Log4j 的 API,而是依赖于 Simple Logging Facade for Java 之一,因为这样消费应用程序可以选择使用哪个日志库实现。如SLF4J's FAQ中所述:

[...] 库和其他嵌入式组件应该考虑 SLF4J 满足他们的日志记录需求,因为图书馆负担不起 他们对最终用户的日志记录框架的选择。

注意:无论您选择做什么,都不要在 Gradle 构建脚本中使用 compile 依赖配置,因为它在一段时间前已被弃用,并且可能不适用于 Gradle 的未来版本。大致相当于compile的配置是api

【讨论】:

  • 这听起来很违反直觉。正在使用的 jar 应该是自给自足的,并且使用它的代码应该不知道它。如果消费代码甚至不想自己记录任何内容,为什么还要包含 log4j 相关库?
  • 你的库应该只包含编译所需的依赖库。您的库本身不会运行,因此它实际上不需要任何实际的实现依赖项,更不用说它是否应该将任何依赖项强加于使用应用程序。考虑一下我所说的 SLF4J 案例。它也有 API JAR,但你猜怎么着——它有几个不同的实现 JAR。如果您在库中选择特定的 JAR,使用应用程序将需要努力选择不同的东西。
  • 消费应用程序对日志一无所知。由于某些项目限制,我无法使用 slf4j。但无论如何,我不确定您为什么建议在消费应用程序中进行任何更改,而不是外部 jar 的日志记录需求。这完全违背了使用第 3 方外部罐子的全部目的。我也遇到了其他外部 jar 的问题,比如 fastxml.jackson。我即将创建一个 uber jar 并暂时结束
  • 即使您的应用程序不直接使用任何日志记录类,它仍然需要提供日志记录配置。所以最后,您的应用程序并不像您想象的那样对选择的库不可知。如果上述任何内容都没有说服您,并且您仍然希望按照自己的方式进行操作,那么只需在您的库的 build.gradle 中使用 api 而不是 implementation,正如答案中已经建议的那样。那可能行得通。如果类路径中不存在该类的 JAR,则创建 uber-jar 也可能无济于事。
  • 创建一个 uber jar 有效,但我仍然很沮丧。这是一个不必要的胖罐子,我真的不想在罐子里放任何东西。我明白你的意思。日志记录只是一个典型的用例。杰克逊依赖呢?当我在消费应用程序中使用这个 jar 时,我也看不到它们。你怎么解释杰克逊?你会在消费应用程序中包含与杰克逊无关的杰克逊依赖项吗?
猜你喜欢
  • 1970-01-01
  • 2012-01-30
  • 2011-12-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-01-08
  • 1970-01-01
  • 2019-08-20
相关资源
最近更新 更多