[TOC]
SPI 在jdbc driver的運用
這幾天在看java 類加載機制,看到 spi 服務機制破壞了雙親委派模型,特地研究了下典型的 spi 服務 jdbc 驅動
首先運行一下代碼,查看 mysql jdbc 驅動的類加載(maven 項目已經引進 jdbc 驅動依賴,版本為5.1.41)
public static void main(String[] args)
{
Enumeration<Driver> drivers = DriverManager.getDrivers();
Driver driver;
while (drivers.hasMoreElements())
{
driver = drivers.nextElement();
System.out.println(driver.getClass() + "------" + driver.getClass().getClassLoader());
}
System.out.println(DriverManager.class.getClassLoader());
}
輸出結果如下:
class com.mysql.jdbc.Driver------sun.misc.Launcher$AppClassLoader@2a139a55
class com.mysql.fabric.jdbc.FabricMySQLDriver------sun.misc.Launcher$AppClassLoader@2a139a55
null
可以看到代碼中并沒有調用 Class.forName(“”)的代碼,但DriverManager中已經加載了兩個 jdbc 驅動,而卻這兩個驅動都是使用的應用類加載器(AppClassLoader)加載的,而DriverManager本身的類加載器確是 null 即BootstrapClassLoader,按照雙親委派模型的規則,委派鏈如下:
SystemApp class loader -> Extension class loader -> Bootstrap class loader
,父加載器BootstrapClassLoader是無法找到AppClassLoader加載的類的,此時使用了線程上下文加載器,Thread.currentThread().setContextClassLoader()可以將委派鏈左邊的類加載器,設置為線程上下文加載器,此時右邊的加載器就可以使用線程上下文加載器委托子加載器加載類
可以查看DriverManager的源碼
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
sun.misc.Providers()
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} 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在初始化時會使用ServiceLoader來加載java.sql.Driver的實現類,此處就是 spi 服務的思想
查看 ServiceLoader 的load 代碼
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(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,使用 reload 方法來加載,ServiceLoader 的主要參數與 reload 的代碼如下:
private static final String PREFIX = "META-INF/services/";
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
LazyIterator是一個懶加載的迭代器,看一下這個迭代器的實現:
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> 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();
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;
}
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
}
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);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
回頭查看DriverManager的初始化代碼,可以看到如下代碼:
while(driversIterator.hasNext()) {
driversIterator.next();
}
可以看出DriverManager會循環調用所有在META-INF/services/java.sql.Driver下定義了所有類的 Class.forName()方法
那么這些加載的驅動是如何被注冊在DriverManager中的?我們看 mysql 的驅動 Driver 的實現類 可以看到 Driver的實現在初始化時就進行了注冊,代碼如下:
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
這段代碼即可將 java.sql.Driver 的實現類注冊進DriverManager,注意此段代碼中 new Driver()是com.mysql.jdbc.Driver
最后查看下實現 spi 服務必不可少的文件 META-INF/services/java.sql.Driver(這個特定用來實現 java.sql.Driver 的接口的 spi 服務)這個文件中內容如下:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
可以看到這兩個類即為文章開頭實驗的那兩個 jdbc 驅動
注意并不是所有版本的 jdbc 驅動都實現了 spi 服務,應該是5.1.5及之后的版本才實現了這種服務,之前的版本還是需要手動調用 Class.forName 方法來加載驅動,還有好像 ojdbc 的驅動均沒有實現 spi 服務
搞清楚了 spi 服務于 DriverManager 加載的過程,我們可以自己嘗試實現一個簡單的 jdbc 驅動(僅僅實現了類加載的部分)
使用 maven 工程,新建類com.lcy.mysql.Driver
public class Driver implements java.sql.Driver
{
static
{
try
{
DriverManager.registerDriver(new com.lcy.mysql.Driver());
}
catch (SQLException e)
{
throw new RuntimeException("register driver fail");
}
}
@Override
public Connection connect(String url, Properties info)
throws SQLException
{
// TODO Auto-generated method stub
return null;
}
@Override
public boolean acceptsURL(String url)
throws SQLException
{
// TODO Auto-generated method stub
return false;
}
@Override
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info)
throws SQLException
{
// TODO Auto-generated method stub
return null;
}
@Override
public int getMajorVersion()
{
// TODO Auto-generated method stub
return 0;
}
@Override
public int getMinorVersion()
{
// TODO Auto-generated method stub
return 0;
}
@Override
public boolean jdbcCompliant()
{
// TODO Auto-generated method stub
return false;
}
@Override
public Logger getParentLogger()
throws SQLFeatureNotSupportedException
{
// TODO Auto-generated method stub
return null;
}
}
僅僅寫了一個初始化方法,其他方法均使用默認空實現,在 src/mian/resources 目錄下新建文件 /META-INF/services/java.sql.Driver 填入內容com.lcy.mysql.Driver 打包發布
在之前的文章開始的測試工程中引入工程依賴(如果是同一工程,直接運行即可),運行可以看到結果如下:
class com.mysql.jdbc.Driver------sun.misc.Launcher$AppClassLoader@2a139a55
class com.mysql.fabric.jdbc.FabricMySQLDriver------sun.misc.Launcher$AppClassLoader@2a139a55
class com.lcy.mysql.Driver------sun.misc.Launcher$AppClassLoader@2a139a55
null
可以看到,已經加載了我們自定義的com.lcy.mysql.Driver(雖然這個加載器沒有實現任何功能,但測試 spi 機制的目的已經實現)
JDBC驅動加載機制
說道JDBC我們寫Java的程序員實在是太過熟悉了,如今的后端系統不論大小幾乎都抹不開和數據庫存在聯系。
JDBC是一個連接數據庫的Java API,包含了相關的接口和類。但是,他不提供針對具體數據庫(MySQL、MS、Oracle)的實際操作,而只是提供了接口,以及調用框架。和具體數據庫的直接交互由對應的驅動程序完成,比如mysql的mysql-connector、oracle的ojdbc、MS的sqljdbc等。
jdbc連接過程
1、加載JDBC驅動程序:
Class.forName("com.mysql.jdbc.Driver") ;
2、提供JDBC連接的URL
String url = jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8
3、創建數據庫的連接
Connection con =
DriverManager.getConnection(url , username , password ) ;
4、創建一個Statement
PreparedStatement pstmt = con.prepareStatement(sql) ;
5、執行SQL語句
ResultSet rs = stmt.executeQuery("SELECT * FROM ...") ;
6、處理結果
while(rs.next()){
//do something
}
7、關閉JDBC對象
Class.forName作用
我們都知道,也聽了無數遍,驅動的加載是由Class.forName 方法完成的。
但是,讓我們深究一下,Class.forName是JSE里面加載一個類到JVM內存的方法,為什么又會關聯了JDBC的驅動加載邏輯呢?
確實JDBC驅動的加載是在Class.forName這一步完成的,但是完成這個工作的是加載的具體的數據庫驅動類的靜態初始化塊完成的。
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
}
由于JVM對類的加載有一個邏輯是:在類被需要的時候,或者首次調用的時候就會把類加載到JVM。反過來也就是:如果類沒有被需要的時候,是不會被加載到JVM的。
當連接數據庫的時候我們調用了Class.forName語句之后,數據庫驅動類被加載到JVM,那么靜態初始化塊就會被執行,從而完成驅動的注冊工作,也就是注冊到了JDBC的DriverManager類中。
由于是靜態初始化塊中完成的加載,所以也就不必擔心驅動被加載多次
拋棄Class.forName
在JDBC 4.0之后實際上我們不需要再調用Class.forName來加載驅動程序了,我們只需要把驅動的jar包放到工程的類加載路徑里,那么驅動就會被自動加載。
這個自動加載采用的技術叫做SPI,數據庫驅動廠商也都做了更新。可以看一下jar包里面的META-INF/services目錄,里面有一個java.sql.Driver的文件,文件里面包含了驅動的全路徑名。
比如mysql-connector里面的內容:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
那么SPI技術又是在什么階段加載的數據庫驅動呢?看一下JDBC的DriverManager類就知道了。
public class DriverManager {
static {
loadInitialDrivers();//......1
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);//.....2
Iterator driversIterator = loadedDrivers.iterator();
//.....
}
上述代碼片段標記…1的位置是在DriverManager類加載是執行的靜態初始化塊,這里會調用loadInitialDrivers方法。
再看loadInitialDrivers方法里面標記…2的位置,這里調用的 ServiceLoader.load(Driver.class); 就會加載所有在META-INF/services/java.sql.Driver文件里邊的類到JVM內存,完成驅動的自動加載。
這就是SPI的優勢所在,能夠自動的加載類到JVM內存。這個技術在阿里的dubbo框架里面也占到了很大的分量。
JDBC如何區分多個驅動?
一個項目里邊很可能會即連接MySQL,又連接Oracle,這樣在一個工程里邊就存在了多個驅動類,那么這些驅動類又是怎么區分的呢?
關鍵點就在于getConnection的步驟,DriverManager.getConnection中會遍歷所有已經加載的驅動實例去創建連接,當一個驅動創建連接成功時就會返回這個連接,同時不再調用其他的驅動實例。DriverManager關鍵代碼如下:
private static Connection getConnection(
//.....
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) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
//......
是不是每個驅動實例都真真實實的要嘗試建立連接呢?不是的!
每個驅動實例在getConnetion的第一步就是按照url判斷是不是符合自己的處理規則,是的話才會和db建立連接。比如,MySQL驅動類中的關鍵代碼:
public boolean acceptsURL(String url) throws SQLException {
return (parseURL(url, null) != null);
}
public Properties parseURL(String url, Properties defaults)
throws java.sql.SQLException {
Properties urlProps = (defaults != null) ? new Properties(defaults)
: new Properties();
if (url == null) {
return null;
}
if (!StringUtils.startsWithIgnoreCase(url, URL_PREFIX)
&& !StringUtils.startsWithIgnoreCase(url, MXJ_URL_PREFIX)
&& !StringUtils.startsWithIgnoreCase(url,
LOADBALANCE_URL_PREFIX)
&& !StringUtils.startsWithIgnoreCase(url,
REPLICATION_URL_PREFIX)) { //$NON-NLS-1$
return null;
}
//......