前言
在之前的一篇文章【小家Spring】Spring注解驱动开发—Servlet 3.0整合Spring MVC(不使用web.xml部署描述符,使用ServletContainerInitializer)
它介绍了基于注解驱动的Servlet容器的启动。今天刚好回头看到了自己写的这篇文章,自己心里就萌生了几个疑问:原理是啥?为何就能自动的这么样执行呢?通过配置文件就能加载类这肯定涉及到类加载机制吧?
带着这些疑问,就决定深究一番,然后做出如下记录,供读者们参考哈~~~
ServiceLoader:服务提供者加载器
SPI概念介绍
SPI:Service Provider Interfaces(服务提供者接口)。正如从SPI的名字去理解SPI就是Service提供者接口
SPI定位:给服务提供厂商与扩展框架功能的开发者使用的接口。
比如大名鼎鼎的JDBC驱动,Java只提供了java.sql.Driver这个SPI接口,具体的实现由各服务提供厂商(比如MySql、Oracle等)去提供。Mysql的驱动实现类为:com.mysql.jdbc.Driver,Oracle的驱动实现类为:oracle.jdbc.driver.OracleDriver,PostgreSQL 的为:org.postgresql.Driver…
ServiceLoader
首先我们简单的看看javadoc和源码字段说明
/** * 一个简单的服务提供商加载设施 * 服务 是一个熟知的接口和类(通常为抽象类)集合。服务提供者 是服务的特定实现 * 服务提供者可以以扩展的形式安装在 **Java 平台的实现中**.也就是将 jar 文件放入任意常用的扩展目录中 * 也可通过将提供者加入应用程序类路径,或者通过其他某些特定于平台的方式使其可用(所以并不限定你的方式,不在类路径也无所谓哟) */ // @since 1.6 Java6以后才有的工具类 public final class ServiceLoader<S> implements Iterable<S> { // 这个路径非常的重要,最终就是去此路径读取 private static final String PREFIX = "META-INF/services/"; // 指向对象类型的 Class<S> 对象 最后通过 Class<S> 对象来构造服务实现类 S 的实例 s private final Class<S> service; //类加载器 ClassLoader private final ClassLoader loader; private final AccessControlContext acc; private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); private LazyIterator lookupIterator; ... }
ServiceLoader与ClassLoader
ServiceLoader与ClassLoader是Java中2个即相互区别又相互联系的加载器。(ServiceLoader是一种加载类的规范,底层还是依赖于ClassLoader的)
JVM利用ClassLoader将类载入内存,这是一个类生命周期的第一步。(一个java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段,当然也有在加载或者连接之后没有被初始化就直接被使用的情况。 Tips:初始化:给类中的静态变量赋予正确的初始值)
ServiceLoader: 上面JavaDoc里已经有了一些概念性的说明。可以说它的使用是非常松散的,没有太多的强制要求。 唯一强制要求的是,提供者类(实现类)必须具有不带参数的构造方法,以便它们可以在加载中被实例化(因此,子接口肯定不行(下面Demo会验证))
ServiceLoader位于java.util包,ClassLoader位于java.lang包。因此可议看出它俩的定位也是不一样的,ServiceLoader被认为是一种工具
我们可以简单的认为:ServiceLoader也像ClassLoader一样,能装载类文件,但是使用时有区别,具体区别如下:
- ServiceLoader装载的是一系列有某种共同特征的实现类,而ClassLoader是个万能加载器;
- ServiceLoader装载时需要特殊的配置,使用时也与ClassLoader有所区别;
- ServiceLoader还实现了Iterator接口。
ServiceLoader使用方式
毕竟光说不练假把式
我从这个问题的抛出,然后逐步讲解:
一般我们使用接口的实现类都是静态new一个实现类赋值给接口引用,如下:
HelloService service = new HelloImpl();
如果需要动态的获取一个接口的实现类呢?
全局扫描全部的Class,然后判断是否实现了某个接口?代价太大,相信没人愿意去这么做吧。
一种合适的方式就是使用配置文件,把实现类名配置在某个地方,然后读取这个配置文件,获取实现类名。而JDK为我们提供的工具ServiceLoader就是采用的这种方式
(该思想其实在Spring体系内,存在大量的使用,并且我觉得比JDK做得还好~~),当然JDK的好处是:它是规范,更容易广而周知,通用性更强
ServiceLoader它的使用方式可列为4个步骤:
1.创建一个接口文件
2.在resources资源目录下创建META-INF/services文件夹
3.在上面services文件夹中创建文件:以接口全类名命名
4.在该文件内,写好实现类的全类名们
使用Demo如下:
// SPI服务接口 public interface IService { String sayHello(); String getScheme(); } // 主要为了测试,看看是子接口是否会被加载 public interface MyIService extends IService { }
准备实现类(两个),模拟服务提供商:
// 服务提供商的具体实现1:HDFS实现 public class HDFSService implements IService { @Override public String sayHello() { return "Hello HDFSService"; } @Override public String getScheme() { return "hdfs"; } } // 服务提供商的具体实现2:Local实现 public class LocalService implements IService { @Override public String sayHello() { return "Hello LocalService"; } @Override public String getScheme() { return "local"; } }
准备一个服务的配置文件:META-INF/services/com.fsx.maintest.IService,然后在该文件里书写上实现类们,内容如下:
com.fsx.serviceloader.MyIService // 注意这个是接口 com.fsx.serviceloader.HDFSService com.fsx.serviceloader.LocalService
main函数测试:
public static void main(String[] args) { // 加载IService下所有的服务 ServiceLoader<IService> serviceLoader = ServiceLoader.load(IService.class); for (IService service : serviceLoader) { System.out.println(service.getScheme() + "=" + service.sayHello()); } }
报错:
java.util.ServiceConfigurationError: com.fsx.serviceloader.IService: Provider com.fsx.serviceloader.MyIService could not be instantiated
很显然写了一个接口,而接口是不能够实例化的。注意到上面说了唯一一个强制要求,就是必须能够实例化(有空的构造函数) 因此做修改如下(只写实现类):
com.fsx.serviceloader.HDFSService com.fsx.serviceloader.LocalService
运行正常,输出如下:
hdfs=Hello HDFSService local=Hello LocalService
可以看到ServiceLoader可以根据IService把定义的两个实现类找出来,返回一个ServiceLoader的实现,而ServiceLoader实现了Iterable接口,所以可以通过ServiceLoader来遍历所有在配置文件中定义的类的实例。
几个注意事项:
1、文件名称是服务接口类型的完全限定
2、文件内若有多个实现类,每行一个(末尾不要有空格和,等符号)
3、文件必须使用 UTF-8 编码
另外ServiceLoader拿实例提供者是有缓存的,策略如下:
1、服务加载器维护到目前为止已经加载的提供者缓存
2、每次调用 iterator 方法返回一个迭代器,它首先按照实例化顺序生成缓存的所有元素
3、然后以延迟方式查找和实例化所有剩余的提供者,并且依次将每个提供者添加到缓存
4、若清除缓存,可议调用ServiceLoader.reload()方法