3.1.1 Java SPI实现原理与适用场景
SPI(Service Provider Interface,服务提供者接口)是一种服务发现机制,是Java的一个内置标准,可以保障不同的开发者实现某个特定的服务。
SPI的本质是将接口实现类的全限定名配置在文件中,由服务加载器读取配置文件、加载实现类并创建实例。使用SPI机制能够实现运行时从配置文件中读取接口的实现类并创建实例。
我们以实现动态切换登录方式为例讲解如何使用Java SPI,虽然这不是一个很好的使用案例,但是通过此案例能更直观地认识Java SPI。
1. 定义登录接口
定义登录接口LoginService,该接口提供了login方法,login方法可以接收用户名和密码并返回登录结果。接口定义的代码如下。
2. 编写接口实现类
如果想使用Shiro框架实现用户鉴权,那么需要提供一个LoginService的实现类ShiroLoginService。使用ShiroLoginService类的代码如下。
如果想直接使用Spring MVC的拦截器实现用户鉴权,那么需要提供一个LoginService的实现类SpringLoginService。使用SpringLoginService类的代码如下。
3. 通过配置使用SpringLoginService类或ShiroLoginService类
当我们想通过修改配置文件的方式而不是修改代码的方式实现权限验证框架的切换时,就可以使用Java SPI,具体做法是运行时从配置文件中读取LoginService的实现类,然后加载并使用配置的实现类。
首先,在项目的resources目录下创建META-INF目录,并在META-INF目录下创建services目录;然后,在services目录下创建名称为LoginService的配置文件(LoginService是接口的全类名);最后,在配置文件中写入使用的LoginService接口实现类的全类名。
提示:只要在META-INF/services目录下,且文件名是接口的全类名,在编写配置文件内容时,IDEA就会自动提示有哪些实现类。
在配置文件中,填写的内容为接口的实现类,多个实现类使用换行的方式分开。在此案例中,如果想使用ShiroLoginService类,则配置如下。
4. 使用Java SPI加载LoginService
编写main方法,测试使用ServiceLoader加载LoginService,代码如下。
ServiceLoader是Java提供的服务加载器,用于实现SPI机制。ServiceLoader提供load静态方法,该方法接收一个接口并返回一个ServiceLoader实例。通过遍历ServiceLoader实例的迭代器(Iterator),我们可以获取接口对应的配置文件中配置的所有实现类实例。
在调用ServiceLoader#load方法后,此时配置文件中注册的实现类还没有被加载到JVM中,只有通过迭代器遍历获取时,才会加载实现类及实例化实现类,并且遍历的顺序就是配置文件中注册实现类的顺序。
在本例中,我们通过forEach语法遍历ServiceLoader时,使用了break语句,因为在登录场景下,不可能同时使用两种LoginService,所以也不应该在SPI配置文件中配置多个LoginService的实现类。
➢ ServiceLoader实现原理
在调用ServiceLoader#load方法时,ServiceLoader根据参数传入的接口获取接口的全类名,将前缀/META-INF/services与接口的全类名拼接定位到配置文件,然后读取配置文件中的字符串并解析字符串,将解析出来的实现类全类名添加到一个数组中,并返回一个ServiceLoader实例。
ServiceLoader实现了Iterable接口,所以可以使用forEach语法遍历。ServiceLoader使用lazy方式(“懒加载”或“延迟加载”)实现迭代器,只有被迭代器的next方法遍历到的类才会被加载和实例化。如果只想使用接口配置文件中注册的第一个实现类,那么在使用迭代器遍历时,可以使用break语句跳出循环。
在使用迭代器遍历时,ServiceLoader通过调用Class#forName方法加载类并且通过反射创建实例。如果不指定加载实现类使用的类加载器,ServiceLoader就会使用当前线程的上下文类加载器加载。
➢ SPI机制的适用场景
适合使用策略模式、责任链模式的场景都可以使用SPI机制。
例如,将SPI机制用在绘制形状的场景:定义一个形状接口,实现矩形、三角形等的绘制,如果想要添加圆形,只需要在形状接口的配置文件中注册圆形即可支持绘制圆形,完全不用修改任何代码。