java spi第一章:JDBC接口怎么加载自己的实现类

spi的定义

什么是spi,全称为 Service Provider Interface,是java的一种服务发现机制。

当服务的提供者提供了相应的实现之后,我们只需要在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。文件内就是实现类的全路径,这样就ServiceLoader加载时就可以通过反射加载具体的实现,完成模块的注入。

下面举一个列子:

我们先定义一个接口:

1
2
3
public interface Active {
String getName();
}

然后定义它的两个实现类

1
2
3
4
5
6
7
8
public class ActiveImpl implements Active  {

@Override
public String getName() {
return "ActiveImpl";
}

}
1
2
3
4
5
6
public class ActiveImpl2 implements Active {
@Override
public String getName() {
return "ActiveImpl2";
}
}

在META-INF/services/下建立Active的全限定名文件com.example.alipay.active.Active

在文件中输入实现类的全限定名

1
2
com.example.alipay.active.impl.ActiveImpl
com.example.alipay.active.impl.ActiveImpl2

然后在测试类中运行

1
2
3
4
5
6
7
8
9
10
    public static void main(String[] args) {
Class<Active> activeClass = Active.class;

ServiceLoader<Active> serviceLoader=ServiceLoader.load(Active.class);
Iterator<Active> iterator = serviceLoader.iterator();
while (iterator.hasNext()){
Active next = iterator.next();
System.out.println(next.getName());
}
}

这样就轻松的实现了一个java中的spi,根据上诉信息,总结出spi的几个要点:

  • 定义接口和接口实现类
  • 接口实现类所在的jar包放在主程序的classpath中
  • 在META-INF/services/建立接口的全限定名,并且在里面输入实现类
  • 使用ServiceLoader.load(Class<> service)加载接口类,输出相应的实现

spi的实现原理

ServiceLoader实现原理:

1
2
3
4
5
6
7
8
9
10
11
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}

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();
}

在ServiceLoader.load(Class<> service) 方法中,只是对一个初始值的设置

1
2
3
4
5
6
7
8
9
10
11
12
public final class ServiceLoader<S> implements Iterable<S>
//配置文件的路径
private static final String PREFIX = "META-INF/services/";
//加载的服务类或接口
private final Class<S> service;
//已加载的服务类集合
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
//类加载器
private final ClassLoader loader;
//内部类,真正加载服务类
private LazyIterator lookupIterator;
}

ServiceLoader的迭代器

1
2
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();

在测试方法中进行第一行获取迭代值的时候,其实是没有值的,在java spi中实行的是懒加载策略,就是在你需要的时候才进行加载

1
Iterator<Active> iterator = serviceLoader.iterator();

当我们调用iterator.hasNext和iterator.next方法的时候,实际上调用的都是LazyIterator的相应方法。

1
2
3
4
5
6
7
8
9
10
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);
}
}

在调用hasNext的时候,当acc为空的时候,调用了hasNextService()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
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);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
  • String fullName = PREFIX + service.getName();进行接口的全限定路径查找
    
    1
    2

    ![](https://tvax3.sinaimg.cn/large/006EuVaagy1gflac7ocjbj30ox0f5jzj.jpg)
    pending = parse(service, configs.nextElement());找到实现类的全路径
    1
    2
    3
    4
    5
    6


    ![](https://tvax3.sinaimg.cn/large/006EuVaagy1gflachtgiij30nl09gdjo.jpg)


    在进行next迭代的时候,其实最后也是进行hasNextService进行判定,然后取出nextName,此值为在hasNextService时存储的类的全限定名,hasNextService如上面代码所示
    private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); 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 { S p = service.cast(c.newInstance()); providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen }
    1
    2

    然后在nextService中,通过反射加载实例
    c = Class.forName(cn, false, loader);
    1
    2

    然后把类全限定名和实例放进Map中,从而迭代器中也存储了实例
    providers.put(cn, p);
    1
    2
    3
    4
    5
    6
    7
    8
    9

    全部实例迭代完之后,你会发现,迭代器中已经存储了实现类的实例

    ![](https://tvax1.sinaimg.cn/large/006EuVaagy1gflad4d1g9j30to0amn38.jpg)

    ***
    探究一下JDBC是怎么加载自己的实现类的

    在rt.jar类库中找到DriverManager类,类中的静态代码块
    static { loadInitialDrivers(); println("JDBC DriverManager initialized"); }
    1
    2
    3
    4
    5
    6
    7
    8
    9

    ![](https://tvax3.sinaimg.cn/large/006EuVaagy1gfladgpc93j30mf0c5tdi.jpg)
    在上面标红的地方加载Drive.class接口的实现类,那我们的实现类在哪里定义的呢,java并没有规定死我们的JDBC驱动实现

    我们找到mysql驱动包

    ![](https://tvax2.sinaimg.cn/large/006EuVaagy1gfladr21mqj30q20bgtcn.jpg)
    在mysql驱动中,在META-INF/services中找到了接口的实现,NonRegisteringDriver实现了Drive接口的所有方法,Driver实现类中把自己的实例注册到了DriverManager中
    **DriverManager.registerDriver(new Driver());**
    public class Driver extends NonRegisteringDriver implements java.sql.Driver { public Driver() throws SQLException { } static { try { DriverManager.registerDriver(new Driver()); } catch (SQLException var1) { throw new RuntimeException("Can't register driver!"); } } }
    1
    2
    3
    4
    5

    这样就成功的找到了Drive接口具体的实现类,后面就用DriverManager进行管理。


    问题来了,JDBC的Driver接口在rt.jar包中定义的,rt.jar包中使用的是启动类加载器,那启动类加载器并不能直接加载我们用户自定义的类,正常情况下我们用户的定义的类是由应用类加载器加载的,那系统怎么做才能找到我们的实现类呢?
    public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }

    不知道读者注意到

ClassLoader cl = Thread.currentThread().getContextClassLoader();

这个没有,在初始化的时候,获取到当前线程的类加载器实现加载过程,在后面加载类的时候,通过cl中的类加载器进行加载,在DriverManager中用的是启动类加载器,所以在这个地方也是启动类加载加载的Driver的实现类。
在深入理解java虚拟机中提到了这一点,破坏了双亲委派模型。启动类加载器本不应该认识这些代码,现在用一种取巧的方法获取到这些代码。进行了逆向加载。


spi的问题

java spi存在的问题:

  1. 第一就是有些时候我想要对单个实现类进行获取,比如key,value的形式,但是java spi并不提供这种操作。
  2. 每次对ServiceLoader进行操作的时候,都会进行重新加载,实现类通常是无状态的,是否可以存储到spring这样的容器中
  3. 线程不安全

在dubbo中有着自己的spi实现,在后面我们会进行详细的分析。