Java SPI机制
文章目录
- Java SPI机制
- 前言
- 代码示例
- ServiceLoader类源码解析
- 成员
- load部分
- 迭代部分
- JDBC连接数据库中的SPI
- SPI在数据库连接中的具体体现
- 总结
前言
SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。 目前有不少框架用它来做服务的扩展发现, 简单来说,它就是一种动态替换发现的机制, 举个例子来说, 有个接口,想运行时动态的给它添加实现,你只需要添加一个实现,然后,把新加的实现,描述给JDK知道就行啦(通过改一个文本文件即可)
代码示例
使用Eclipse完成一个简单的SPI例子需要三个步骤
1.完成一个接口(Animal),和两个接口的实现类 (Dog和Cat);
Animal接口:
package com.mec.aboutSPI;
public interface Animal {
void shout();
}
Dog类:
package com.mec.aboutSPI;
public class Dog implements Animal{
public Dog() {
}
@Override
public void shout() {
System.out.println("wangwang");
}
}
Cat类
package com.mec.aboutSPI;
public class Cat implements Animal {
public Cat() {
}
@Override
public void shout() {
System.out.println("miaomiao");
}
}
2.在src目录下建立META-INF目录、再在META-INF目录下建立services目录
(具体为什么要这样建立,大家先接受就好,具体看到源码就明白了),在services目录下建立一个文件,文件的名字是接口的全限定名(包名 + 类名)。文件的内容为接口实现类的全限定名。一行写一个。
3.在主函数中进行测试
package com.mec.aboutSPI;
import java.util.ServiceLoader;
public class Text {
public static void main(String[] args) {
ServiceLoader<Animal> services = ServiceLoader.load(Animal.class);
for (Animal service : services) {
service.shout();
}
}
}
实验结果
ServiceLoader类源码解析
声明:关于System里面的方法,以我目前的能力没有办法看的很透彻,所以我只能大概的讲一讲里面的逻辑。
成员
private static final String PREFIX = "META-INF/services/";
// The class or interface representing the service being loaded
//需要寻求服务的接口类型
private final Class<S> service;
// The class loader used to locate, load, and instantiate providers
//该类加载器用户加载实现类
private final ClassLoader loader;
// The access control context taken when the ServiceLoader is created
//这个说实话,我也不知道干啥的,哈哈哈,不过有兴趣的可以百度查一查
private final AccessControlContext acc;
// Cached providers, in instantiation order
//键:配置文件中每一行的内容,即实现类的全限定名
//值:强转成接口类型的实现类对象
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
//这个就是个迭代器,LazyIterator 是一个内部类
private LazyIterator lookupIterator;
load部分
load方法是ServiceLoader的一个静态方法,也是我们举的例子的主函数的第一步
public static <S> ServiceLoader<S> load(Class<S> service) {
//得到当前线程的上下文加载器,关于上下文文加载器,我会后续的写一篇关于上下文加载器的博客
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
//判断svc是否为null,是的话报错,不是的话将它赋值给service
service = Objects.requireNonNull(svc, "Service interface cannot be null");
//判断类加载器cl是不是null,是的话,将系统类加载器赋值给loader,不是的话将cl赋值给它。
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
public void reload() {
//清空该Map
providers.clear();
//初始化迭代器,该类是ServiceLoader的一个内部类
lookupIterator = new LazyIterator(service, loader);
}
迭代部分
通过我们的例子可以明白,我们通过迭代遍历来使用每个实现类
这里我就不把所有源码展示出来了,因为我目前没有能力将每一行都解释出来。只能看个大概意思
- 读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称,值得注意的是,ServiceLoader可以跨越jar包获取META-INF下的配置文件。
下面的代码可以间接的理解成hasNext方法下的内容
try {
//PREFIX是一个常量,就是 META-INF/services/
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
- 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化,并且强传成该类实现的接口的类型, 最终加入到Map中。
下面的代码可以间接的理解成next方法下的内容
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//对该类进行加载
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
//如果该类不是改接口的实现类,报出异常
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
//将该实现类的对象的类型强传成接口类型,并加入到Map中
S p = service.cast(c.newInstance());
providers.put(cn, p);
上述代码中有这么一行 c = Class.forName(cn, false, loader);
这里面的loader是上下文类加载器,首先我们要明确一个概念:假设A类中有对B类和C类的引用,如果B类和C类并没有被类加载器所加载,系统会使用加载A类的类加载器在双亲委托机制下去加载B类和C类。
**ServiceLoader类是由根类加载器即启动类加载器加载的,所以在该类中的引用的其它类也要使用根类加载器,但是根类加载器加载的是系统类,所以在ServiceLoader中引用的非系统类无法被加载,比如数据库驱动Driver类,这也算是双亲委托机制的一个局限性,所以为了打破这种尴尬,产生了线程上下文类加载器,这也就是上下文类加载器所存在的意义。初始线程的上下文类加载器就是系统类加载器。**关于上下文类加载,大家尽情期待我的下一篇博文吧!
JDBC连接数据库中的SPI
使用SPI的优点:
降低了程序的耦合性,比如你想替换一个接口的实现方式,你只需要更改配置文件就可以了,就拿连接数据库来说吧。
//我们连接数据库一般第一行都会写这个,目的是加载第三方架包的驱动
//如果使用SPI的话,这一行是可以不用写的,具体原因请往下看
Class.forName("com.mysql.jdbc.Driver");
下面的图片是将连接数据库jar包解压结果:
我们发现它也存在META-INF目录
再看看它的配置文件
配置文件的名称是Java核心包中的接口
配置文件的内容是实现类的全限定名
通过SPI的方式,第三方服务模块实现接口后,在第三方的项目代码的META-INF/services目录下的配置文件指定实现类的全路径名,源码框架即可找到实现类。
SPI在数据库连接中的具体体现
在rt.jar目录下有一个叫做java.sql的包,这是Java系统为用户提供的连接数据库的接口,以及少部分的实现类,我认为这就是Java提供给数据库厂商的API,数据库厂商去实现这些接口,最终做成jar包供用户使用。
我们温故一下连接数据库的操作
//我们连接数据库一般第一行都会写这个,目的是加载第三方架包的驱动
//如果使用SPI的话,这一行是可以不用写的,具体原因请往下看
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection(url, root, password);
//这样就算是连接上了
);
getConnection方法是DriverManager类的一个静态方法,当调用它时,会对DriverManager类进行初始化,当然会提前进行加载,初始化时会执行该类静态块的内容。
private static void loadInitialDrivers() {
String drivers;
try {
/*
先粗略说一下AccessController.doPrivileged,这就要涉及到Java安全模式,JVM为了防止
外部的代码可以随意访问本地资源设定了不同的权限和不同的域,类加载器加载类时,会把不同权限的类
加载道不同的域里,也就是说,一个类拥有它所在域的所有权限。如果一个域中的类进行了该域没有权限的操作
就会报错。为了外部的扩展,提供了doPrivileged方法,该方法执行的操作与其说是无视权限,还不如说它是拥有极高的权限
这部分的内容很深,很复杂,不我这样的小博主能解释清楚的,我认为当下的学习了解这是跟安全有关的就行。
*/
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
/*
这里我们希望得到连接数据库的驱动的全限定名(com.mysql.jdbc.Driver);
这块我写了些代码做了测试,如果你提前没有进行
System.setProperty("jdbc.drivers","com.mysql.jdbc.Driver");操作
那你得到会是null,下面还有一层保险,就是使用SPI的方式
*/
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//这里的内容在博文的前面已经说过,不再多说
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
//这个方法中有一行执行了Class.ForName()的操作
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
//System.getProperty("jdbc.drivers");返回值为null所以程序该方法结束了
if (drivers == null || drivers.equals("")) {
return;
}
//如果你很早就进行了设置,那么这个将会对得到的字符串进行处理,并最终加载com.mysql.jdbc.Driver类
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
总结
SPI可以同过配置文件的方式,加载一个类或者,得到该实现类的对象,降低了程序间的耦合度。方便了我们对接口实现方式的替换,据说ServiceLoader多线程时不安全。