Java & Gradle & log4j NoClassDefFoundError: org/apache/logging/log4j/LogManager

Java & Gradle & log4j NoClassDefFoundError: org/apache/logging/log4j/LogManager

我正在尝试在我的 java 核心控制台应用程序中使用 log4j。在文档中有关于如何为 log4j 添加 Gradle 依赖项的示例代码:

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'
}

我将其粘贴到我的 build.gradle 文件中并为 运行 编写了示例代码。

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");
    }
}

除此之外,我还添加了清单,以便 运行 来自控制台的代码。

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

如果我使用 Gradle 构建它 - 一切都会成功构建。但是如果我尝试 运行 java -jar 从我的构建目录然后我得到

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

我找到了 1 个解决方法:修改 jar{} 部分:

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

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

它开始工作了,我什至尝试压缩 .jar 文件并看到添加了 log4j 依赖项。

但是我不想传递对 log4j 的依赖是什么?如果我设置 implementation group 而不是 compile group 它就不会再起作用了。有没有完整的使用log4j的例子?

最重要的是,要指出一件事,虽然它不能与 java -jar <.jar> 一起使用,但出于某种原因它仍然可以在 Intellij idea 中使用。有人可以解释一下为什么吗?

您遇到的问题是由启动应用程序时应用程序类路径中的问题引起的(参见 What is a classpath and how do I set it?)。

Gradle 有很多选项可以帮助您正确设置类路径。由于您没有提及任何 Gradle 版本,我将在示例中使用版本 7.1。假设您在名为 appName:

的项目中有此 build.gradle 文件
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 不是编译你的代码所必需的,所以它在 runtimeOnly 依赖配置中,参见 dependency configurations)

手动设置类路径

如果生成普通 JAR 文件(执行 jar task 并查看 build/libs),您可以使用以下命令执行应用程序:

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

(我假设所有 jar 都在当前目录中;路径分隔符是系统特定的,在 Windows 上是 ;

这变得很容易很难正确,因此 application Gradle 插件生成一个 shell 和一个批处理脚本来帮助您:执行 installDist task 并查看进入 build/install/appName(或者您可以使用 distZipdistTar 任务来压缩此文件夹的版本)。设置正确的命令简化为调用:

bin/appName

在清单中硬编码类路径

您还可以在 JAR 清单文件中对所需的依赖项进行硬编码。在 Gradle 中,您可以使用:

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

获取包含启动应用程序所需的所有信息的清单文件:

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

注意 runtimeClasspath 的用法,这是一个包含启动应用程序所需的 runtimeOnlyimplementation 依赖项配置的配置。

如果依赖 jar 与 appName.jar 在同一文件夹中,您可以 运行 应用程序:

java -jar appName.jar

Spring 引导加载程序

如果你想将所有依赖项都放在一个 JAR 中,你可以使用 Spring Boot Gradle plugin 而不是 application 插件:

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

bootJar 任务将生成一个 appName.jar,其结构类似于 distZip 文件生成的 appName.zip 文件。然而 shell/batch 脚本被替换为 Java 代码,因此您可以调用:

java -jar appName.jar

胖罐

你在问题中做了什么:

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

通常称为fat JAR。基本上它是通过将您的 jar 文件和所有依赖项解压缩到一个文件夹中并将其重新压缩在一起来生成的。

这可行,但有一些缺点:例如您可能会丢失有关依赖项的许可信息(这可能违反许可),并且您将无法在不重新编译的情况下用新版本替换依赖项。请参阅 一个大得离谱的胖罐子的例子。