serviceloader通过读取meta-inf/services/下以接口全限定名命名的文本文件加载实现类,要求实现类public且含无参构造函数;常见错误包括路径错误、文件名不符、类访问权限不足;jdbc驱动利用此机制自动注册;它与spring@service无关,不可混用。

Java里ServiceLoader到底怎么加载实现类
它不靠反射扫描包,也不读META-INF/services/以外的任何配置——只认固定路径下的纯文本文件,文件名必须是接口全限定名,内容是实现类的全限定名,一行一个。
常见错误现象:ServiceLoader.load(XXX.class)返回空迭代器,但明明写了实现类、也打包进去了。原因基本就三个:
- 文件没放在
META-INF/services/目录下(比如放成了resources/META-INF/services/但没被正确打包) - 文件名写错:不是
com.example.MyService,而是MyService或MyService.class - 实现类没声明为public,或没提供无参构造函数(
ServiceLoader用Class.newInstance()老方式实例化,Java 9+虽改用Constructor.newInstance(),但仍要求可访问)
JDBC驱动为什么不用Class.forName()也能自动注册
因为从JDBC 4.0起,规范强制要求驱动jar包在META-INF/services/java.sql.Driver里声明自己。JVM启动DriverManager时会主动调用ServiceLoader.load(Driver.class),触发加载和静态块执行。
这意味着你删掉Class.forName("com.mysql.cj.jdbc.Driver")不会报错,但要注意:
立即学习“Java免费学习笔记(深入)”;
- Java 6–8默认启用SPI加载;Java 9+模块系统下,如果驱动没导出
java.sql.Driver服务,或模块未声明uses java.sql.Driver,就会失效 - 多个驱动共存时,
ServiceLoader按ClassLoader委托顺序遍历,不保证加载顺序,别依赖“先加载哪个”来控制行为 -
DriverManager内部缓存了已注册驱动,重复调用load()不会重复注册,但也不会自动卸载
ServiceLoader和Spring @Service根本不是一回事
前者是JDK原生、无依赖、只做类发现与简单实例化;后者是Spring容器管理的完整Bean生命周期(代理、AOP、依赖注入、作用域等)。混用容易踩坑:
- 用
ServiceLoader加载的实例,Spring完全不知道,无法注入其他Bean,也不能被@Transactional增强 - 如果SPI实现类本身又依赖Spring Bean,得手动从
ApplicationContext里取,破坏了松耦合 - 测试时容易漏掉SPI配置——本地IDE运行可能正常(因为类路径凑巧对),但打成fat jar后
META-INF/services/被覆盖或合并丢失
自定义SPI时ServiceLoader.load()的参数陷阱
必须传接口类型,不能传抽象类或具体类。传错会导致NoClassDefFoundError或空结果,且没有任何提示。
典型误用:
- 传
MyServiceImpl.class——错,ServiceLoader只接受服务接口 - 传
MyService.class但该类是abstract——错,SPI规范要求服务类型是interface - 多模块项目中,服务接口和实现分属不同jar,确保接口jar被实现jar显式依赖(Maven里
<scope>compile</scope>),否则运行时找不到接口类
性能上影响不大,但每次load()都会重新扫描全部jar,频繁调用建议缓存ServiceLoader实例(它本身是线程安全的)。
真正难搞的是跨模块、跨类加载器场景:OSGi或某些容器里,ServiceLoader默认用当前线程上下文类加载器(Thread.currentThread().getContextClassLoader()),如果实现类由另一个类加载器加载,就得手动传入正确的ClassLoader参数。











