您的当前位置:首页正文

JDBCSPI类加载机制

2021-02-18 来源:步旅网
JDBCSPI类加载机制

⼀句话总结:

1 ⾃动加载:DriverManager的静态代码块执⾏的时刻,使⽤该时刻当前线程类加载器加载java.sql.Driver⽂件,并⽤当前线程类加载器加载及、实例化、registerDriver

2 没有⾃动加载的,⼿动forName,传⼊⼀个类加载器及是否初始化;或loadClass󰀀实例化()

3 getConncetion时,校验调⽤getConnection的类所在类加载器与driver实现所在类加载器(校验存放的driver是否属于调⽤者的Classloader), 这个连接通过伪造⼀个中间⼈骗过校验

SPI概述

SPI全称为(Service Provider Interface) ,是JDK内置的⼀种服务提供发现机制;主要被框架的开发⼈员使⽤,⽐如java.sql.Driver接⼝,数据库⼚商实现此接⼝即可,当然要想让系统知道具体实现类的存在,还需要使⽤固定的存放规则,需要在classpath下的META-INF/services/⽬录⾥创建⼀个以服务接⼝命名的⽂件,这个⽂件⾥的内容就是这个接⼝的具体的实现类;下⾯以JDBC为实例来进⾏具体的分析。

JDBC驱动

1.准备驱动包

mysql

mysql-connector-java 5.1.47

org.postgresql postgresql 42.2.2

com.microsoft.sqlserver mssql-jdbc 7.0.0.jre8

分别准备了mysql,postgresql和sqlserver,可以打开jar,发现每个jar包的META-INF/services/都存在⼀个java.sql.Driver⽂件,⽂件⾥⾯存在⼀个或多个类名,⽐如mysql:

com.mysql.jdbc.Driver

com.mysql.fabric.jdbc.FabricMySQLDriver

提供的每个驱动类占据⼀⾏,解析的时候会按⾏读取,具体使⽤哪个会根据url来决定;

2.简单实例

String url = \"jdbc:mysql://localhost:3306/db3\";String username = \"root\";String password = \"root\";

String sql = \"update travelrecord set name=\\'bbb\\' where id=1\";

Connection con = DriverManager.getConnection(url, username, password);

类路径下存在多个驱动包,具体在使⽤DriverManager.getConnection应该使⽤哪个驱动类会解析url来识别,不同的数据库有不同的url前缀;

3.驱动类加载分析

具体META-INF/services/下的驱动类是什么时候加载的,DriverManager有⼀个静态代码块:

static {

loadInitialDrivers();

println(\"JDBC DriverManager initialized\");}

private static void loadInitialDrivers() { String drivers; try {

drivers = AccessController.doPrivileged(new PrivilegedAction() { public String run() {

return System.getProperty(\"jdbc.drivers\"); } });

} catch (Exception ex) { drivers = null; }

// If the driver is packaged as a Service Provider, load it. // Get all the drivers through the classloader // exposed as a java.sql.Driver.class service.

// ServiceLoader.load() replaces the sun.misc.Providers()

AccessController.doPrivileged(new PrivilegedAction() { public Void run() {

ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);【会遍历DriverManage静态代码块执⾏时,当前线程类加载器下找】 Iterator driversIterator = loadedDrivers.iterator();

/* Load these drivers, so that they can be instantiated.

* It may be the case that the driver class may not be there * i.e. there may be a packaged driver with the service class * as implementation of java.sql.Driver but the actual class

* may be missing. In that case a java.util.ServiceConfigurationError * will be thrown at runtime by the VM trying to locate * and load the service. *

* Adding a try catch block to catch those runtime errors * if driver not available in classpath but it's

* packaged as service and that service is there in classpath. */ try{

while(driversIterator.hasNext()) {

driversIterator.next();【重要,在这⼀句forname和实例化】 }

} catch(Throwable t) { // Do nothing }

return null; } });

println(\"DriverManager.initialize: jdbc.drivers = \" + drivers); 【第⼆种⽅式】

if (drivers == null || drivers.equals(\"\")) { return; }

String[] driversList = drivers.split(\":\");

println(\"number of Drivers:\" + driversList.length); for (String aDriver : driversList) { try {

println(\"DriverManager.Initialize: loading \" + aDriver); Class.forName(aDriver, true,

ClassLoader.getSystemClassLoader()); } catch (Exception ex) {

println(\"DriverManager.Initialize: load failed: \" + ex); } }}

在加载DriverManager类的时候会执⾏loadInitialDrivers⽅法,⽅法内通过了两种加载驱动类的⽅式,分别是:使⽤系统变量⽅式和

ServiceLoader加载⽅式;系统变量⽅式其实就是在变量jdbc.drivers中配置好驱动类,然后使⽤Class.forName进⾏加载;下⾯重点看⼀下ServiceLoader⽅式,此处调⽤了load⽅法但是并没有真正去加载驱动类,⽽是返回了⼀个LazyIterator,后⾯的代码就是循环变量迭代器:

private static final String PREFIX = \"META-INF/services/\";

private class LazyIterator implements Iterator {

Class service; ClassLoader loader;

Enumeration configs = null; Iterator pending = null; String nextName = null;

private LazyIterator(Class service, ClassLoader loader) { this.service = service; this.loader = loader; }

private boolean hasNextService() { if (nextName != null) { return true; }

if (configs == null) { try {

String fullName = PREFIX + service.getName(); service:java.sql.Driver if (loader == null)

configs = ClassLoader.getSystemResources(fullName); else

configs = loader.getResources(fullName);【重要,遍历META-INF/servises/java.sql.Driver资源⽂件】 } catch (IOException x) {

fail(service, \"Error locating configuration files\ } }

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 {

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 } ...... }

类中指定了⼀个静态常量PREFIX = “META-INF/services/”,然后和java.sql.Driver拼接组成了fullName,然后通过类加载器()去获取所有类路径(当前类加载)下java.sql.Driver⽂件,获取之后存放在configs中,⾥⾯的每个元素对应⼀个⽂件,每个⽂件中可能会存在多个驱动类,所以使⽤pending⽤来存放每个⽂件中的驱动信息,获取驱动信息之后在nextService中使⽤Class.forName加载类信息,并且指定不进⾏初始化;同时在下⾯使⽤newInstance 触发各驱动的静态代码块()对驱动类进⾏了实例化操作;每个驱动类中都提供了⼀个静态注册代码块,⽐如mysql:

static { try {

java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) {

throw new RuntimeException(\"Can't register driver!\"); }}

这⾥⼜实例化了⼀个驱动类,同时注册到DriverManager;接下来就是调⽤DriverManager的getConnection⽅法,代码如下:

private static Connection getConnection(

String url, java.util.Properties info, Class caller) throws SQLException { /*

* When callerCl is null, we should check the application's * (which is invoking this class indirectly)

* classloader, so that the JDBC driver class outside rt.jar * can be loaded from here. */

ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;【调⽤⽅的类所在类加载器】 synchronized(DriverManager.class) {

// synchronize loading of the correct classloader. if (callerCL == null) {

callerCL = Thread.currentThread().getContextClassLoader(); } }

if(url == null) {

throw new SQLException(\"The url cannot be null\ }

println(\"DriverManager.getConnection(\\\"\" + url + \"\\\")\");

// Walk through the loaded registeredDrivers attempting to make a connection. // Remember the first exception that gets raised so we can reraise it. SQLException reason = null;

for(DriverInfo aDriver : registeredDrivers) {

// If the caller does not have permission to load the driver then // skip it.

if(isDriverAllowed(aDriver.driver, callerCL)) {(中⽤⼀个代理中间⼈骗过了) try {

println(\" trying \" + aDriver.driver.getClass().getName()); Connection con = aDriver.driver.connect(url, info); if (con != null) { // Success!

println(\"getConnection returning \" + aDriver.driver.getClass().getName()); return (con); }

} catch (SQLException ex) { if (reason == null) { reason = ex; } }

} else {

println(\" skipping: \" + aDriver.getClass().getName()); }

}

// if we got here nobody could connect. if (reason != null) {

println(\"getConnection failed: \" + reason); throw reason; }

println(\"getConnection: no suitable driver found for \"+ url);

throw new SQLException(\"No suitable driver found for \"+ url, \"08001\"); }

此⽅法主要是遍历之前注册的DriverInfo,拿着url信息去每个驱动类中建⽴连接,当然每个驱动类中都会进⾏url匹配校验,成功之后返回Connection,如果中途有失败的连接并不影响尝试新的驱动连接,遍历完之后还是⽆法获取连接,则抛出异常;

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- 版权所有