SPI是一種JDK提供的加載插件的靈活機制,分離了接口與實現,就拿常用的數據庫驅動來說,我們只需要在spring系統中引入對應的數據庫依賴包(比如mysql-connector-java以及針對oracle的ojdbc6驅動),然後在yml或者properties配置文件中對應的數據源配置就可自動使用對應的sql驅動,比如mysql的配置:
spring:
datasource:
url: jdbc:mysql://localhost:3306/xxxxx?autoReconnect=true&useSSL=false&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: dev
password: xxxxxx
platform: mysql
spi機制正如jdk的classloader一樣,你不引用它,它是不會自動加載到jvm的,不是引入了下面的的兩個sql驅動依賴就必然會加載oracle以及mysql的驅動:
<!--oracle驅動-->
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>12.1.0.1-atlassian-hosted</version>
</dependency>
<!--mysql驅動-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
正是由於jdk的這種spi機制,我們在spring項目中使用對應的驅動才這麼簡單,我們只需做兩件事:
1、在pom文件中引入對應的驅動依賴
2、在配置文件中配置對應的數據源即可
那麼在spring項目中到底是誰觸發了數據庫驅動的spi加載機制呢?爲了說明這個問題,咱們先說說jdk的spi的工作機制,jdk的spi通過ServiceLoader這個類來完成對應接口實現類的加載工作,就拿咱們要說的數據庫驅動來說,ServiceLoader會在spring項目的classpath中尋找那些滿足下麪條件的類:
1、這些jar包的META-INF/services有一個java.sql.Driver的文件
對應java.sql.Driver文件中爲該數據庫驅動對應的數據庫驅動的實現類,比如mysql驅動對應的就是com.mysql.cj.jdbc.Driver,如下圖所示:
JDK這部分有關SPI具體的實現機制可以閱讀下ServiceLoader的內部類LazyIterator,該類的hasNextService、nextService兩個方法就是具體SPI機制工作底層機制。
好了,上面簡要概述了下JDK的SPI工作機制,下面繼續看spring框架如何使用spi機制來完成數據庫驅動的自動管理的(加載、註銷),接下來就按照事情發展的先後的先後順序把mysql驅動加載的全過程屢一下,筆者使用的是springboot 2.x,數據源使用的數據源爲Hikari,這是後來居上的一款數據源,憑藉其優秀的性能以及監控機制成爲了springboot 2.x之後首推的數據源,
用過springboot的小夥伴對springboot的自動裝載機制,數據源的配置也是使用的自動裝配機制,具體類DataSourceAutoConfiguration,
注意上面標紅部分,這裏面引入的Hikari、Tomcat等(除了DataSourceJmxConfiguration之外)都是一些數據源配置,我們先看下springboot推薦的Hikari數據源配置:
/**
** 這是一個Configuration類,該類定義了創建HikariDataSource的Bean方法
***/
@Configuration
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
matchIfMissing = true)
static class Hikari {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public HikariDataSource dataSource(DataSourceProperties properties) {
// 使用配置文件中的數據源配置來創建Hikari數據源
HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
}
由於在DataSourceAutoConfiguration類中首先引入的就是Hikari的配置,DataSource沒有創建,滿足ConditionalOnMissingBean以及其他一些條件,就會使用該配置類創建數據源,好了接下來看下createDataSource到底是怎麼創建數據源的,這個過程又是怎麼跟SPI關聯起來的,
abstract class DataSourceConfiguration {
@SuppressWarnings("unchecked")
protected static <T> T createDataSource(DataSourceProperties properties, Class<? extends DataSource> type) {
//使用DataSourceProperties數據源配置創建DataSourceBuilder對象(設計模式中的建造者模式)
return (T) properties.initializeDataSourceBuilder().type(type).build();
}
//下面看下DataSourceBuilder的build方法
public T build() {
//在該例子中,type返回的是com.zaxxer.hikari.HikariDataSource類
Class<? extends DataSource> type = getType();
//實例化HikariDataSource類
DataSource result = BeanUtils.instantiateClass(type);
maybeGetDriverClassName();
//bind方法中會調用屬性的設置,反射機制,在設置driverClassName屬性時
bind(result);
return (T) result;
}
// HikariConfig的方法,HikariDataSource繼承自HikariConfig類
public void setDriverClassName(String driverClassName)
{
checkIfSealed();
Class<?> driverClass = null;
ClassLoader threadContextClassLoader = Thread.currentThread().getContextClassLoader();
try {
if (threadContextClassLoader != null) {
try {
//加載driverClassName對應的類,即com.mysql.cj.jdbc.Driver類,該類爲mysql對應的驅動類
driverClass = threadContextClassLoader.loadClass(driverClassName);
LOGGER.debug("Driver class {} found in Thread context class loader {}", driverClassName, threadContextClassLoader);
}
catch (ClassNotFoundException e) {
LOGGER.debug("Driver class {} not found in Thread context class loader {}, trying classloader {}",
driverClassName, threadContextClassLoader, this.getClass().getClassLoader());
}
}
if (driverClass == null) {
driverClass = this.getClass().getClassLoader().loadClass(driverClassName);
LOGGER.debug("Driver class {} found in the HikariConfig class classloader {}", driverClassName, this.getClass().getClassLoader());
}
} catch (ClassNotFoundException e) {
LOGGER.error("Failed to load driver class {} from HikariConfig class classloader {}", driverClassName, this.getClass().getClassLoader());
}
if (driverClass == null) {
throw new RuntimeException("Failed to load driver class " + driverClassName + " in either of HikariConfig class loader or Thread context classloader");
}
try {
// 創建com.mysql.cj.jdbc.Driver對象,接下來看下com.mysql.cj.jdbc.Driver創建對象過程中發生了什麼
driverClass.newInstance();
this.driverClassName = driverClassName;
}
catch (Exception e) {
throw new RuntimeException("Failed to instantiate class " + driverClassName, e);
}
}
// com.mysql.cj.jdbc.Driver類
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
//調用DriverManager註冊自身,DriverManager使用CopyOnWriteArrayList來存儲已加載的數據庫驅動,然後當創建連接時最終會調用DriverManager的getConnection方法,這纔是真正面向數據庫的,只不過spring的jdbc幫助我們屏蔽了這些細節
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
上面已經來到了DriverManager類,那麼DriverManager類裏面是否有什麼祕密呢,繼續往下走,看下DriverManager的重要方法:
static {
//靜態方法,jvm第一次加載該類時會調用該代碼塊
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
//DriverManager類的loadInitialDrivers方法
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來加載SPI機制提供的驅動,本文用到了兩個,一個是mysql的,一個是oracle的,注意該方法只會在jvm第一次加載DriverManager類時纔會調用,所以會一次性加載所有的數據庫驅動
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> 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.
*/
//下面的代碼就是真正完成數據庫驅動加載的地方,對應ServiceLoader類的LazyIterator類,所以看下該類的hasNext一級next方法即可,上面已經講過,這裏就不再贅述
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);
}
}
}
好了,上面已經把springboot如何使用jdk的spi機制來加載數據庫驅動的,至於DriverManager的getConnection方法調用過程可以使用類似的方式分析下,在DriverManager的getConnection方法打個斷點,當代碼停在斷點處時,通過Idea或者eclipse的堆棧信息就可以看出個大概了
希望本文能幫助一些人瞭解mysql驅動加載的整個過程,加深對SPI機制的理解