spi不解决泛型擦除,需通过注解、静态方法、匿名子类或配置元数据等方式让实现类主动携带并暴露实际类型信息,从而在运行时还原泛型。

SPI 本身不解决泛型擦除问题,它只负责按约定发现并加载实现类。所谓“还原运行时实际配置类型”,关键不在 SPI 加载动作,而在于你如何让被加载的实现类**主动携带可识别的泛型结构信息**,并在使用时通过反射+类型签名手段提取它。
让 SPI 实现类显式声明泛型契约
不要依赖接口定义中的泛型(如 ConfigProvider<t></t>),因为接口被擦除后,SPI 加载到的实例无法体现 T 是什么。正确做法是:让每个具体实现类自己表明它支持的配置类型。
- 在实现类上加自定义注解,例如
@ConfigType(MyAppConfig.class),运行时通过clazz.getAnnotation(ConfigType.class).value()直接拿到 Class 对象 - 或强制实现类提供静态方法
Class> getSupportedConfigType(),SPI 加载后调用该方法获取类型 - 避免把泛型参数塞进接口定义里,否则擦除后所有实现都变成
ConfigProvider<object></object>,无法区分
用 TypeToken 模式捕获带参类型(适用于配置工厂类)
如果 SPI 加载的是一个泛型工厂(如 ConfigFactory<t></t>),且你控制其实现方式,可用匿名子类技巧保留泛型签名:
- 定义 SPI 接口为非泛型:
public interface ConfigFactoryProvider { ConfigFactory> create(); } - 具体实现写成:
new ConfigFactoryProvider() { public ConfigFactory<myappconfig> create() { return new ConfigFactory<myappconfig>() {}; } }</myappconfig></myappconfig> - 在
create()方法内,通过((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]提取MyAppConfig.class - 这个技巧依赖匿名内部类会保留泛型父类签名,是绕过擦除的成熟实践
结合配置元数据做类型绑定
很多配置场景中,“实际类型”其实由外部配置决定(如 YAML 中的 type: com.example.MyAppConfig)。这时 SPI 只需加载通用解析器,再按配置字符串动态加载对应 Class:
- SPI 加载一个
ConfigDeserializer,它不带泛型,但接受String configTypeClassName参数 - 运行时读取配置文件中的 type 字段,用
Class.forName(configTypeClassName)加载真实类型 - 再用 Jackson/Gson 的
TypeReference或TypeToken构造完整泛型类型进行反序列化 - 这样“类型还原”发生在配置解析阶段,而非 SPI 加载阶段,逻辑更清晰、更可控
避免常见误区
别指望 SPI 服务加载器能自动推断泛型——ServiceLoader.load(ConfigProvider.class) 返回的全是原始类型,擦除已发生,无法挽回。
- 不要在
META-INF/services/文件里写带泛型的类名(如com.example.MyProvider<string></string>),语法非法 - 不要试图对加载后的实例做
instanceof ConfigProvider<myappconfig></myappconfig>,编译不通过 - 不要在 SPI 接口方法签名里用泛型参数传递配置类型,例如
<t> T loadConfig(Class<t> type)</t></t>,这会让调用方承担类型传递责任,脱离 SPI 本意











