Understanding Java Context ClassLoader and Its Role in Breaking the Parent Delegation Model
This article explains why Java introduced the Context ClassLoader to bypass the parent‑delegation model, how it enables SPI mechanisms such as JDBC drivers, and the specific class‑loading pitfalls encountered when running Spring Boot applications as executable JARs.
Context ClassLoader Emergence
Although the JVM uses the parent‑delegation model for class loading, the Context ClassLoader was introduced to break this model in situations such as Java SPI, where third‑party implementations need to be loaded by a different class loader.
In Java 17, platform classes (e.g., java.sql.Connection) are loaded by the Platform class loader, while third‑party drivers are loaded by the System/Application class loader. When the two are loaded by different loaders, the parent‑delegation model cannot locate the implementation class, causing ClassNotFound errors.
Example output of class loader inspection:
System.out.println("java.sql.Connection => " + Connection.class.getClassLoader());
System.out.println("java.sql.DriverManager => " + DriverManager.class.getClassLoader());
System.out.println("java.sql.Driver => " + Driver.class.getClassLoader());
System.out.println("com.mysql.cj.jdbc.ConnectionImpl => " + com.mysql.cj.jdbc.ConnectionImpl.class.getClassLoader());Because the Platform class loader cannot see the driver implementation, the Context ClassLoader of the current thread is used to load the driver via ServiceLoader and Class.forName(..., true, classLoader) .
Typical ways to obtain or specify a class loader:
Class.forName("com.example.MyClass", true, myClassLoader);
Thread.currentThread().getContextClassLoader();Spring Boot’s executable‑jar mode uses its own LaunchedURLClassLoader , which can cause additional loading pitfalls, especially when third‑party libraries rely on the thread context class loader or when asynchronous tasks run in a ForkJoinPool that inherits the system class loader.
Example Spring Boot code demonstrating class‑loader visibility:
package com.example.demo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.*;
@Component
@Slf4j
public class ThreadPoolConfig {
@PostConstruct
void init() throws ExecutionException, InterruptedException {
log.info("Current class loader: {}", getClass().getClassLoader());
log.info("Application class loader: {}", Demo1Application.class.getClassLoader());
log.info("Thread context loader: {}", Thread.currentThread().getContextClassLoader());
Thread thread = new Thread(() ->
log.info("New thread context loader: {}", Thread.currentThread().getContextClassLoader()));
thread.start();
thread.join();
CompletableFuture
future = CompletableFuture.runAsync(() ->
log.info("CompletableFuture context loader: {}", Thread.currentThread().getContextClassLoader()));
future.get();
}
}Running the code in IDE (no packaging) shows the application class loader as the context loader, while running the packaged jar shows org.springframework.boot.loader.LaunchedURLClassLoader as the loader, leading to potential class‑loading failures.
Conclusion
The thread context class loader breaks the parent‑delegation model, enabling SPI mechanisms to load third‑party implementations. In Spring Boot executable‑jar mode, the custom LaunchedURLClassLoader introduces specific loading pitfalls that developers must be aware of.
Cognitive Technology Team
Cognitive Technology Team regularly delivers the latest IT news, original content, programming tutorials and experience sharing, with daily perks awaiting you.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.