Appearance
这是偏门但很实用的一篇:插件化、SPI、可插拔架构都绕不开 ServiceLoader。
ServiceLoader
ServiceLoader 能跑起来的关键有三件事:
META-INF/services
public interface PayStrategy { String name(); void pay(int amount); }
public class WechatPay implements PayStrategy { @Override public String name() { return "wechat"; } @Override public void pay(int amount) { /* ... */ } }
路径:META-INF/services/全限定接口名
META-INF/services/全限定接口名
文件内容:
com.example.pay.WechatPay
ServiceLoader<PayStrategy> loader = ServiceLoader.load(PayStrategy.class); for (PayStrategy s : loader) { System.out.println(s.name()); }
接口在 api.jar,实现类在 impl.jar,META-INF/services/... 必须跟着 实现类 的 jar 走。
api.jar
impl.jar
META-INF/services/...
在容器/插件体系里经常出现:ServiceLoader.load() 找不到实现类。
ServiceLoader.load()
解决方式:明确传入 ClassLoader。
ClassLoader cl = Thread.currentThread().getContextClassLoader(); ServiceLoader<PayStrategy> loader = ServiceLoader.load(PayStrategy.class, cl);
ServiceLoader 的顺序与 jar 扫描顺序相关,不能依赖。 如果有默认实现,建议按 name() 显式选择。
name()
ServiceLoader 通过反射创建实例,必须要无参构造。
某些构建工具会把 META-INF/services 过滤掉, 最终运行时找不到任何实现。
public class PayStrategyFactory { private static final Map<String, PayStrategy> CACHE = new HashMap<>(); static { ServiceLoader<PayStrategy> loader = ServiceLoader.load(PayStrategy.class); for (PayStrategy s : loader) { CACHE.put(s.name(), s); } } public static PayStrategy get(String name) { return CACHE.get(name); } }
这样你就能按业务类型拿到对应实现,而不是靠加载顺序“碰运气”。
如果 SPI 加载失败,我通常按这个顺序排:
ServiceLoader 很轻,但坑不少。 它真正适合的场景是:少量可插拔实现 + 简单配置。
只要把注册文件、类加载器和实现选择这三件事做好, SPI 就能很稳定地跑起来。