根本原因是Spring Boot的LaunchedURLClassLoader未将BOOT-INF/classes和lib资源注册到父加载器,导致Class.forName()等默认使用错误类加载器;应显式传入Thread.currentThread().getContextClassLoader()。

Spring Boot 打包成可执行 Jar 后,用 Class.forName("xxx") 或 ClassLoader.loadClass() 加载类时抛出 ClassNotFoundException,根本原因在于 Spring Boot 的自定义 ClassLoader(LaunchedURLClassLoader)未将 BOOT-INF/classes 和 BOOT-INF/lib 下的资源按传统方式注册到父加载器,导致标准反射机制无法定位类。
使用当前线程上下文类加载器(推荐)
Spring Boot 启动时会将 LaunchedURLClassLoader 设置为当前线程的上下文类加载器(TCCL),这是官方推荐的获取 Class 对象的方式:
- 替换
Class.forName("com.example.MyService")为:Class.forName("com.example.MyService", true, Thread.currentThread().getContextClassLoader()) - 或更简洁地:
Thread.currentThread().getContextClassLoader().loadClass("com.example.MyService") - 注意:不要用
getClass().getClassLoader()(可能返回AppClassLoader)或ClassLoader.getSystemClassLoader()(系统类加载器,不感知 BOOT-INF)
避免在静态上下文中硬编码类加载逻辑
在工具类、静态初始化块或配置类中直接调用 Class.forName 容易忽略类加载器上下文,引发运行时异常:
- ❌ 错误示例:
static { Class.forName("com.example.Plugin"); } - ✅ 正确做法:延迟到运行时,并显式传入 TCCL;或改用 Spring 的
ApplicationContext.getBean(Class<t>)</t>获取已托管的 Bean - 若必须静态加载,确保在 Spring 环境就绪后(如
ApplicationRunner中)再执行
处理第三方库内部反射调用失败
某些框架(如 Jackson、MyBatis、Lombok 插件)会在内部使用 Class.forName,它们默认使用调用栈顶部类的类加载器,可能不匹配:
- 对 Jackson:可通过
ObjectMapper.setBase64Variant(...)等方式绕过,但更稳妥的是自定义Module并设置SimpleModule.setDeserializerModifier(...)控制反序列化行为 - 对 MyBatis:检查
typeAliasesPackage配置是否正确指向BOOT-INF/classes下的包路径;必要时用@MapperScan显式指定 - 通用方案:启动前设置 TCCL:
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());(放在main方法最开始)
调试技巧:确认类是否真实可加载
遇到“找不到类”时,别急着改代码,先验证类是否存在且路径正确:
- 解压 jar,确认
BOOT-INF/classes/com/example/MyService.class存在 - 运行时打印类加载器信息:
System.out.println(Thread.currentThread().getContextClassLoader());
应输出类似org.springframework.boot.loader.LaunchedURLClassLoader@xxxx - 尝试列出所有已知资源:
Thread.currentThread().getContextClassLoader().getResources("com/example/MyService.class")
若返回空枚举,说明路径拼写错误或包结构不匹配

















