SPI服务详解
1.数据库连接的过程
在java开发中,数据库连接时必不可少的一个过程,那么数据库的链接过程大体可分为如下几个步骤:
1.数据库URL
在链接数据库使用各种与数据库类型相关的参数,例如主机名、端口号和数据库名。
JDBC URL的一般语法为:
jdbc:subprotocol:other stuff
其中,subprotocol用于选择链接到数据库的具体驱动程序。other stuff参数的格式随所使用的subprotocol不同而不同。
2.驱动程序jar
你需要获取包含你的所使用的数据库的驱动程序的jar文件。
3.启动数据库
启动电脑上的mysql服务
4.注册驱动类
但是到jdk1.6之后不需要用户主动去注册驱动类,这里面通过jdk提供的一种机制可以达到动态注册驱动的效果,而这种机制叫做SPI(Server Provider Interface)机制,本文后续会对SPI做一个详细的介绍。
5.链接数据库
将上面的4,5步骤整合到代码如下:
Class.forName("com.mysql.jdbc.Driver");
DriverManager.getConnection("url","user","password");//链接到数据库并拿到一个链接
2.类加载机制
不是在讲数据库的链接吗?为什么突然说到了类加载机制了?很多读者看到这里可能会带这个疑惑,为什么会讲解类加载机制呢,因为在SPI机制中的底层实现是有用到类加载的。
我们第一次开始编写程序的时候都需要安装java开发包,也就是所谓的jdk,如果你只是运行java程序,那么你就只需要安装jre就行, 对于这两者的区别,这里不过多的介绍。安装完jdk,需要我们去配置相关的环境变量,其中就包括了classpath,那么classpath的配置有什么用呢,感兴趣的可以去看一下: 一篇文章彻底弄懂类加载–源码级别讲解!!!!!!这篇文章。
3.SPI机制
SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。
这种机制和我之前发布的: springboot中的自动装箱原理文章中所提到的spring factory机制有点类似,这种机制还在dubbo中有所体现。SPI服务体现在代码层面如下:
- 案例
需要先写一个接口,我这边定义了一个Moveable 的接口
public interface Moveable {
void move();
}
然后写了两个实现类,实现类不限数量,其中道理,等看完各位应该就能知会:
public class Ship implements Moveable {
@Override
public void move() {
System.out.println("船正在水上跑...");
}
}
public class Tank implements Moveable {
@Override
public void move() {
System.out.println("坦克正在路上打仗...");
}
}
然后在classpath下添加如下文件,文件名字是接口的全限定类名,内容是实现类的全限定类名(这里为什么要这么写?),多个实现类用换行符分隔(这里目前我们只需要关注showtable.two.SPI.Moveable这个文件名)
内容也是showtable.two.SPI.Moveable文件中的:
测试
下面是测试类的编写,在测试类中使用到了java.util包下的ServiceLoader类(这个类就是SPI机制的核心类):
public class SPIDemo1 {
public static void main(String[] args) {
ServiceLoader<Moveable> load = ServiceLoader.load(Moveable.class);
Iterator<Moveable> iterator = load.iterator();
while (iterator.hasNext()){
iterator.next().move();
}
}
}
运行结果:
- 源码解析
在看下面的解析时,我们要思考一下几个问题(最后在总结会进行一个解答):
1.在上面的案例中,为什么需要写一个接口?
2.ServiceLoader迭代器模式的实现?
3.案例中的类是怎么被初始化的?
上面我们写的案例中,我们知道了SPI的机制是通过ServiceLoader类实现的,那我们就去看看在这个类中,都干了些什么事情,先看类结构:
public final class ServiceLoader<S>
implements Iterable<S>
{
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
private LazyIterator lookupIterator;
}
然后再看一个重要的方法的load(),这个方法在JDBC中的DriverManager类中有使用,因此,我们着重看这个方法:
//通过传递过来的class类的全限定名去进行字符串的拼接,然后去加载文件中所写的类
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
load方法创建了一些属性,重要的是实例化了内部类,LazyIterator(迭代器模式的体现: 23种设计模式)。最后返回ServiceLoader的实例:
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
查找实现类和创建实现类的过程,都在LazyIterator完成。当我们调用iterator.hasNext和iterator.next方法的时候,实际上调用的都是LazyIterator的相应方法。
public Iterator<S> iterator() {
return new Iterator<S>() {
public boolean hasNext() {
return lookupIterator.hasNext();
}
public S next() {
return lookupIterator.next();
}
.......
};
}
再去看看LazyIterator中的所做的事情,主要就是两个方法,一个hasNext,一个next,一个经典的迭代模式的实现方式:
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
而在hasNext和next方法中又调用了hasNextService()和nextService()方法,其中hasNextService()方法,里面执行的逻辑如下:
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//将传递进来的需要加载的服务的名字和"META-INF/services/"进行一个拼接,
//比如我们的上面的例子中的“showtable.two.SPI.Moveable”拼接之后就是classpath:/META-INF/services/showtable.two.SPI.Moveable,然后通过类加载器去classpath中去加载对应的类。
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);
}
}
//在这里进行一个判断,如果文件中所编写的类全部加载完成了,那么就返回false,否则返回true
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//通过Class中forname的方法去显示加载类,也就是位于services/META-INF/showtable.two.SPI.Moveable中所写的类,通过全限定名去加载它,这里也能
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 {
//类的初始化,并做一个判断,如果存在这个类返回响应的对象,不然就抛出异常
S p = service.cast(c.newInstance());
//将初始化的类放入ServiceLoader中维护的一个LinkedHashMap容器中
providers.put(cn, p);
//返回初始化的对象
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
也就是说LazyIterator查找实现类和创建实现类的功能其实是在hasNextService()、nextService()这两个方法中实现的。
总结:
通过对ServiceLoader中的源码解析,我们对上面提出的几个问题进行一个解答:
1.在上面的案例中,为什么需要写一个接口?
在调用ServiceLoader中的load()的时候,我们传递的class参数是接口的class对象,也就是说,ServiceLoader是通过接口的全限定名去找classpath中的文件的,这样设计更具有扩展性,可以让我们在使用的时候更加灵活,添加相关的功能类的时候更方便。
2.ServiceLoader迭代器模式的实现?
这个大家自己思考
3.案例中的类是怎么被初始化的?
这个也是自己思考
- SPI在JDBC连接中的使用
我们都知道随着jdk和数据库的发展,我们可以不用显示地去加载数据库驱动了,而加载数据库驱动Class.forName(“com.mysql.jdbc.Driver”),通过执行静态代码块,然后将Driver加入到registeredDrivers容器中,然后就是获取连接执行相关sql语句。
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
public static synchronized void registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException {
/* Register the driver if it has not already been added to our list */
if(driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerException();
}
println("registerDriver: " + driver);
}
上面是传统的JDBC的连接方式,但是这种方式每次去连接数据库的时候都要去注册驱动显得那么的不友好,所以,随着jdk和数据库的发展,我们引入了SPI服务机制,可以让我们不用去显示加载驱动也可以获取连接,而我们所需要做的只是将相应的数据库的jar包导入即可,非常优雅的实现方式!
SPI是在DriverManager调用getConnection()方法的时候出触发的,我们都知道当调用静态方法的时候会执行类中的静态代码,也就是说,在我们调用getConnection()获取连接的时候会调用DriverManager中的静态代码块:
public static Connection getConnection(String url,
java.util.Properties info) throws SQLException {
return (getConnection(url, info, Reflection.getCallerClass()));
}
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
然后在这个代码块中,又调用了loadInitialDrivers()方法,而在loadInitialDrivers()方法中,我们能看到调用了ServiceLoader中的load方法,并将Driver接口作为参数传进去,那么也就是说,我们会ServiceLoader类会去META-INF/service下去加载文件java.sql.Driver。
private static void loadInitialDrivers() {
...................
...................
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
.....................
.....................
}
}
然后就是按照上面讲解SPI机制的工作流程去执行。
通过反射+设计模式+文件所实现的SPI是很优雅、巧妙的。这种方式很好的诠释了面向接口编程的思想,希望大家好好理解,对编程思想还是挺有帮助的,还有就是希望大家能点点赞!!!
(还就是,在这个过程中,其实是使用到了类加载器的,如果不是很明白的,希望大家去看看类加载相关的知识点,对理解SPI服务很有帮助)