Java SPI & JDBC相关源码解析

Java SPI的具体约定为:

当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。 基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader

应用:common-logging、jdbc4

用JDBC做例子:
JDBC4之前,需要先Class.forName("com.mysql.jdbc.Driver");加载驱动
驱动的加载过程,实际上是通过反射加载com.mysql.jdbc.Driver类,在类静态块中注册驱动:

static {
    try {
        DriverManager.registerDriver(new Driver());
    } catch (SQLException var1) {
        throw new RuntimeException("Can't register driver!");
    }
}

注册过程就是在DriverManager的CopyOnWriteArrayList<DriverInfo>列表中添加一个DriverInfo实例

在调用驱动获取连接(Connection conn=DriverManager.getConnection(url,user,password);)时,实质是遍历这个CopyOnWriteArrayList,尝试用所有已注册的驱动创建连接,并返回第一个非null的连接:

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

JDBC4之后:
不需要再显式通过Class.forName注册驱动,原因:
DriverManager在静态块通过SPI完成了注册过程:

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
    ……        
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            while(driversIterator.hasNext()) {
                driversIterator.next();
            }
            …… 
        }
        ……
    }
}

load方法实际上是调用当前线程类加载器或根加载器加载java.sql.Driver.class创建了一个ServiceLoader实例
在ServiceLoader构造方法中,又通过reload方法创建了一个内部实现的懒加载迭代器LazyIterator的实例
之后通过loadedDrivers.iterator()获取该实例,在遍历方法next()中,完成com.mysql.jdbc.Driver类的加载:
next()方法调用了nextService()方法:

private S nextService() {
    ……
    Class<?> c = null;
    try {
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,"Provider " + cn + " not found");
    }
    ……
}

之所以是限定在/META-INF/services目录,是因为LazyIterator的hasNextServices方法在每个Name前面都添加了这个前缀:

String fullName = PREFIX + service.getName();


自定义实现时可以修改,如Dubbo框架就限定在 /META-INF/dubbo/internal/META-INF/dubbo/META-INF/services目录下:
类名:ExtensionLoader

private Map<String, Class<?>> loadExtensionClasses() {
     ……    
     this.loadFile(extensionClasses, "META-INF/dubbo/internal/");
     this.loadFile(extensionClasses, "META-INF/dubbo/");
     this.loadFile(extensionClasses, "META-INF/services/");
     return extensionClasses;
}

测试:
在 try-catch 块后面一句打断点,debug运行:

查看mysql-connector的META-INF/services/

之所以加载了第一个而不是第二个,是因为测试的时候,使用的url是 jdbc:mysql:// 开头,com.mysql.jdbc.Driver已经可以返回一个非空连接。
即使会尝试让所有驱动都连接一遍:
在FabricMySQLDriver.class的connect方法,可以看到:

return !url.startsWith("jdbc:mysql:fabric://") ? null : super.parseURL(url.replaceAll("fabric:", ""), defaults);

即:测试用的url会让FabricMySQLDriver创建一个null连接,从而被getConnection方法pass掉

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值