ServiceLoader 本身不提供“变量缓存筛选”能力,仅按约定加载服务实现类;高性能变量缓存需由业务接口、实现类与外部缓存(如 Caffeine)协同实现,关键在于服务实例的缓存生命周期管理。

直接说结论:ServiceLoader 本身不提供“变量缓存筛选”能力,它只负责按约定加载服务实现类;所谓“高性能变量缓存筛选”,需由你定义的业务接口 + 实现类 + 外部缓存(如 Caffeine、Guava Cache 或 MemoryCache)协同完成。关键不是用不用 uses,而是怎么让 ServiceLoader 加载出来的服务实例,能安全、高效地参与缓存生命周期管理。
一、uses 声明只是编译期提示,不参与运行时加载
uses 出现在 module-info.java 中,仅用于向模块系统声明“本模块会使用某接口的服务”,它:
– 不触发任何类加载或实例化
– 不影响 ServiceLoader 的查找路径或行为
– 仅在模块图解析、JLink 打包或依赖检查(如 jdeps --check-deps)时起作用
真正起作用的是:
– META-INF/services/xxx.ServiceInterface 文件是否存在于 classpath
– 文件中写的全限定类名是否可被当前类加载器定位
– 对应实现类是否满足 public + 无参构造器 + 包路径完全匹配
二、ServiceLoader 加载的服务如何接入变量缓存
假设你要缓存一组“动态变量规则”,比如不同环境(dev/test/prod)下对同一配置项的取值策略:
- 定义统一接口:
public interface VariableResolver { String resolve(String key, Mapcontext); } - 多个实现类(如
DevVariableResolver、ProdVariableResolver)分别打包在不同模块中,并各自提供META-INF/services/com.example.VariableResolver配置文件 - 主模块通过
ServiceLoader.load(VariableResolver.class)获取全部实现,再根据context.get("env")筛选目标实例 - 将筛选出的实例包装进缓存(注意:缓存的是“策略对象”,不是原始字符串值)
示例代码片段:
// 缓存策略对象,避免每次 ServiceLoader.iterator() 遍历
private final LoadingCache<string variableresolver> resolverCache =
Caffeine.newBuilder()
.maximumSize(16)
.build(env -> {
return ServiceLoader.load(VariableResolver.class)
.stream()
.filter(r -> r.getClass().getSimpleName().toLowerCase().contains(env))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("No resolver for env: " + env));
});
</string>
三、高性能的关键细节(容易踩坑)
ServiceLoader 本身轻量,但组合缓存后性能瓶颈常出现在这几处:
-
类加载器错位:默认用
Thread.currentThread().getContextClassLoader(),若你在 Spring Boot 的 @PostConstruct 或 WebMvcConfigurer 中调用,需显式传入getClass().getClassLoader()防止跨模块找不到资源 -
缓存键设计不合理:不要用
env + timestamp这类易变字段做 key;推荐用env + version(version 来自模块的MANIFEST.MF或module-info.java中的open module xxx { requires ... }版本声明) -
实现类状态污染:所有
VariableResolver实现必须是无状态的(stateless),否则缓存复用会导致并发问题;如有必要持内部状态,应使用ThreadLocal或ConcurrentHashMap隔离 -
未预热导致冷启动抖动:在应用启动完成前(如 Spring 的
ApplicationRunner),主动调用一次resolverCache.get("prod"),让缓存和 ServiceLoader 都完成初始化
四、替代方案建议(比纯 ServiceLoader 更适合变量场景)
如果变量筛选逻辑复杂(如支持表达式、权重路由、灰度比例),ServiceLoader 显得笨重。更推荐:
-
基于 SPI 的工厂模式:用
ServiceLoader加载VariableResolverFactory,由工厂根据上下文返回带缓存能力的 resolver 实例 - 模块化配置中心集成:把变量规则写成 YAML/JSON,放在 Nacos/Apollo,各模块监听变更并刷新本地缓存 —— 此时 ServiceLoader 只用于加载不同格式解析器(YamlResolver、JsonResolver)
-
JPMS + Service Provider + ModuleLayer:Java 9+ 中可动态构建
ModuleLayer,按需加载含特定provides声明的模块,比传统 classpath 扫描更可控










