MyBatis3源码深度解析(三)Connnection

前言

本节将详细研究Connection接口的相关内容,例如JDBC驱动程序的类型、DriverManager类、Driver接口以及DataSource接口等。

2.3 Connnection

一个Connection对象表示通过JDBC驱动程序与数据源建立的连接。

这里的数据源可以是关系型数据库管理系统(DBMS)、文件系统或者其他通过JDBC驱动访问的数据源。

使用JDBC API的应用程序可能需要维护多个Connection对象,一个Connection对象可能访问多个数据源,也可能访问单个数据源。

获取JDBC中的Connection对象有两种方式:

(1)通过JDBC API中提供的DriverManager类获取;
(2)通过DataSource接口的实现类获取(推荐使用)。

目前使用比较广泛的数据库连接池,如C3P0、DBCP、Druid等都是javax.sql.DataSource接口的具体实现。

2.3.1 JDBC驱动程序的类型

2.3.1.1 JDBC-ODBC Bridge Driver

SUN公司发布JDBC规范时,市面上并没有成熟的JDBC驱动程序可用,但微软的ODBC(Open-DataBase-Connectivity,开放式数据库互连)技术已经非常成熟,通过ODBC驱动程序可以连接所有类型的数据源。

因此,SUN公司发布了JDBC-ODBC桥接驱动,在JDBC和ODBC之间建立一个桥连接,利用现成的ODBC框架将JDBC调用转换为ODBC调用

JDBC-ODBC Bridge Driver
但是,由于桥连接的限制,并非所有功能都能直接转换并正常调用,同时多层调用转换对性能也有一定的影响,在JDBC规范中一般不推荐采用该方式

2.3.1.2 Native API Driver

这类驱动程序会直接调用数据库提供的原生链接库或客户端,因为没有中间过程,访问速度通常表现良好。

Native API Driver
但由于驱动程序和链接库或客户端绑定,无法达到JDBC跨平台的基本目的,因此在JDBC规范中也不推荐该类驱动程序

2.3.1.3 HDBC-Net Driver

这类驱动程序会将JDBC调用转换为独立于数据库的协议,然后通过特定的中间件或服务器转换为数据库通信协议,主要目的是获取更好的架构灵活性。

HDBC-Net Driver
然而,通过中间服务器转换会对性能造成一定影响。这类驱动程序并不常见,微软的ADO.NET属于这种架构。

2.3.1.4 Native Protocol Driver

这是最常见的JDBC驱动程序,开发中使用的驱动包基本都属于此类,通常由数据库厂商直接提供,例如mysql-connector-java。

这类驱动程序会把JDBC调用转换为数据库特定的网络通信协议,而使用网络通信,驱动程序可以纯Java实现,支持跨平台部署,性能也较好。

Native Protocol Driver

2.3.2 java.sql.Driver

2.3.2.1 静态代码块加载驱动类

所有的JDBC驱动都必须实现Driver接口,而且实现类必须包含一个静态代码块,在该静态代码块中向DriverManager注册自己的一个实例。如MyBatis内置的HSQLDB数据库的驱动程序实现类如下:

源码1org.hsqldb.jdbc.JDBCDriver

public JDBCDriver() {
}

public static final JDBCDriver driverInstance = new JDBCDriver();
static {
    try {
        DriverManager.registerDriver(driverInstance);
    } catch (Exception e) {
    }
}

由 源码1 可知,当加载驱动类JDBCDriver时,上面的静态代码块就会执行,向DriverManager注册一个JDBCDriver驱动类的实例。

这也是为什么在使用JDBC操作数据库时需要先使用Class.forName()方法加载驱动类。而为了确保驱动类可以使用Class.forName()方法进行加载,它必须提供一个无参数的构造方法。

2.3.2.2 SPI机制加载驱动类

在JDBC 4.0版本之前,使用DriverManager获取Connection对象都需要通过代码显式地加载驱动类,如:

Class.forName("org.hsqldb.jdbcDriver");

JDBC 4.0及以上的版本,对DriverManager的getConnection方法做了增强,可以通过Java的SPI机制加载驱动。

符合JDBC 4.0及以上版本的驱动程序的jar包中必须存在一个 META-INF\services\java.sql.Driver 文件,并在该文件中指定Driver接口的实现类。

如hsqldb-2.4.0.jar中:

hsqldb-2.4.0.jar中的SPI机制
在DriverManager类中,定义了一段静态代码块:

源码2DriverManager.java

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                // 读取jdbc.drivers属性指定的驱动类
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }

    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            // 通过SPI机制加载Driver接口的实现类
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            try {
                while (driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } // catch ...
            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);
            // 依次加载jdbc.drivers属性指定的驱动类
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

由 源码2 可知,静态代码块会调用loadInitialDrivers方法,该方法通过两种方式加载驱动实现类。

loadInitialDrivers方法中,首先会读取jdbc.drivers属性指定的驱动类(只是读取属性,还没加载),例如在使用命令启动项目时使用jdbc.drivers属性指定驱动类:

java -Djdbc.drivers=org.hsqldb.jdbcDriver -jar xxx.jar

随后,通过SPI机制,借助ServiceLoader读取 META-INF\services\java.sql.Driver 文件中配置的Driver接口的实现类,完成驱动实现类的加载。

最后,如果jdbc.drivers属性不为空,则使用Class.forName依次加载jdbc.drivers属性指定的驱动类。

通过一个简单示例来调试一下:

  • 自定义一个驱动实现类,并配置到VM参数中
public class MyDriver implements Driver {

    static {
        try {
            DriverManager.registerDriver(new MyDriver());
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public MyDriver() {
    }
    
    // 重写Driver接口的方法 ...
    
}

配置到VM参数

  • 编写测试类
public class Example01 {

@Test
public void testSPI() {
    try {
        DriverManager.getConnection("jdbc:hsqldb:mem:mybatis", "sa", "");
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

}

以Debug方式启动项目,可以发现DriverManager中注册了2个驱动类,恰好就是自定义的MyDriver以及 META-INF\services\java.sql.Driver 文件中定义的org.hsqldb.jdbc.JDBCDriver,说明上述的两种配置方式都生效了。

DriverManager中注册了2个驱动类

2.3.3 DriverManager

由类名可知,这是一个驱动管理器,为JDBC客户端管理一组可用的驱动实现。

源码3DriverManager.java

// 注册驱动实现类
public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {...}
public static synchronized void registerDriver(java.sql.Driver driver,
        DriverAction da) throws SQLException {...}
        
// 解除驱动注册
public static synchronized void deregisterDriver(Driver driver)
        throws SQLException {...}

// 获取数据库连接
public static Connection getConnection(String url)
        throws SQLException {...}
public static Connection getConnection(String url,
        String user, String password) throws SQLException {...}
public static Connection getConnection(String url,
        java.util.Properties info) throws SQLException {...}
2.3.3.1 registerDriver&deregisterDriver

由 源码3 可知,registerDriver方法用于将驱动实现类注册到DriverManager中,这个方法会在驱动实现类加载时通过静态代码块隐式调用。registerDriver方法有2个重载方法:

  • registerDriver(java.sql.Driver driver):直接注册驱动实现类;
  • registerDriver(java.sql.Driver driver, DriverAction da):注册一个驱动实现类的同时,并注册一个DriverAction的实现类,以监听DriverManager使用deregisterDriver方法解除该监听器的注册时回调。
2.3.3.2 getConnection

这个方法是提供给JDBC客户端调用的,可以接收一个JDBC URL作为参数,DriverManager会对所有注册的驱动实现类进行遍历,找到能够识别该URL的驱动实现,并调用connect方法与数据库建立连接,然后返回Connection对象。

由 源码3 可知,getConnection方法有3个重载方法:

  • getConnection(String url):当连接数据库不需要用户名和密码时使用;
  • getConnection(String url, String user, String password):当连接数据库只需要用户名、密码时使用;
  • getConnection(String url, java.util.Properties info):当连接数据库除了需要用户名、密码,还需要其他额外的参数时使用。

JDBC URL的格式为:jdbc:<subprotocol>:<subname>。其中subprotocol用于指定数据库连接机制由一个或者多个驱动程序提供支持;subname的内容取决于subprotocol。

常用的数据库驱动程序的驱动实现类的类名及JDBC URL如下:

(1)Oracle

驱动程序类名:oracle.jdbc.driver.OracleDriver
JDBC URL:jdbc:oracle:thin:@//<host>:<port>/<ServiceName>jdbc:oracle:thin:@//<host>:<port>/<SID>

(2)MySql

驱动程序类名:com.mysql.jdbc.Driver
JDBC URL:jdbc:mysql://<host>:<port>/<database>

(3)IBM DB2

驱动程序类名:com.ibm.db2.jcc.DB2Driver
JDBC URL:jdbc:db2://<host>:<port>/<database>

2.3.4 javax.sql.DataSource

2.3.4.1 DataSource接口介绍

DataSource是比较推荐的获取数据源连接的一种方式。JDBC驱动程序都会实现DataSource接口,通过DataSource接口实现类的实例,返回一个Connection对象的实例。

DataSource对象表示能够提供数据源连接的数据源对象。如果数据库相关信息发生了变化,则可以简单地修改DataSource对象的属性来反映这种变化,而不用修改应用程序的任何代码。

JDBC API中有3种类型的DataSource对象,分别是DataSource、XADataSource(支持分布式事务)、ConnectionPoolDataSource(提供连接池提高系统性能和伸缩性)。

而具体有哪些DataSource对象的属性,却决于具体的实现类,不同的实现类的属性略有差异。例如在MySQL驱动的实现类MysqlDataSource对象中具有以下属性(列举部分):

  • databaseName:数据库名称;
  • user:用户名;
  • password:密码;
  • hostName:主机名;
  • port:监听端口;
  • url:JDBC URL;
  • encoding:编码。

DataSource对象的实现类会为这些属性提供对应的getter和setter方法,并在创建对象时初始化这些属性。

2.3.4.2 使用JNDI提高可移植性

使用JNDI(Java Naming and Directory Interface,Java命名和目录接口)可以提高应用程序的可移植性。在应用程序中,使用JNDI API,就可以仅通过一个逻辑名称来获取DataSource对象。当数据源的属性发生变化时,不会影响JDBC客户端的代码。

JNDI为应用程序提供了一种通过网络访问远程服务的方式,可以把一个逻辑名称和数据源建立映射关系。

下面是使用JNDI的一个示例:

public class Example02 {

    @Before
    public void before() throws IOException {
        // 创建MyBatis提供的数据源工厂
        DataSourceFactory dataSourceFactory = new PooledDataSourceFactory();
        Properties dsProps = new Properties();
        InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("database.properties");
        dsProps.load(inputStream);
        dataSourceFactory.setProperties(dsProps);
        // 获取数据源对象
        DataSource dataSource = dataSourceFactory.getDataSource();
        try {
            // Apache Tomcat提供的JNDI实现
            Properties jndiProps = new Properties();
            jndiProps.put(Context.INITIAL_CONTEXT_FACTORY, "org.apache.naming.java.javaURLContextFactory");
            jndiProps.put(Context.URL_PKG_PREFIXES, "org.apache.naming");
            InitialContext ctx = new InitialContext(jndiProps);
            // 将逻辑名称"java:testJNDI"和数据源DataSource绑定起来
            ctx.bind("java:testJNDI", dataSource);
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void testJNDI() {
        try {
            // Apache Tomcat提供的JNDI实现
            Properties jndiProps = new Properties();
            jndiProps.put(Context.INITIAL_CONTEXT_FACTORY, "org.apache.naming.java.javaURLContextFactory");
            jndiProps.put(Context.URL_PKG_PREFIXES, "org.apache.naming");
            InitialContext ctx = new InitialContext(jndiProps);
            // 通过逻辑名称"java:testJNDI"获取数据源DataSource
            DataSource dataSource = (DataSource)ctx.lookup("java:testJNDI");
            Connection connection = dataSource.getConnection();
            Assert.assertNotNull(connection);
            System.out.println("成功获取到Connection对象...");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

在示例的代码中,创建了一个javax.naming.InitialContext实例,然后调用该实例的bind方法将字符串形式的逻辑名称 java:testJNDI 和数据源DataSource对象绑定在一起。

testJNDI方法中,调用InitialContext实例的lookup方法,传入逻辑名称 java:testJNDI,获取已经绑定的数据源DataSource对象,如果成功获取到,说明JNDI确实生效了。

需要注意的是,JDK只是提供了JNDI的规范,具体的实现由不同厂商来完成。当前示例使用的是Apache Tomcat中提供的JNDI实现,因此需要在项目中的pom.xml文件中添加相关依赖:

<dependency>
    <groupId>tomcat</groupId>
    <artifactId>naming-java</artifactId>
    <version>5.0.28</version>
</dependency>
<dependency>
    <groupId>tomcat</groupId>
    <artifactId>naming-common</artifactId>
    <version>5.0.28</version>
</dependency>
<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.1.1</version>
</dependency>

在实际的Java EE项目中,JNDI命名服务的创建通常由应用服务器(如Tomcat、WebLogic等)来创建,开发者只需要查找命名服务并使用即可。例如,在Tomcat服务器中,通过配置其 context.xml 配置文件就可以配置JNDI数据源:

<Resource name="java:testJNDI" 
          scope="Shareable"
          type="javax.sql.DataSource"
          factory="org.apache.ibatis.datasource.pooled.PooledDataSourceFactory"
          username="sa"
          password=""
          driverClassName="org.hsqldb.jdbcDriver"
          url="jdbc:hsqldb:mem:mybatis"/>

2.3.6 关闭Connection

源码4Connection.java

public interface Connection  extends Wrapper, AutoCloseable {
    void close() throws SQLException;
    boolean isClosed() throws SQLException;
    boolean isValid(int timeout) throws SQLException;
}

当使用完Connection对象后,需要显式地关闭该对象。由 源码4 可知,Connection接口提供了几个方法处理Connection对象地关闭。

  • close:关闭Connection对象。当调用该方法时,由该Connection对象创建的所有Statement对象都会被关闭;
  • isClosed:判断Connection对象是否已被关闭;
  • isValid:判断Connection对象是否有效。

本节完,更多内容请查阅分类专栏:MyBatis3源码深度解析

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灰色孤星A

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值