classgraph更可靠因其支持显式传入任意classloader并链式委托,而reflections仅依赖系统类加载器,在osgi等场景下无法扫描插件类。

ClassGraph 在多 ClassLoader 环境下为什么比 Reflections 更可靠
因为 Reflections 严重依赖 ClassLoader.getSystemResources(),在 OSGi、Spring Boot DevTools、JRebel 或自定义 URLClassLoader 嵌套场景中,它默认只扫系统类加载器路径,根本看不到你放在 lib/ 子目录或模块隔离区里的注解类。ClassGraph 则允许显式传入任意 ClassLoader 实例,甚至支持链式委托(比如把 Thread.currentThread().getContextClassLoader() 和你的插件 PluginClassLoader 同时注册),真正按需扫描。
实操建议:
- 永远用
new ClassGraph().enableAnnotationInfo().addClassLoader(yourClassLoader)显式指定,别依赖默认行为 - 如果类加载器有父子关系(如 Tomcat 的
WebAppClassLoader→SharedClassLoader),把父加载器也加进去:.addClassLoader(parentCl).addClassLoader(childCl) - 避免调用
scan()前没设.enableAnnotationInfo()—— 这个开关不打开,findClassesWithAnnotation()返回空列表,且无任何警告
如何用 ClassGraph 扫出被 @Component 标记但不在 classpath 根路径的类
典型场景:插件 jar 放在 plugins/my-plugin.jar,主程序通过 URLClassLoader 加载,但 ClassGraph 默认不扫描 jar 文件内部,除非你告诉它“去哪找”。
实操建议:
- 不要只依赖
addClassLoader();对每个插件 jar,用.acceptPaths("plugins/")或.acceptUrls("file:///path/to/plugins/my-plugin.jar") - 若插件 jar 被加密或封装成自定义格式(如
.plug),需先解包到临时目录,再用.acceptPaths(tempDir.getAbsolutePath()) - 扫描前加
.verbose()(仅开发期)看实际加载了哪些 URL —— 常见错误是路径末尾漏了/,导致acceptPaths("plugins")不匹配plugins/my-plugin.jar
扫描结果怎么安全注入 Spring 容器而不触发重复注册或 BeanDefinition 冲突
直接调用 ctx.registerBean(...) 很危险:Spring 可能已通过 @ComponentScan 注册同名 bean,或多个插件扫描出相同接口实现,造成 NoUniqueBeanDefinitionException。
实操建议:
- 不用
ApplicationContext.registerBean(),改用BeanDefinitionRegistry+ 手动构造RootBeanDefinition,并设置setRole(BeanDefinition.ROLE_INFRASTRUCTURE)或ROLE_APPLICATION明确意图 - 给每个动态注册的 bean 名加前缀,比如
"plugin-" + className + "-" + pluginId,避免命名冲突 - 检查是否已存在同类型 bean:
ctx.getBeanNamesForType(MyService.class),若已有且非代理类,跳过注册或抛明确异常 - 务必在
ApplicationContextInitializer或BeanFactoryPostProcessor阶段注册,不能等到ApplicationRunner—— 那时容器已冻结,registerBean()会静默失败
Reflections 仍有价值的唯一场景:纯 JDK 类路径 + 极简启动
如果你的应用打包为单个 fat jar,没有热部署、没有插件机制、所有类都由同一个 LaunchedURLClassLoader 加载,且项目已用 guava,那么 Reflections 启动快、API 简单,new Reflections("com.example", new TypeAnnotationsScanner()) 确实够用。
但注意两个硬伤:
-
Reflections不支持 Java 9+ 的模块路径(--module-path),遇到java.lang.module.FindException就停摆 - 它的缓存基于类加载器 identity,一旦
ClassLoader被回收(如 DevTools 重启),旧缓存不会自动清理,可能返回已卸载类的残留引用
复杂类加载环境里,ClassGraph 的 ScanResult.close() 和显式生命周期管理,才是可控性的底线。










