~/blog/Java的SPI机制和JDBC

Java的SPI机制和JDBC

发布日期
337414分钟

SPI使用

我们定义了一个IService类,由各个服务提供方提供具体的实现

package com.example;
interface IService {
    void doServe();
}

服务提供方提供的实现:

package com.provider;
class AService implements IService {
    @Override
    void doService(){}
}

class BService implements IService {
    @Override
    void doService(){}
}

Java通过ServiceLoader来进行服务的加载,服务提供方在META-INF/services中定义接口全限定名文件,在其中添加具体的实现类,有多个换行分隔 eg: META/services/com.example.IService

com.provider.AService
com.provider.BService

服务使用方只需要使用ServiceLoader.loadService(IService.class)方法就可以获取所有IService.class的实例:

ServiceLoader<IService> iServices = ServiceLoader.load(IService.class);
for (IService iService : iServices) {
    iService.serve();
}

底层机制

public final class ServiceLoader<S> implements Iterable<S> {
    private static final String PREFIX = "META-INF/services/";
    // 要加载的IService接口
    private final Class<S> service;

    // 类加载器
    private final ClassLoader loader;

    // 缓存的已经完成实例化的IService实例
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 待加载的IService类
    private LazyIterator lookupIterator;
}
// 拼接 META-INF/services + classname,获取所有文件
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);
}

// 遍历文件,readLine
String ln = r.readLine();
if (!providers.containsKey(ln) && !names.contains(ln)) names.add(ln);
return names.iterator();

// 最后通过 Class.forName()完成加载
c = Class.forName(cn, false, loader);
// 实例化,添加缓存中
S p = service.cast(c.newInstance());
providers.put(cn, p);

实例代码 JDBC

服务注册

我们看一下MySQL的Driver实现,注意static块代码,因为是静态的,所以当完成类加载时就会讲Driver注册到DriverManager:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    // 通过DriverManager进行注册
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

服务加载

JDBC中使用java.sql.DriverManager#loadInitialDrivers方法进行所有Driver实现的加载

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// 这里的Iterator是ServiceLoader中的LazyIterator
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
    while(driversIterator.hasNext()) {
        driversIterator.next(); // LazyIterator的next方法会创建一个实例
    }
} catch(Throwable t) {
// Do nothing
}

获取数据库连接

使用DriverManager.getConnection获取数据库连接,遍历所有的Driver,尝试连接,获取成功连接的Connection并返回

for(DriverInfo aDriver : registeredDrivers) {
    if(isDriverAllowed(aDriver.driver, callerCL)) {
        try {
            Connection con = aDriver.driver.connect(url, info);
            if (con != null) {
                return (con);
            }
        } catch (SQLException ex) {
            if (reason == null) {
                reason = ex;
            }
        }
    } else {
        println("skipping: " + aDriver.getClass().getName());
    }
}

打破双亲委派机制

我们说MySQL打破双亲委派机制指的是DriverManager#getConnection方法中,调用ServieLoader.load并不使用当前的类加载器,而是当前线程上下文的类加载器

synchronized(DriverManager.class) {
    if (callerCL == null) {
        callerCL = Thread.currentThread().getContextClassLoader();
    }
}

因为DriverManager位于java.rt包下,而这个包是通过Bootstrap类加载器进行加载的,当在这个类中使用ServiceLoader时也会使用Bootstrap类加载器进行加载服务实现类--这样当然是不可以的,所以在调用getConnection方法是如果不指定类加载器的话会从当前线程上下文中获取到AppClassLoader