这套「三步走」总结得挺准,但原生 SPI 直接拿来做生产级插件化,基本都要在它上面再包一层——要么自己写个 ExtensionLoader,要么直接用 Dubbo SPI / Spring 的方案。

先补几个上面没展开的坑。最要命的是 ServiceConfigurationError 属于「一坏全坏」:配置文件里任何一个实现类加载失败(类名拼错、依赖缺失、构造器抛异常),整个迭代就中断了,不会跳过继续。所以调用方遍历时最好自己 try-catch 兜住,或者干脆放弃 ServiceLoader,手写「读 META-INF/services 文件 → 逐行 Class.forName → 单独 new」的逻辑,把每个插件的失败隔离开。

类加载器这块也要注意。ServiceLoader.load(Class) 默认用的是线程上下文类加载器,在 Tomcat、OSGi、或者你自己给每个插件配独立 ClassLoader 的场景下,很容易因为实现类对调用方不可见而加载不到。插件化本来就是要做隔离的,建议显式传:ServiceLoader.load(PayChannel.class, pluginClassLoader)。

另外手写那个配置文件太容易翻车了——文件名必须严格等于接口全限定名,多一个空格、CRLF 换行、UTF-8 BOM 都可能读不出来。推荐用 Google 的 AutoService,加个 @AutoService(PayChannel.class) 注解,编译期自动生成,省掉这类低级错误。Java 9 模块化项目则是在 module-info.java 里写 provides com.example.PayChannel with com.example.alipay.AlipayChannel。

Java 9 之后还有两个小改进值得知道:一是可以用 provider method 替代无参构造器,类里写个 public static PayChannel provider() 就行;二是 loader.stream().map(Provider::type) 能只拿类型信息不全量实例化,配合 findFirst() 短路,对「只想知道有哪些插件」的场景很省。

如果你的插件需要在加载时读外部配置、做依赖注入、按条件激活,那原生 SPI 那 5 个硬伤一个都绕不过去,直接上 Dubbo 的 ExtensionLoader(@SPI / @Adaptive / @Activate)或者 Spring 的 spring.factories / 新版 META-INF/spring/*.imports 会更省事,本质都是「名字到实现类的注册表 + 缓存」这一套。