Skip to content

Issue Report: Spring Boot 环境下 DebugTools 导致 ClassLoader 污染与 IncompatibleClassChangeError #316

Description

@gofreehj

1. Issue Summary / 缺陷摘要

  • Title: RunTargetMethodRequestHandler 调用 ClassLoaderResourceSyncUtils.syncToSystemClassLoader 破坏 Spring Boot 类加载隔离,导致 IncompatibleClassChangeError
  • Affects: debug-tools-server / debug-tools-agent (tested on v5.2.0)
  • Component: io.github.future0923.debug.tools.server.utils.ClassLoaderResourceSyncUtils
  • Severity: High (会导致目标微服务 JVM 内的 Spring AOP、动态代理及后续所有常规 HTTP 请求抛出不可恢复的运行时异常)

2. Issue Description / 现象描述

在使用 DebugTools 连接 Spring Boot 微服务并远程执行带有动态 AOP / 缓存切面(例如 @Cache / autoload-cache、Spring CGLIB / JDK 动态代理)的方法时,抛出以下异常:

java.lang.IncompatibleClassChangeError: Class com.jarvis.cache.interceptor.aopproxy.CacheAopProxy does not implement the requested interface com.jarvis.cache.aop.CacheAopProxyChain
	at com.jarvis.cache.CacheHandler.proceed(CacheHandler.java:170)
	at com.jarvis.cache.interceptor.CacheMethodInterceptor.invoke(CacheMethodInterceptor.java:58)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691)
	...
	at io.github.future0923.debug.tools.server.netty.handler.RunTargetMethodRequestHandler.run(RunTargetMethodRequestHandler.java:195)
	at io.github.future0923.debug.tools.server.netty.handler.RunTargetMethodRequestHandler.handle(RunTargetMethodRequestHandler.java:157)

不仅通过 DebugTools 发起的调用报错,在此之后通过常规 HTTP 访问微服务相同接口,也会永久报错 IncompatibleClassChangeError,直到 Pod 重启。


3. Root Cause Analysis / 根因剖析

3.1 现场内存字节码诊断

我们在运行中的容器 JVM 中注入诊断 Agent,枚举了内存中所有已加载的 com.jarvis.cache 类信息:

====== CODE SOURCE DIAGNOSE ======
[LOC] CacheHandler @ 76a6f045      | Loader: LaunchedURLClassLoader@4e096385 | Location: jar:file:/usr/share/crm-data.jar!/BOOT-INF/lib/autoload-cache-7.0.8.jar!/
[LOC] CacheAopProxyChain @ 4f402027 | Loader: LaunchedURLClassLoader@4e096385 | Location: jar:file:/usr/share/crm-data.jar!/BOOT-INF/lib/autoload-cache-7.0.8.jar!/

[LOC] CacheAopProxy @ 6030afb      | Loader: sun.misc.Launcher$AppClassLoader@18b4aac2 | Location: jar:file:/usr/share/crm-data.jar!/BOOT-INF/lib/autoload-cache-spring-boot-starter-7.0.8.jar!/
[LOC] CacheAopProxyChain @ 56eaf8d3 | Loader: sun.misc.Launcher$AppClassLoader@18b4aac2 | Location: jar:file:/usr/share/crm-data.jar!/BOOT-INF/lib/autoload-cache-7.0.8.jar!/
====== CODE SOURCE DIAGNOSE END ======

发现关键异常事实
JVM 内存中存在 两个不同的 CacheAopProxyChain 接口 Class 对象

  1. 一个由 Spring Boot 的 LaunchedURLClassLoader 加载;
  2. 另一个由系统的 AppClassLoader 加载;
  3. CacheAopProxy 的 class 声明实现了由 AppClassLoader 加载的 CacheAopProxyChain
  4. CacheHandler 是在 Spring 启动时由 LaunchedURLClassLoader 加载的,其内部代码引用的接口类型属于 LaunchedURLClassLoader

CacheHandler.proceed 执行字节码指令 invokeinterface com/jarvis/cache/aop/CacheAopProxyChain 时,JVM 在运行时依据规范判定:

AppClassLoader 加载的 CacheAopProxy 并没有实现 LaunchedURLClassLoader 加载的 CacheAopProxyChain 接口!

因此严格抛出 IncompatibleClassChangeError

3.2 为什么 AppClassLoader 会去加载 Spring 的嵌套 Jar?

反编译 RunTargetMethodRequestHandler.java

ClassLoader classLoader = AllClassLoaderHttpHandler.getDefaultClassLoader();
if (classLoader != null) {
    ClassLoaderResourceSyncUtils.syncToSystemClassLoader(classLoader); // <-- 元凶
    Thread.currentThread().setContextClassLoader(classLoader);
}

再反编译 ClassLoaderResourceSyncUtils.syncToSystemClassLoader

public static void syncToSystemClassLoader(ClassLoader targetClassLoader) {
    ClassLoader sysCl = ClassLoader.getSystemClassLoader();
    if (targetClassLoader instanceof URLClassLoader && sysCl instanceof URLClassLoader) {
        Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
        addURL.setAccessible(true);
        for (URL url : missingUrls((URLClassLoader)sysCl, ((URLClassLoader)targetClassLoader).getURLs())) {
            addURL.invoke(sysCl, url); // 把 Spring 容器内所有 BOOT-INF/lib/*.jar 强行加入系统类加载器!
        }
    }
}

双亲委派机制断裂链条

  1. AppClassLoader 是 Spring Boot LaunchedURLClassLoader 的父加载器。
  2. 正常情况下,AppClassLoader 只包含启动 jar 包本身(如 crm-data.jar),不能解析内部的 BOOT-INF/lib/。所有业务类、starter 均由 LaunchedURLClassLoader 加载。
  3. 当 DebugTools 调用 syncToSystemClassLoader 后,BOOT-INF/lib/*.jar 的 URL 被反射追加到了 AppClassLoader
  4. 后续一旦触发任何按需类加载(例如执行方法时首次加载切面代理类 CacheAopProxy),LaunchedURLClassLoader 遵循双亲委派优先向上询问父加载器。
  5. 父加载器 AppClassLoader 此时发现自己能够加载这个类,于是截胡加载了 CacheAopProxy 及其关联接口,导致原本应归属于 Spring 上下文的类被拆分到两个互不兼容的 ClassLoader 中,引发 JVM 永久性类加载器污染。

4. Fix / 解决方案

4.1 分析

在执行目标方法时,DebugTools 已经通过如下逻辑明确指定了类加载上下文:

Thread.currentThread().setContextClassLoader(classLoader);
Class<?> targetClass = DebugToolsClassUtils.loadClass(className, classLoader);

DebugTools 直接使用 Spring 的 LaunchedURLClassLoader 加载并执行目标类即可,完全不需要、也不应该通过反射去修改系统类加载器 SystemClassLoaderAppClassLoader)。

4.2 修复代码

ClassLoaderResourceSyncUtils.javasyncToSystemClassLoader 改为安全空操作(No-op):

package io.github.future0923.debug.tools.server.utils;

public class ClassLoaderResourceSyncUtils {

    /**
     * 在 Spring Boot 等模块化/分层类加载环境中,严禁将子加载器的 URL 注入父级 SystemClassLoader,
     * 否则将破坏类隔离并引发 IncompatibleClassChangeError / LinkageError。
     */
    public static void syncToSystemClassLoader(ClassLoader classLoader) {
        // No-op to prevent ClassLoader pollution in Spring Boot / modular environments
    }
}

4.3 验证结果

  1. 将编译后的 ClassLoaderResourceSyncUtils.class 打包回 debug-tools-agent.jar
  2. 重启目标服务 Pod 以清理先前被污染的 JVM。
  3. 挂载打补丁后的 Agent。
  4. 测试带 @Cache / AOP 代理的目标方法(如 findRepairStationgetCityAll),全部调用成功,HTTP 与 DebugTools 远程调用均 100% 正常,无任何类加载冲突异常。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions