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?
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