[TOC]
SPI 在jdbc driver的運用
這幾天在看java 類加載機制,看到 spi 服務(wù)機制破壞了雙親委派模型,特地研究了下典型的 spi 服務(wù) jdbc 驅(qū)動
首先運行一下代碼,查看 mysql jdbc 驅(qū)動的類加載(maven 項目已經(jīng)引進 jdbc 驅(qū)動依賴,版本為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());
}
輸出結(jié)果如下:
class com.mysql.jdbc.Driver------sun.misc.Launcher$AppClassLoader@2a139a55
class com.mysql.fabric.jdbc.FabricMySQLDriver------sun.misc.Launcher$AppClassLoader@2a139a55
null
可以看到代碼中并沒有調(diào)用 Class.forName(“”)的代碼,但DriverManager中已經(jīng)加載了兩個 jdbc 驅(qū)動,而卻這兩個驅(qū)動都是使用的應(yīng)用類加載器(AppClassLoader)加載的,而DriverManager本身的類加載器確是 null 即BootstrapClassLoader,按照雙親委派模型的規(guī)則,委派鏈如下:
SystemApp class loader -> Extension class loader -> Bootstrap class loader
,父加載器BootstrapClassLoader是無法找到AppClassLoader加載的類的,此時使用了線程上下文加載器,Thread.currentThread().setContextClassLoader()可以將委派鏈左邊的類加載器,設(shè)置為線程上下文加載器,此時右邊的加載器就可以使用線程上下文加載器委托子加載器加載類
可以查看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的實現(xiàn)類,此處就是 spi 服務(wù)的思想
查看 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();
}
創(chuàng)建了一個ServiceLoader,使用 reload 方法來加載,ServiceLoader 的主要參數(shù)與 reload 的代碼如下:
private static final String PREFIX = "META-INF/services/";
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
LazyIterator是一個懶加載的迭代器,看一下這個迭代器的實現(xiàn):
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會循環(huán)調(diào)用所有在META-INF/services/java.sql.Driver下定義了所有類的 Class.forName()方法
那么這些加載的驅(qū)動是如何被注冊在DriverManager中的?我們看 mysql 的驅(qū)動 Driver 的實現(xiàn)類 可以看到 Driver的實現(xiàn)在初始化時就進行了注冊,代碼如下:
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
這段代碼即可將 java.sql.Driver 的實現(xiàn)類注冊進DriverManager,注意此段代碼中 new Driver()是com.mysql.jdbc.Driver
最后查看下實現(xiàn) spi 服務(wù)必不可少的文件 META-INF/services/java.sql.Driver(這個特定用來實現(xiàn) java.sql.Driver 的接口的 spi 服務(wù))這個文件中內(nèi)容如下:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
可以看到這兩個類即為文章開頭實驗的那兩個 jdbc 驅(qū)動
注意并不是所有版本的 jdbc 驅(qū)動都實現(xiàn)了 spi 服務(wù),應(yīng)該是5.1.5及之后的版本才實現(xiàn)了這種服務(wù),之前的版本還是需要手動調(diào)用 Class.forName 方法來加載驅(qū)動,還有好像 ojdbc 的驅(qū)動均沒有實現(xiàn) spi 服務(wù)
搞清楚了 spi 服務(wù)于 DriverManager 加載的過程,我們可以自己嘗試實現(xiàn)一個簡單的 jdbc 驅(qū)動(僅僅實現(xiàn)了類加載的部分)
使用 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;
}
}
僅僅寫了一個初始化方法,其他方法均使用默認(rèn)空實現(xiàn),在 src/mian/resources 目錄下新建文件 /META-INF/services/java.sql.Driver 填入內(nèi)容com.lcy.mysql.Driver 打包發(fā)布
在之前的文章開始的測試工程中引入工程依賴(如果是同一工程,直接運行即可),運行可以看到結(jié)果如下:
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
可以看到,已經(jīng)加載了我們自定義的com.lcy.mysql.Driver(雖然這個加載器沒有實現(xiàn)任何功能,但測試 spi 機制的目的已經(jīng)實現(xiàn))
JDBC驅(qū)動加載機制
說道JDBC我們寫Java的程序員實在是太過熟悉了,如今的后端系統(tǒng)不論大小幾乎都抹不開和數(shù)據(jù)庫存在聯(lián)系镰惦。
JDBC是一個連接數(shù)據(jù)庫的Java API罚渐,包含了相關(guān)的接口和類迅耘。但是税娜,他不提供針對具體數(shù)據(jù)庫(MySQL月洛、MS、Oracle)的實際操作捌议,而只是提供了接口哼拔,以及調(diào)用框架。和具體數(shù)據(jù)庫的直接交互由對應(yīng)的驅(qū)動程序完成瓣颅,比如mysql的mysql-connector倦逐、oracle的ojdbc、MS的sqljdbc等宫补。
jdbc連接過程
1檬姥、加載JDBC驅(qū)動程序:
Class.forName("com.mysql.jdbc.Driver") ;
2、提供JDBC連接的URL
String url = jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8
3守谓、創(chuàng)建數(shù)據(jù)庫的連接
Connection con =
DriverManager.getConnection(url , username , password ) ;
4穿铆、創(chuàng)建一個Statement
PreparedStatement pstmt = con.prepareStatement(sql) ;
5、執(zhí)行SQL語句
ResultSet rs = stmt.executeQuery("SELECT * FROM ...") ;
6斋荞、處理結(jié)果
while(rs.next()){
//do something
}
7荞雏、關(guān)閉JDBC對象
Class.forName作用
我們都知道,也聽了無數(shù)遍平酿,驅(qū)動的加載是由Class.forName 方法完成的凤优。
但是,讓我們深究一下蜈彼,Class.forName是JSE里面加載一個類到JVM內(nèi)存的方法筑辨,為什么又會關(guān)聯(lián)了JDBC的驅(qū)動加載邏輯呢?
確實JDBC驅(qū)動的加載是在Class.forName這一步完成的幸逆,但是完成這個工作的是加載的具體的數(shù)據(jù)庫驅(qū)動類的靜態(tài)初始化塊完成的棍辕。
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對類的加載有一個邏輯是:在類被需要的時候,或者首次調(diào)用的時候就會把類加載到JVM还绘。反過來也就是:如果類沒有被需要的時候楚昭,是不會被加載到JVM的。
當(dāng)連接數(shù)據(jù)庫的時候我們調(diào)用了Class.forName語句之后拍顷,數(shù)據(jù)庫驅(qū)動類被加載到JVM抚太,那么靜態(tài)初始化塊就會被執(zhí)行,從而完成驅(qū)動的注冊工作昔案,也就是注冊到了JDBC的DriverManager類中尿贫。
由于是靜態(tài)初始化塊中完成的加載,所以也就不必?fù)?dān)心驅(qū)動被加載多次
拋棄Class.forName
在JDBC 4.0之后實際上我們不需要再調(diào)用Class.forName來加載驅(qū)動程序了踏揣,我們只需要把驅(qū)動的jar包放到工程的類加載路徑里庆亡,那么驅(qū)動就會被自動加載。
這個自動加載采用的技術(shù)叫做SPI捞稿,數(shù)據(jù)庫驅(qū)動廠商也都做了更新身冀《凼可以看一下jar包里面的META-INF/services目錄,里面有一個java.sql.Driver的文件搂根,文件里面包含了驅(qū)動的全路徑名。
比如mysql-connector里面的內(nèi)容:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
那么SPI技術(shù)又是在什么階段加載的數(shù)據(jù)庫驅(qū)動呢铃辖?看一下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();
//.....
}
上述代碼片段標(biāo)記…1的位置是在DriverManager類加載是執(zhí)行的靜態(tài)初始化塊,這里會調(diào)用loadInitialDrivers方法娇斩。
再看loadInitialDrivers方法里面標(biāo)記…2的位置仁卷,這里調(diào)用的 ServiceLoader.load(Driver.class); 就會加載所有在META-INF/services/java.sql.Driver文件里邊的類到JVM內(nèi)存,完成驅(qū)動的自動加載犬第。
這就是SPI的優(yōu)勢所在锦积,能夠自動的加載類到JVM內(nèi)存。這個技術(shù)在阿里的dubbo框架里面也占到了很大的分量歉嗓。
JDBC如何區(qū)分多個驅(qū)動丰介?
一個項目里邊很可能會即連接MySQL,又連接Oracle鉴分,這樣在一個工程里邊就存在了多個驅(qū)動類哮幢,那么這些驅(qū)動類又是怎么區(qū)分的呢?
關(guān)鍵點就在于getConnection的步驟志珍,DriverManager.getConnection中會遍歷所有已經(jīng)加載的驅(qū)動實例去創(chuàng)建連接橙垢,當(dāng)一個驅(qū)動創(chuàng)建連接成功時就會返回這個連接,同時不再調(diào)用其他的驅(qū)動實例伦糯。DriverManager關(guān)鍵代碼如下:
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());
}
}
//......
是不是每個驅(qū)動實例都真真實實的要嘗試建立連接呢柜某?不是的!
每個驅(qū)動實例在getConnetion的第一步就是按照url判斷是不是符合自己的處理規(guī)則敛纲,是的話才會和db建立連接喂击。比如,MySQL驅(qū)動類中的關(guān)鍵代碼:
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;
}
//......