一、先说spi的实现过程:
spi由java.util.ServiceLoader类实现:
1、创建接口和其实现类:
public interface Fruit {
void sayHello();
}
public class Banana implements Fruit {
@Override
public void sayHello() {
System.out.println("Hello Banana");
}
}
public class Apple implements Fruit {
@Override
public void sayHello() {
System.out.println("Hello Apple");
}
}
2、在classpath路径下创建目录结构/META-INF/services/
并在services目录下创建文件,名称时接口的全限定名com.spi.Fruit
并在com.spi.Fruit文件中写其实现类的全限定名,一行写一个,如下:
com.spi.Banana
#这是注释
com.spi.Apple
效果如下:
3、先说下spi的大致原理,后面再说测试类FruitTest:
java.util.ServiceLoader<S>
作为spi服务的基类,它内部会维护一个private LazyIterator lookupIterator;
,如下,可见继承Iterator
的LazyIterator
类必须实现hasNext()
和next()
方法,这两个方法的作用如下:
private class LazyIterator implements Iterator<S> {
public boolean hasNext() {
return hasNextService();
}
public S next() {
return nextService();
}
}
来看下hasNextService()
和nextService()
分别做了什么:
//以下为伪代码,只做原理分析
Configs configs = null;
String nextName = null;
private boolean hasNextService() {
//如果已经拿到实现类nextName,则直接返回true
if (nextName != null) {
return true;
}
if (configs == null) {
String fullName = "META-INF/services/" + "META-INF/services/目录下的文件名(也就是接口名)";
//configs实际上获取到了META-INF/services/下文件的内容(也就是实现类的集合)
configs = loader.getResources(fullName);
}
//拿出集合里的第一个实现类放到nextName
pending = parse(service, configs.nextElement());
nextName = pending.next();
return true;
}
private S nextService() {
hasNextService();
//hasNextService()已经把第一个实现类nextName拿到,故而直接加载
Class<?> c = Class.forName(nextName, false, loader);
//直接实例化
S p = service.cast(c.newInstance());
//置空nextName,让hasNextService()从configs集合里拿下一个实现类
nextName = null;
return p;
}
可见, 当调用java.util.ServiceLoader<S>
内部维护的LazyIterator
集合的next()
方法后才会真正的去实例化具体的实现类。
4、根据上面的解析,分析测试类FruitTest,如注释所示:
public class FruitTest {
public static void main(String[] args) {
//ServiceLoader.load()只是去在内部维护一个Iterator,并没有加载任何实现类
ServiceLoader<Fruit> s = ServiceLoader.load(Fruit.class);
//拿到内部维护的Iterator
Iterator<Fruit> iterator = s.iterator();
/*
* 调用hasNext()也就是调用了hasNextService(),
* 第一次会获取实现类名称集合,然后拿出第一个实现类名称nextName
*/
while (iterator.hasNext()) {
/*
* 调用next(),就是去把当前的实现类名称nextName,进行实例化,并返回
*/
Fruit fruit = iterator.next();
//获取到之后,就可以使用了
fruit.sayHello();
}
}
}
二、spi机制的实际应用
1、logback日志,LogbackServletContainerInitializer
实现了servlet3.0的ServletContainerInitializer
接口,当容器启动会自动调用所有实现这个接口的实现类的onStartup()
方法,因此在web.xml文件中根本不用配置相关Listener。
2、据说mysql从jdbc4开始,利用spi机制加载驱动,不用再写Class.forName()
,如下:
下面来详细分析spi机制是如何在jdbc4规范和mysql驱动的加载中发挥作用的:
1、首先,根据spi规范,mysql驱动jar包下肯定已经提供了/META-INF/services/java.sql.Driver
这样的配置文件,并写好了如上图所示的两个实现类。
2、在实现了spi的前提下,省略Class.forName("com.mysql.jdbc.Driver")
,
直接调用java.sql.DriverManager.getConnection(url)
,
根据类的加载规则,先是初始化类的静态内容,然后才开始调用静态方法,
在DriverManager
有个静态块:
static {
loadInitialDrivers();//方法如下
println("JDBC DriverManager initialized");
}
//为了简洁去除部分代码
private static void loadInitialDrivers() {
... ...
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(java.sql.Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
while(driversIterator.hasNext()) {
driversIterator.next();
}
... ...
}
显而易见,DriverManager
类的静态块利用了spi机制,通过ServiceLoader.load(java.sql.Driver.class)
来加载jar包的/META-INF/services/
目录下提供的实现类,并且通过调用driversIterator.next()
方法实例化。
那把com.mysql.jdbc.Driver
实例化之后,DriverManager.getConnection(url)
是如何调用到这个实例的呢?
其实com.mysql.jdbc.Driver
类在实例化之前会先初始化,在初始化时候会执行静态块,如下:
static {
java.sql.DriverManager.registerDriver(new Driver());
}
可见,它又调用回DriverManager
的注册方法把自己的一个实例注册进去了。
最终,DriverManager.getConnection()
就是遍历已经注册的所有实现类,再调用实现类的connect()
方法获取连接。
三、spi解决了什么?如何进行使用?
- spi可以实现解耦,比如sql驱动,只需要按照接口获取链接并执行代码,不用关心具体是mysql还是oracle提供的驱动实现,由导入的jar包或者集成的组件来决定。
- 如果同时引入多个同类的服务组件,无法指定哪个会生效!!
- 并非引入具体组件(jar包)之后就会自动初始化具体的实现类,还先需要有个地方触发ServiceLoader的spi发现功能,这个触发一般是在接口规范类中来做这个事情,而且ServiceLoader的调用跟具体实现类是完全解耦的。
- 使用spi的前提是,接口或者超类规范!目的是,具体服务解耦!如果只是用来初始化和运行具体的类,那么直接new和调用就行。