Java SPI 实战:ServiceLoader 的正确打开方式(含类加载器坑)
这是偏门但很实用的一篇插件化、SPI、可插拔架构都绕不开ServiceLoader。先说结论ServiceLoader能跑起来的关键有三件事META-INF/services文件必须放在实现类所在的 jar加载时要使用正确的 ClassLoader同一个 SPI 最好只有一个默认实现否则顺序不可控一、最小可用的 SPI 示例1) 定义接口SPIpublicinterfacePayStrategy{Stringname();voidpay(intamount);}2) 实现类publicclassWechatPayimplementsPayStrategy{OverridepublicStringname(){returnwechat;}Overridepublicvoidpay(intamount){/* ... */}}3) 注册文件路径META-INF/services/全限定接口名文件内容com.example.pay.WechatPay4) 加载ServiceLoaderPayStrategyloaderServiceLoader.load(PayStrategy.class);for(PayStrategys:loader){System.out.println(s.name());}二、最容易踩的 5 个坑1) 注册文件放错 jar接口在api.jar实现类在impl.jarMETA-INF/services/...必须跟着实现类的 jar 走。2) 类加载器不对在容器/插件体系里经常出现ServiceLoader.load()找不到实现类。解决方式明确传入 ClassLoader。ClassLoaderclThread.currentThread().getContextClassLoader();ServiceLoaderPayStrategyloaderServiceLoader.load(PayStrategy.class,cl);3) 多实现顺序不可控ServiceLoader的顺序与 jar 扫描顺序相关不能依赖。如果有默认实现建议按name()显式选择。4) 实现类没有无参构造ServiceLoader通过反射创建实例必须要无参构造。5) 打包时资源被过滤某些构建工具会把META-INF/services过滤掉最终运行时找不到任何实现。三、一个“够用”的选择器写法publicclassPayStrategyFactory{privatestaticfinalMapString,PayStrategyCACHEnewHashMap();static{ServiceLoaderPayStrategyloaderServiceLoader.load(PayStrategy.class);for(PayStrategys:loader){CACHE.put(s.name(),s);}}publicstaticPayStrategyget(Stringname){returnCACHE.get(name);}}这样你就能按业务类型拿到对应实现而不是靠加载顺序“碰运气”。四、排查清单实战版如果 SPI 加载失败我通常按这个顺序排META-INF/services文件是否打进 jar文件内容是否是实现类全限定名实现类是否在 classpathClassLoader 是否正确是否存在多个 jar 提供同名实现最后总结ServiceLoader很轻但坑不少。它真正适合的场景是少量可插拔实现 简单配置。只要把注册文件、类加载器和实现选择这三件事做好SPI 就能很稳定地跑起来。