javalog4jpom.xmlmaven-assembly-pluginuberjar

Log4J implementation not found in fat JAR


I already found out how to configure Log4J programmatically, and now I need to build a “fat JAR” that includes all dependencies, so that the program can be run simply with java -jar example.jar and be copied around without the need for a lib folder next to it.

Here's my project structure:

$ tree -I '.idea|test|target'
.
├── log.txt
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── log4j_fat_jar
        │               └── Main.java
        └── resources
            └── META-INF
                └── MANIFEST.MF

MANIFEST.MF:

Manifest-Version: 1.0
Main-Class: com.example.log4j_fat_jar.Main

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example.log4j-fat-jar</groupId>
    <artifactId>log4j-fat-jar</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>23</maven.compiler.source>
        <maven.compiler.target>23</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>3.0.0-beta2</version>
        </dependency>

        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>3.0.0-beta2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.7.1</version>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive> <!-- https://stackoverflow.com/a/54309611 -->
                       <manifest>
                            <mainClass>com.example.log4j_fat_jar.Main</mainClass>
                        </manifest>
                        <manifestFile>${basedir}/src/main/resources/META-INF/MANIFEST.MF</manifestFile>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

Main.java:

package com.example.log4j_fat_jar;

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.config.Configurator;
import org.apache.logging.log4j.core.config.builder.api.*;
import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;

import java.io.File;

public class Main {
    private static final Logger LOG = LogManager.getLogger(Main.class);

    public static void main(String[] arguments) {
        File logFilePath = new File("log.txt");
        Main.setLoggingLevel(Level.TRACE, logFilePath);
        LOG.info("main(): Hello, World!");
        LOG.warn("main(): This is a warning.");
        LOG.error("main(): This is an error.");
    }

    public static void setLoggingLevel(Level level, File logFilePath) {
        ConfigurationBuilder<BuiltConfiguration> builder = ConfigurationBuilderFactory.newConfigurationBuilder();
        final String PATTERN = "%d{ISO8601} %-5p %-9t – %c{1} – %m%n";

        LayoutComponentBuilder layout = builder
            .newLayout("PatternLayout")
            .addAttribute("pattern", PATTERN);

        AppenderComponentBuilder consoleAppender = builder
            .newAppender("Stdout", "Console")
            .addAttribute("target", "SYSTEM_OUT")
            .add(layout);

        AppenderComponentBuilder fileAppender = builder
            .newAppender("File", "File")
            .addAttribute("fileName", logFilePath)
            .addAttribute("append", true)
            .addAttribute("locking", false)
            .addAttribute("immediateFlush", true)
            .add(layout);

        builder.add(consoleAppender);
        builder.add(fileAppender);

        RootLoggerComponentBuilder rootLogger = builder.newRootLogger(Level.WARN);
        rootLogger.add(builder.newAppenderRef("Stdout"));
        rootLogger.add(builder.newAppenderRef("File"));
        builder.add(rootLogger);

        LoggerComponentBuilder appLogger = builder
            .newLogger(Main.class.getPackageName(), level)
            .add(builder.newAppenderRef("Stdout"))
            .add(builder.newAppenderRef("File"))
            .addAttribute("additivity", false);

        builder.add(appLogger);
        Configurator.initialize(builder.build());
        Configurator.reconfigure(builder.build());
        LOG.info("Logging level set to {}", level);
    }
}

Here's the command-line output when running the program from within IntelliJ, using the green triangle button:

2025-03-15T21:39:03,233 INFO  main      – Main – Logging level set to TRACE
2025-03-15T21:39:03,256 INFO  main      – Main – main(): Hello, World!
2025-03-15T21:39:03,256 WARN  main      – Main – main(): This is a warning.
2025-03-15T21:39:03,257 ERROR main      – Main – main(): This is an error.

However, when running the compiled JAR file manually from the command line, I get this error:

$ java -version
openjdk version "23.0.1" 2024-10-15
OpenJDK Runtime Environment Zulu23.30+13-CA (build 23.0.1+11)
OpenJDK 64-Bit Server VM Zulu23.30+13-CA (build 23.0.1+11, mixed mode, sharing)

$ java -jar target/log4j-fat-jar-1.0-SNAPSHOT-jar-with-dependencies.jar 
ERROR StatusLogger Exception thrown by constructor for class org.apache.logging.log4j.core.impl.Log4jContextFactory
 org.apache.logging.log4j.plugins.di.NotInjectableException: No @Inject constructor or default constructor found for Key[type: org.apache.logging.log4j.core.selector.ContextSelector]
        at org.apache.logging.log4j.plugins.di.DefaultInstanceFactory.createDefaultFactory(DefaultInstanceFactory.java:133)
        at org.apache.logging.log4j.plugins.di.DefaultInstanceFactory.lambda$getFactory$3(DefaultInstanceFactory.java:111)
        at java.base/java.util.Optional.orElseGet(Optional.java:364)
        at org.apache.logging.log4j.plugins.di.DefaultInstanceFactory.getFactory(DefaultInstanceFactory.java:111)
        at org.apache.logging.log4j.plugins.di.InstanceFactory.getFactory(InstanceFactory.java:82)
        at org.apache.logging.log4j.plugins.di.InstanceFactory.getInstance(InstanceFactory.java:104)
        at org.apache.logging.log4j.core.impl.Log4jContextFactory.<init>(Log4jContextFactory.java:123)
        at org.apache.logging.log4j.core.impl.Log4jContextFactory.<init>(Log4jContextFactory.java:61)
        at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
        at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:501)
        at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:485)
        at org.apache.logging.log4j.spi.LoggingSystem.tryInstantiate(LoggingSystem.java:251)
        at org.apache.logging.log4j.spi.LoggingSystem$SystemProvider.createLoggerContextFactory(LoggingSystem.java:298)
        at org.apache.logging.log4j.spi.LoggingSystem.lambda$new$0(LoggingSystem.java:82)
        at org.apache.logging.log4j.util.Lazy.lambda$map$0(Lazy.java:39)
        at org.apache.logging.log4j.util.LazyUtil$SafeLazy.get(LazyUtil.java:109)
        at org.apache.logging.log4j.spi.LoggingSystem.getLoggerContextFactory(LoggingSystem.java:169)
        at org.apache.logging.log4j.LogManager.getFactory(LogManager.java:390)
        at org.apache.logging.log4j.LogManager.getContext(LogManager.java:127)
        at org.apache.logging.log4j.LogManager.getLogger(LogManager.java:554)
        at com.example.log4j_fat_jar.Main.<clinit>(Main.java:13)
ERROR StatusLogger Log4j could not find a logging implementation. Please add log4j-core dependencies to classpath or module path. Using SimpleLogger to log to the console.
Exception in thread "main" java.lang.IllegalArgumentException: Invalid Configuration class specified
        at org.apache.logging.log4j.core.config.builder.impl.DefaultConfigurationBuilder.build(DefaultConfigurationBuilder.java:219)
        at org.apache.logging.log4j.core.config.builder.impl.DefaultConfigurationBuilder.build(DefaultConfigurationBuilder.java:182)
        at org.apache.logging.log4j.core.config.builder.impl.DefaultConfigurationBuilder.build(DefaultConfigurationBuilder.java:68)
        at com.example.log4j_fat_jar.Main.setLoggingLevel(Main.java:65)
        at com.example.log4j_fat_jar.Main.main(Main.java:17)
Caused by: java.lang.reflect.InvocationTargetException
        at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:74)
        at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:501)
        at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:485)
        at org.apache.logging.log4j.core.config.builder.impl.DefaultConfigurationBuilder.build(DefaultConfigurationBuilder.java:194)
        ... 4 more
Caused by: org.apache.logging.log4j.plugins.di.NotInjectableException: No @Inject constructor or default constructor found for chain Key[type: org.apache.logging.log4j.core.impl.ReusableLogEventFactory] -> Key[type: org.apache.logging.log4j.core.ContextDataInjector]
        at org.apache.logging.log4j.plugins.di.DefaultInstanceFactory.createDefaultFactory(DefaultInstanceFactory.java:133)
        at org.apache.logging.log4j.plugins.di.DefaultInstanceFactory.lambda$getFactory$3(DefaultInstanceFactory.java:111)
        at java.base/java.util.Optional.orElseGet(Optional.java:364)
        at org.apache.logging.log4j.plugins.di.DefaultInstanceFactory.getFactory(DefaultInstanceFactory.java:111)
        at org.apache.logging.log4j.plugins.di.InstanceFactory.getInstance(InstanceFactory.java:126)
        at org.apache.logging.log4j.plugins.di.DefaultInstanceFactory.lambda$getArgumentFactory$9(DefaultInstanceFactory.java:211)
        at org.apache.logging.log4j.plugins.di.DefaultInstanceFactory.lambda$getInjectableInstance$7(DefaultInstanceFactory.java:159)
        at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:215)
        at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1709)
        at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:570)
        at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:560)
        at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:636)
        at java.base/java.util.stream.AbstractPipeline.evaluateToArrayNode(AbstractPipeline.java:291)
        at java.base/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:656)
        at java.base/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:662)
        at org.apache.logging.log4j.plugins.di.DefaultInstanceFactory.getInjectableInstance(DefaultInstanceFactory.java:160)
        at org.apache.logging.log4j.plugins.di.DefaultInstanceFactory.lambda$createDefaultFactory$6(DefaultInstanceFactory.java:136)
        at org.apache.logging.log4j.plugins.di.InstanceFactory.getInstance(InstanceFactory.java:115)
        at org.apache.logging.log4j.core.config.LoggerConfig.<init>(LoggerConfig.java:220)
        at org.apache.logging.log4j.core.config.AbstractConfiguration.<init>(AbstractConfiguration.java:154)
        at org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration.<init>(BuiltConfiguration.java:50)
        at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
        ... 7 more

What am I doing wrong?


Solution

  • Check out our FAQ entry about single-JAR applications.

    What happens is that by using the Maven Assembly Plugin, you are squashing many important metadata files. As many modern (Java 6+) libraries, Log4j Core 3 declares ServiceLoader services in META-INF/services/<service class name> files. If multiple JARs provide the same service, Maven Assembly Plugin will chose one service file and drop all the others.

    Instead of creating a fat JAR, you should use one of the JAR-in-JAR solutions. In this case the original JAR files are stored inside your single-JAR application and a small classloader is used to load them. Since the JARs are not unpackaged, there is no metadata file loss.

    You can use for example Spring Boot Maven plugin:

    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>3.4.3</version>
        <executions>
            <execution>
                <goals>
                    <goal>repackage</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
    

    Eclipse has also a jar in jar loader, but it is not as easy to use.

    Note: Some Maven fat JAR plugins are able to merge service descriptors and other shared files, but this requires a lot of technical knowledge to properly merge all file name conflicts. See this answer if you want to go that way