I have a little java application with picocli
and apache-poi
:
build.gradle
plugins {
id 'java'
id 'application'
id 'org.graalvm.buildtools.native' version '0.10.1'
}
group = 'com.example'
version = '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
implementation 'info.picocli:picocli:4.7.5'
annotationProcessor 'info.picocli:picocli-codegen:4.7.5'
implementation('org.apache.poi:poi:5.2.5')
implementation('org.apache.poi:poi-ooxml:5.2.5')
}
compileJava {
options.compilerArgs += ["-Aproject=${project.group}/${project.name}"]
}
application {
mainClass = 'com.example.NativeCompileExample'
}
NativeCompileExample.java
package com.example;
import org.apache.poi.hssf.usermodel.HSSFPrintSetup;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import picocli.CommandLine;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.concurrent.Callable;
@CommandLine.Command(
name = "example",
mixinStandardHelpOptions = true,
description = "example description")
public class NativeCompileExample implements Callable<Integer> {
@CommandLine.Parameters(description = "The input file")
private File inputFile;
@CommandLine.Parameters(description = "The output file")
private File outputFile;
public static void main(String[] args) throws IOException {
final int exitCode = new CommandLine(new NativeCompileExample()).execute(args);
System.exit(exitCode);
}
@Override
public Integer call() throws Exception {
try (final XSSFWorkbook workbook = new XSSFWorkbook(inputFile)) {
for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
final XSSFSheet sheet = workbook.getSheetAt(i);
sheet.getPrintSetup().setLandscape(true);
sheet.getPrintSetup().setPaperSize(HSSFPrintSetup.A4_PAPERSIZE);
}
workbook.write(new FileOutputStream(outputFile));
}
return 0;
}
}
I compile the application with native-compile via gradle with graalvm-21 (installed via sdkman. Version 21.0.2-graalce
):
./gradlew clean build nativeCompile
When I run the compiled application, I see the following error:
Exception in thread "main" java.lang.ExceptionInInitializerError
at org.apache.logging.log4j.LogManager.<clinit>(LogManager.java:61)
at org.apache.poi.ooxml.POIXMLDocumentPart.<clinit>(POIXMLDocumentPart.java:56)
at java.base@21.0.2/java.lang.Class.ensureInitialized(DynamicHub.java:601)
at java.base@21.0.2/java.lang.Class.ensureInitialized(DynamicHub.java:601)
at com.example.NativeCompileExample.call(NativeCompileExample.java:37)
at com.example.NativeCompileExample.call(NativeCompileExample.java:18)
at picocli.CommandLine.executeUserObject(CommandLine.java:2041)
at picocli.CommandLine.access$1500(CommandLine.java:148)
at picocli.CommandLine$RunLast.executeUserObjectOfLastSubcommandWithSameParent(CommandLine.java:2461)
at picocli.CommandLine$RunLast.handle(CommandLine.java:2453)
at picocli.CommandLine$RunLast.handle(CommandLine.java:2415)
at picocli.CommandLine$AbstractParseResultHandler.execute(CommandLine.java:2273)
at picocli.CommandLine$RunLast.execute(CommandLine.java:2417)
at picocli.CommandLine.execute(CommandLine.java:2170)
at com.example.NativeCompileExample.main(NativeCompileExample.java:31)
at java.base@21.0.2/java.lang.invoke.LambdaForm$DMH/sa346b79c.invokeStaticInit(LambdaForm$DMH)
Caused by: java.lang.IllegalStateException: java.lang.InstantiationException: org.apache.logging.log4j.message.DefaultFlowMessageFactory
at org.apache.logging.log4j.spi.AbstractLogger.createDefaultFlowMessageFactory(AbstractLogger.java:239)
at org.apache.logging.log4j.spi.AbstractLogger.<init>(AbstractLogger.java:138)
at org.apache.logging.log4j.status.StatusLogger.<init>(StatusLogger.java:141)
at org.apache.logging.log4j.status.StatusLogger.<clinit>(StatusLogger.java:91)
... 16 more
Caused by: java.lang.InstantiationException: org.apache.logging.log4j.message.DefaultFlowMessageFactory
at java.base@21.0.2/java.lang.Class.newInstance(DynamicHub.java:719)
at org.apache.logging.log4j.spi.AbstractLogger.createDefaultFlowMessageFactory(AbstractLogger.java:237)
... 19 more
Caused by: java.lang.NoSuchMethodException: org.apache.logging.log4j.message.DefaultFlowMessageFactory.<init>()
at java.base@21.0.2/java.lang.Class.checkMethod(DynamicHub.java:1075)
at java.base@21.0.2/java.lang.Class.getConstructor0(DynamicHub.java:1238)
at java.base@21.0.2/java.lang.Class.newInstance(DynamicHub.java:706)
... 20 more
When running the application without native-compile it, it works. I assume it has something to do with the fact that log4j is not supported by graalvm.
Any tips how I can tell apache-poi to use something different or just provide an empty implementation for the logging classes?
I tried to exclude log4j-api
from the apache-poi
dependencies via gradle:
implementation('org.apache.poi:poi:5.2.5') {
exclude group: 'org.apache.logging.log4j', module: 'log4j-api'
}
implementation('org.apache.poi:poi-ooxml:5.2.5') {
exclude group: 'org.apache.logging.log4j', module: 'log4j-api'
}
But then ofcourse the error was not gone because now apache-poi
complains that classes are missing.
I also tried to add log4j-over-slf4j
but it provides different packages as used by apache-poi
:
log4j-over-slf4j
the package structure is org.apache.log4j.*
apache-poi
uses the log classes from the following package: org.apache.logging.log4j.*
This is a known issue (cf. Issue #1539) due to the way Log4j 2.x instantiates message factories, which is apparently not supported by GraalVM.
The issue is addressed in PR #2392 for the java.util.logging
and Logback backend of Log4j API.
Log4j Core users currently need to generate reachability configuration themselves. There is an ongoing discussion on what kind of reachability metadata should be shipped with log4j-core
by default.
The core of the problem is that libraries such as Log4j Core and Logback transform configuration files into a tree of Java components using reflection.
Since the reflective calls don't have compile-time constant arguments, GraalVM is not able to determine which classes need to be included.
To workaround this you can add a reachability configuration to your application (cf. reachability metadata).
The easiest way to do this is to:
reflect-config.json
and resources-config.json
configuration files.*-config.json
files into a META-INF/native-image/<group.id>/<artifact.id>
resource folder of your application,