Hi, Spring Boot Team! First, describe my question: Spring Boot version 3.2.8, OpenJDK 64-Bit Server VM (build 17.0.2+8-86, mixed mode, sharing). I continuously receive NoClassDefFoundError in my java services. But strangely, some pods are normal, some pods can’t load org/springframework/aop/framework/AdvisedSupport$MethodCacheKey class. I'm every sure this class file is in the correct position in Spring Boot FatJar(/!BOOT-INF/lib/spring-aop-6.1.11.jar!/).

2025-08-06 00:28:31.093 [iot-device-simulator-7565776bcb-ldnpp:8008] [] [simulator-task-notify-ordered-0-1] ERROR com.xxx.xxx.xxx.XxxApplication - Uncaught exception in thread: Thread[simulator-task-notify-ordered-0-1,5,main], classloader: org.springframework.boot.loader.launch.LaunchedClassLoader@2dda6444ex: 
 java.lang.NoClassDefFoundError: org/springframework/aop/framework/AdvisedSupport$MethodCacheKey
    at org.springframework.aop.framework.AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice(AdvisedSupport.java:494)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:207)
    at jdk.proxy2/jdk.proxy2.$Proxy113.updateGatewayStatus(Unknown Source)

Second, I tried to read Spring Boot class loading mechanism(LaunchedClassLoader), I found something strange in FileDataBlock.fillBuffer() method. Why using RandomAccessFile to read FatJar file after the class loading thread is interrupted? But I'm not sure is this the problem root cause, because many IOException(s) are catched and ignored.

class FileDataBlock implements CloseableDataBlock {

    private void fillBuffer(long position) throws IOException {
        if (Thread.currentThread().isInterrupted()) {
            fillBufferUsingRandomAccessFile(position);
            return;
        }
        try {
            if (this.fileChannelInterrupted) {
                repairFileChannel();
                this.fileChannelInterrupted = false;
            }
            this.buffer.clear();
            this.bufferSize = this.fileChannel.read(this.buffer, position);
            this.bufferPosition = position;
        }
        catch (ClosedByInterruptException ex) {
            this.fileChannelInterrupted = true;
            fillBufferUsingRandomAccessFile(position);
        }
    }

    private void fillBufferUsingRandomAccessFile(long position) throws IOException {
        if (this.randomAccessFile == null) {
            this.randomAccessFile = new RandomAccessFile(this.path.toFile(), "r");
            tracker.openedFileChannel(this.path);
        }
        byte[] bytes = new byte[BUFFER_SIZE];
        this.randomAccessFile.seek(position);
        int len = this.randomAccessFile.read(bytes);
        this.buffer.clear();
        if (len > 0) {
            this.buffer.put(bytes, 0, len);
        }
        this.bufferSize = len;
        this.bufferPosition = position;
    }

If the I/O operations is interrupted by Future.cancel(true), it seems the class loading process is interrupted, and the class can't be loaded forever.

public class FutureTask<V> implements RunnableFuture<V> {

    public boolean cancel(boolean mayInterruptIfRunning) {
        if (!(state == NEW && STATE.compareAndSet
              (this, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
            return false;
        try {    // in case call to interrupt throws exception
            if (mayInterruptIfRunning) {
                try {
                    Thread t = runner;
                    if (t != null)
                    // If this thread is blocked in an I/ O operation upon an InterruptibleChannel then the channel will be closed, the thread's interrupt status will be set, and the thread will receive a java. nio. channels. ClosedByInterruptException.
                        t.interrupt();
                } finally { // final state
                    STATE.setRelease(this, INTERRUPTED);
                }
            }
        } finally {
            finishCompletion();
        }
        return true;
    }

Third, I replaced Future.cancel(true) with Future.cancel(false), it seemed worked. I had tried my best effort to debug this problem. There's no log and I don't know how to repackage spring-boot-maven-plugin, and put the spring-boot-loader classes in the FatJar. Hope to receive your reply!

Comment From: mhalbritter

Spring Boot 3.2.8 is no longer OSS supported. Please update to a supported version and see if this issue still persists.

Comment From: oneby-wang

Hi, I upgraded Spring Boot version to 3.5.4 and opened JVM arg -verbose:class to trace class loading. The problem still exists. In error java services, I saw some classes was loaded from spring-aop-6.2.9.jar, except org.springframework.aop.framework.AdvisedSupport$MethodCacheKey.

 [12.907s][info][class,load] org.springframework.aop.RawTargetAccess source: jar:nested:/app/app.jar/!BOOT-INF/lib/spring-aop-6.2.9.jar!/
 [12.907s][info][class,load] org.springframework.aop.scope.ScopedObject source: jar:nested:/app/app.jar/!BOOT-INF/lib/spring-aop-6.2.9.jar!/

Here is the full error stack trace.

2025-08-06 08:33:27.604 [iot-device-simulator-7565776bcb-dbl6t:8008] [] [simulator-task-notify-ordered-0-0] ERROR com.xxx.xxx.xxx.XxxCloudApplication - Uncaught exception in thread: Thread[simulator-task-notify-ordered-0-0,5,main], classloader: org.springframework.boot.loader.launch.LaunchedClassLoader@2dda6444, ex: 
 java.lang.NoClassDefFoundError: org/springframework/aop/framework/AdvisedSupport$MethodCacheKey
    at org.springframework.aop.framework.AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice(AdvisedSupport.java:517)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:207)
    at jdk.proxy2/jdk.proxy2.$Proxy119.updateGatewayStatus(Unknown Source)
    at com.xxx.xxx.xxx.biz.service.task.impl.WorkerServiceImpl.updateGatewaySimulatorStatus(WorkerServiceImpl.java:419)
    at com.xxx.xxx.xxx.biz.service.task.impl.WorkerServiceImpl.startGatewaySimulatorTask(WorkerServiceImpl.java:220)
    at com.xxx.xxx.xxx.biz.service.task.impl.SimulatorTaskDispatchReader.lambda$listen$0(SimulatorTaskDispatchReader.java:50)
    at com.xxx.xxx.xxx.starter.logging.mdc.MdcAwareRunnable.run(MdcAwareRunnable.java:30)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
    at java.base/java.lang.Thread.run(Thread.java:833)
 Caused by: java.lang.ClassNotFoundException: org.springframework.aop.framework.AdvisedSupport$MethodCacheKey
    ... 10 common frames omitted

My business logic is: when a leader become follower, it will cancel re-balance task. I simplify my code as following, as you can see, the task first calls a rest api(jdk proxy) which will cause aop classes loading.

public void reBalanceSimulatorTasks() {
    log.info("First log");
    try {
        Result<List<IotDeviceSimulatorDTO>> result = iotDeviceRemoteSimulatorClientApi.getRunnableGatewayList();
        log.info("Second log");
        // ....
    } catch (Exception ex) {
        log.error("Third log");
    }
}

In correct java services, logs are like this, first print "First log" and then load classes and then print "Second log".

2025-08-06 08:48:37.213 [iot-device-simulator-7565776bcb-xw2sc:8008] [] [re-balance-schedule-0] INFO  c.c.i.c.biz.service.leader.impl.LeaderServiceImpl - First log
 [44.593s][info][class,load] org.springframework.aop.framework.AdvisedSupport$MethodCacheKey source: jar:nested:/app/app.jar/!BOOT-INF/lib/spring-aop-6.2.9.jar!/
 [44.593s][info][class,load] org.springframework.aop.TrueClassFilter source: jar:nested:/app/app.jar/!BOOT-INF/lib/spring-aop-6.2.9.jar!/
 [44.594s][info][class,load] org.springframework.aop.ThrowsAdvice source: jar:nested:/app/app.jar/!BOOT-INF/lib/spring-aop-6.2.9.jar!/
 [44.595s][info][class,load] org.springframework.aop.ProxyMethodInvocation source: jar:nested:/app/app.jar/!BOOT-INF/lib/spring-aop-6.2.9.jar!/
 [44.595s][info][class,load] org.springframework.aop.framework.ReflectiveMethodInvocation source: jar:nested:/app/app.jar/!BOOT-INF/lib/spring-aop-6.2.9.jar!/
 [44.596s][info][class,load] org.springframework.aop.framework.InterceptorAndDynamicMethodMatcher source: jar:nested:/app/app.jar/!BOOT-INF/lib/spring-aop-6.2.9.jar!/
2025-08-06 08:48:37.613 [iot-device-simulator-7565776bcb-xw2sc:8008] [] [re-balance-schedule-0] INFO  c.c.i.c.biz.service.leader.impl.LeaderServiceImpl - "Second log"

In error java services, I can only find "First log" in my log. It seems remote call is interrupted by Future.cancel(true), because after cancel operation, I can't find any more log behind this remote call.

2025-08-06 08:48:37.213 [iot-device-simulator-7565776bcb-dbl6t:8008] [] [re-balance-schedule-0] INFO  c.c.i.c.biz.service.leader.impl.LeaderServiceImpl - First log

I'm 99% sure replacing Future.cancel(true) with Future.cancel(false) can solve this problem. I had tried a lot of times, and the class loading process wasn't interrupted.

Comment From: wilkinsona

Why using RandomAccessFile to read FatJar file after the class loading thread is interrupted

A file channel cannot be used from an interrupted thread whereas RandomAccessFile can. See https://github.com/spring-projects/spring-boot/issues/38611 for background.

If you would like us to investigate why this arrangement apparently isn't working for you, please provide a minimal sample that reproduces your problem.

Comment From: oneby-wang

Hi, when i tried to reproduce the problem, I found this is a issue already fixed by #38611. Huh, our team hardcode the spring-boot-maven-plugin to 3.2.0 and cause some version upgrade misleading on me. My sample is spring-boot-class-loading. Above 3.2.1 the class loading problem is fixed. Thanks for your relay~