Skip to main content

Command Palette

Search for a command to run...

Reasons Logback Throws “SimpleLoggerContext cannot be cast to LoggerContext” After Migrating to GraalVM

When a Java application migrates to GraalVM native image, one of the most confusing runtime errors developers encounter comes from the logging subsystem—especially Logback and SLF4J.

Published
6 min read
Reasons Logback Throws “SimpleLoggerContext cannot be cast to LoggerContext” After Migrating to GraalVM
T

I am Tuanh.net. As of 2024, I have accumulated 8 years of experience in backend programming. I am delighted to connect and share my knowledge with everyone.

The infamous exception:

java.lang.ClassCastException: class org.slf4j.helpers.SubstituteLoggerFactory$SubstituteLoggerContext 
cannot be cast to class ch.qos.logback.classic.LoggerContext

This error usually surfaces after a successful native image build, right at runtime—ironically, when the application is supposed to log its “startup successful” message. Let’s dive deep into why it happens, how to reproduce it, and how to fix it without turning your log system into a silent mime show.

1. The Hidden Cause Behind the Error

1.1. SLF4J Service Discovery in GraalVM

In a regular JVM world, SLF4J discovers implementations like Logback dynamically at runtime using the ServiceLoader mechanism. The SPI (Service Provider Interface) system loads:

META-INF/services/org.slf4j.spi.SLF4JServiceProvider

which points to something like:

ch.qos.logback.classic.spi.LogbackServiceProvider

However, in GraalVM native images, this discovery process is not dynamic anymore. The ahead-of-time compiler (AOT) removes or omits dynamic class loading to optimize memory and startup time. As a result, the ServiceLoader mechanism cannot find LogbackServiceProvider, leading SLF4J to fall back to its default stub implementation: SubstituteLoggerFactory.

Thus, when Logback initializes, it tries to cast the fallback logger context (a stub) to the expected LoggerContext, causing the ClassCastException.

1.2. Why “It Works in JVM but Not in GraalVM”

When running on the JVM, the following code executes perfectly:

Logger logger = LoggerFactory.getLogger(MyApp.class);
logger.info("Application started!");

But when compiled to native image, GraalVM does not retain reflection or service metadata automatically. Without explicit configuration, Logback’s SPI file is never read — meaning SLF4J never discovers Logback.

That’s like inviting Logback to the party but forgetting to put its name on the guest list.

2. The Java Example and Root Analysis

Let’s reproduce the issue step by step.

2.1. Example Project Setup

We’ll use a simple Spring Boot application that logs a startup message:

package com.example.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
private static final Logger log = LoggerFactory.getLogger(DemoApplication.class);

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
log.info("Hello from GraalVM!");
}
}

Dependencies in pom.xml:

<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-logging</artifactid>
</dependency>

Now, compile to a native image using:

mvn -Pnative native:compile
./target/demo

Boom — you get:

Caused by: java.lang.ClassCastException: class org.slf4j.helpers.SubstituteLoggerFactory$SubstituteLoggerContext cannot be cast to class ch.qos.logback.classic.LoggerContext

2.2. Dissecting the Stacktrace

The key lines are:

LoggerFactory.getILoggerFactory()
→ returns SubstituteLoggerFactory
→ returns SubstituteLoggerContext
→ Logback expects LoggerContext
→ cast fails

This is purely a service loading mismatch caused by GraalVM’s static linking nature.

3. Techniques to Fix the Problem

3.1. Include SLF4J Service Providers in Reflection Config

You must instruct GraalVM to preserve service metadata for Logback.Add the following file: src/main/resources/META-INF/native-image/reflect-config.json:

[
{
"name": "ch.qos.logback.classic.spi.LogbackServiceProvider",
"allDeclaredConstructors": true
}
]

and also make sure the service provider is registered by adding:

--initialize-at-build-time=ch.qos.logback.classic.spi.LogbackServiceProvider

in your native-image.properties:

Args = --report-unsupported-elements-at-runtime 
--initialize-at-build-time=ch.qos.logback.classic
--initialize-at-build-time=org.slf4j.impl.StaticLoggerBinder

This ensures Logback’s provider class is visible to SLF4J at build time.

3.2. Add Resource Configuration

Add a resource-config.json file to retain Logback configuration files:

{
"resources": [
{ "pattern": "logback.xml" },
{ "pattern": "logback-spring.xml" },
{ "pattern": "META-INF/services/.*" }
]
}

This keeps your log configuration alive in the native image. Otherwise, even if Logback loads, it will fail silently without its XML config.

3.3. Verify Binding at Build Time

When building the native image, check your logs. You should see:

SLF4J: Found binding in [jar:file:/.../logback-classic-1.4.14.jar!/org/slf4j/impl/StaticLoggerBinder.class]

If not — it’s still using the stub.You can add a debug startup line:

System.out.println("Logger Factory: " + LoggerFactory.getILoggerFactory().getClass().getName());

Expected output:

Logger Factory: ch.qos.logback.classic.LoggerContext

4. Extended Discussion: When the Error Persists

4.1. Mixing Multiple Logging Libraries

If your dependencies pull multiple logging frameworks (e.g., slf4j-simple, log4j-slf4j2-impl, etc.), GraalVM will embed multiple providers, and SLF4J might bind to the wrong one.Run this Maven command to inspect duplicates:

mvn dependency:tree | grep slf4j

If you see both slf4j-simple and logback-classic, exclude the simple logger:

<exclusion>
<groupid>org.slf4j</groupid>
<artifactid>slf4j-simple</artifactid>
</exclusion>

4.2. Spring Boot 3.x and Native Image Integration

Spring Boot 3 introduced spring-aot-maven-plugin to generate AOT metadata automatically.If you’re using it, you can inspect generated configs in:

target/generated/aotResources/META-INF/native-image

Sometimes Spring AOT misses the LogbackServiceProvider, so you may need to add a manual hint:

@NativeHint(
types = @TypeHint(
types = ch.qos.logback.classic.spi.LogbackServiceProvider.class,
access = AccessBits.ALL
)
)

That’s like telling GraalVM: “Hey, don’t throw my logger out of the freezer.”

5. Final Thoughts and Broader Perspective

5.1. Why Logging Is Tricky in Native Images

Logging frameworks rely heavily on reflection, classpath scanning, and runtime initialization—three things GraalVM deliberately limits for performance. It’s like trying to do karaoke in a library: you technically can, but you’ll get a lot of warnings.

5.2. Long-Term Strategy

If you’re planning to go all-in with native images, consider Micronaut Logging or TinyLog — both designed to work natively without reflection.However, if you’re tied to Spring Boot or enterprise ecosystems, stick to Logback and provide explicit reflection and resource configurations.

6. Complete Example Configuration Summary

src/
├─ main/
│ ├─ java/com/example/demo/DemoApplication.java
│ ├─ resources/
│ │ ├─ logback-spring.xml
│ │ ├─ META-INF/native-image/
│ │ │ ├─ reflect-config.json
│ │ │ └─ resource-config.json
│ │ └─ native-image.properties

reflect-config.json

[
{ "name": "ch.qos.logback.classic.spi.LogbackServiceProvider", "allDeclaredConstructors": true }
]

resource-config.json

{
"resources": [
{ "pattern": "logback." },
{ "pattern": "META-INF/services/.
" }
]
}

native-image.properties

Args = --initialize-at-build-time=ch.qos.logback.classic,org.slf4j.impl.StaticLoggerBinder 
--report-unsupported-elements-at-runtime

Conclusion

The SimpleLoggerContext cannot be cast to LoggerContext error in GraalVM is not really a Logback bug—it’s a visibility issue caused by the static world of native compilation.Once you restore Logback’s SPI visibility and preserve its configuration files, the logger works just as it does on the JVM—only faster, leaner, and ready for microservice duty.

Want to ask how to handle custom appenders or async logging in GraalVM?👉 Drop your question in the comments below — let’s debug it together.

Read more at : Reasons Logback Throws “SimpleLoggerContext cannot be cast to LoggerContext” After Migrating to GraalVM

More from this blog

T

tuanh.net

540 posts

Are you ready to elevate your Java, OOP, Spring, and DevOps skills? Look no further!