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

355 阅读5分钟

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

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

下面举一个列子:

我们先定义一个接口:

public interface Active {
    String getName();
}

然后定义它的两个实现类

public class ActiveImpl implements Active  {

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

}
public class ActiveImpl2 implements Active {
    @Override
    public String getName() {
        return "ActiveImpl2";
    }
}

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

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

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

然后在测试类中运行

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

![](/Users/shuaizhongxian/Library/Application Support/typora-user-images/image-20200604230633229.png)

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

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

ServiceLoader实现原理:

    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) 方法中,只是对一个初始值的设置

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的迭代器

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

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

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

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

        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()方法。

        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();进行接口的全限定路径查找
    

![](/Users/shuaizhongxian/Library/Application Support/typora-user-images/image-20200604234103581.png)

pending = parse(service, configs.nextElement());找到实现类的全路径

在进行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
        }

然后在nextService中,通过反射加载实例

c = Class.forName(cn, false, loader);

然后把类全限定名和实例放进Map中,从而迭代器中也存储了实例

providers.put(cn, p);

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


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

在rt.jar类库中找到DriverManager类,类中的静态代码块

    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

在上面标红的地方加载Drive.class接口的实现类,那我们的实现类在哪里定义的呢,java并没有规定死我们的JDBC驱动实现

我们找到mysql驱动包

在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!");
        }
    }
}

这样就成功的找到了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虚拟机中提到了这一点,破坏了双亲委派模型。启动类加载器本不应该认识这些代码,现在用一种取巧的方法获取到这些代码。进行了逆向加载。


java spi存在的问题:

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

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