mysql spi_jdbc的 spi 服务机制

本文深入探讨了Java SPI(Service Provider Interface)服务机制在MySQL JDBC驱动加载过程中的作用。通过分析`DriverManager`的源码,揭示了如何通过SPI自动加载`java.sql.Driver`的实现类,无需显式调用`Class.forName()`。文章还介绍了`ServiceLoader`的工作原理,并展示了如何自定义JDBC驱动并利用SPI进行加载。在JDBC 4.0以后,自动加载驱动成为可能,无需手动加载,增强了代码的简洁性和可维护性。
摘要由CSDN通过智能技术生成

[TOC]

SPI 在jdbc driver的运用

这几天在看java 类加载机制,看到 spi 服务机制破坏了双亲委派模型,特地研究了下典型的 spi 服务 jdbc 驱动

首先运行一下代码,查看 mysql jdbc 驱动的类加载(maven 项目已经引进 jdbc 驱动依赖,版本为5.1.41)

public static void main(String[] args)

{

Enumeration 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() {

public String run() {

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

}

});

} catch (Exception ex) {

drivers = null;

}

sun.misc.Providers()

AccessController.doPrivileged(new PrivilegedAction() {

public Void run() {

ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);

Iterator 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 ServiceLoader load(Class service) {

ClassLoader cl = Thread.currentThread().getContextClassLoader();

return ServiceLoader.load(service, cl);

}

public static ServiceLoader load(Class service,

ClassLoader loader)

{

return new ServiceLoader<>(service, loader);

}

private ServiceLoader(Class 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

{

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();

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 action = new PrivilegedAction() {

public Boolean run() { return hasNextService(); }

};

return AccessController.doPrivileged(action, acc);

}

}

public S next() {

if (acc == null) {

return nextService();

} else {

PrivilegedAction action = new PrivilegedAction() {

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() {

public String run() {

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

}

});

} catch (Exception ex) {

drivers = null;

}

AccessController.doPrivileged(new PrivilegedAction() {

public Void run() {

ServiceLoader 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;

}

//......

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值