- MyBatis 3源码深度解析
- 江荣波
- 4752字
- 2021-03-30 18:20:37
2.3 Connection详解
一个Connection对象表示通过JDBC驱动与数据源建立的连接,这里的数据源可以是关系型数据库管理系统(DBMS)、文件系统或者其他通过JDBC驱动访问的数据。使用JDBC API的应用程序可能需要维护多个Connection对象,一个Connection对象可能访问多个数据源,也可能访问单个数据源。
从JDBC驱动的角度来看,Connection对象表示客户端会话,因此它需要一些相关的状态信息,例如用户Id、一组SQL语句和会话中使用的结果集以及事务隔离级别等信息。
我们可以通过两种方式获取JDBC中的Connection对象:
(1)通过JDBC API中提供的DriverManager类获取。
(2)通过DataSource接口的实现类获取。
使用DataSource的具体实现获取Connection对象是比较推荐的一种方式,因为它增强了应用程序的可移植性,使代码维护更加容易,并且使应用程序能够透明地使用连接池和处理分布式事务。几乎在所有的Java EE项目中都是使用DataSource的具体实现来维护应用程序和数据库连接的。目前使用比较广泛的数据库连接池C3P0、DBCP、Druid等都是javax.sql.DataSource接口的具体实现。
本节会详细介绍Connection接口相关的内容,例如JDBC驱动的类型、DriverManager类、Driver接口以及DataSource接口等。
2.3.1 JDBC驱动类型
JDBC驱动程序有很多可能的实现,这些驱动实现类型主要包括以下几种:
1.JDBC-ODBC Bridge Driver
SUN发布JDBC规范时,市场上可用的JDBC驱动程序并不多,但是已经逐渐成熟的ODBC方案使得通过ODBC驱动程序几乎可以连接所有类型的数据源。所以SUN发布了JDBC-ODBC的桥接驱动,利用现成的ODBC架构将JDBC调用转换为ODBC调用,避免了JDBC无驱动可用的窘境,如图2-6所示。但是,由于桥接的限制,并非所有功能都能直接转换并正常调用,而多层调用转换对性能也有一定的影响,除非没有其他解决方案,否则不采用桥接架构。
2.Native API Driver
这类驱动程序会直接调用数据库提供的原生链接库或客户端,因为没有中间过程,访问速度通常表现良好,如图2-7所示。但是驱动程序与数据库和平台绑定无法达到JDBC跨平台的基本目的。在JDBC规范中也是不被推荐的选择。
图2-6 JDBC-ODBC桥接驱动
图2-7 Native API类型驱动
3.JDBC-Net Driver
这类驱动程序会将JDBC调用转换为独立于数据库的协议,然后通过特定的中间组件或服务器转换为数据库通信协议,主要目的是获得更好的架构灵活性,如图2-8所示。例如,更换数据库时可以通过更换中间组件实现。数据库厂商开发的驱动通常还提供额外的功能,例如高级安全特性等,而且通过中间服务器转换会对性能有一定影响。JDBC领域这种类型驱动并不常见,而微软的ADO.NET是这种架构的典型。
4.Native Protocol Driver
这是最常见的驱动程序类型,开发中使用的驱动包基本都属于此类,通常由数据库厂商直接提供,例如mysql-connector-java,驱动程序把JDBC调用转换为数据库特定的网络通信协议,如图2-9所示。使用网络通信,驱动程序可以纯Java实现,支持跨平台部署,性能也较好。
图2-8 JDBC-Net驱动类型
图2-9 Native Protocol驱动类型
2.3.2 java.sql.Driver接口
所有的JDBC驱动都必须实现Driver接口,而且实现类必须包含一个静态初始化代码块。我们知道,类的静态初始化代码块会在类初始化时调用,驱动实现类需要在静态初始化代码块中向DriverManager注册自己的一个实例,例如:
当我们加载驱动实现类时,上面的静态初始化代码块就会被调用,向DriverManager中注册一个驱动类的实例。这就是为什么我们使用JDBC操作数据库时一般会先加载驱动,例如:
为了确保驱动程序可以使用这种机制加载,Driver实现类需要提供一个无参数的构造方法。
DriverManager类与注册的驱动程序进行交互时会调用Driver接口中提供的方法。Driver接口中提供了一个acceptsURL()方法,DriverManager类可以通过Driver实现类的acceptsURL()来判断一个给定的URL是否能与数据库成功建立连接。当我们试图使用DriverManager与数据库建立连接时,会调用Driver接口中提供的connect()方法,具体如下:
该方法有两个参数:第一个参数为驱动能够识别的URL;第二个参数为与数据库建立连接需要的额外参数,例如用户名、密码等。
当Driver实现类能够与数据库建立连接时,就会返回一个Connection对象,当Driver实现类无法识别URL时则会返回null。
注意
在DriverManager类初始化时,会试图加载所有jdbc.drivers属性指定的驱动类,因此我们可以通过jdbc.drivers属性来加载驱动,例如:
JDBC 4.0以上的版本对DriverManager类的getConnection()方法做了增强,可以通过Java的SPI机制加载驱动。符合JDBC 4.0以上版本的驱动程序的JAR包中必须存在一个META-INF/services/java.sql.Driver文件,在java.sql.Driver文件中必须指定Driver接口的实现类。
2.3.3 Java SPI机制简介
在JDBC 4.0版本之前,使用DriverManager获取Connection对象之前都需要通过代码显式地加载驱动实现类,例如:
JDBC 4.0之后的版本对此做了改进,我们不再需要显式地加载驱动实现类。这得益于Java中的SPI机制,本节我们就来简单地了解SPI机制。
SPI(Service Provider Interface)是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制。比如有一个接口,想在运行时动态地给它添加实现,只需要添加一个实现,SPI机制在程序运行时就会发现该实现类,整体流程如图2-10所示。
图2-10 Java SPI机制
当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services目录中创建一个以服务接口命名的文件,这个文件中的内容就是这个接口具体的实现类。当其他的程序需要这个服务的时候,就可以查找这个JAR包中META-INF/services目录的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名加载服务实现类,然后就可以使用该服务了。
JDK中查找服务实现的工具类是java.util.ServiceLoader。接下来我们看一下ServiceLoader类的使用,代码如下:
ServiceLoader类提供了一个静态的load()方法,用于加载指定接口的所有实现类。调用该方法后,classpath下META-INF/services目录的java.sql.Driver文件中指定的所有实现类都会被加载。
最后我们再来了解一下DriverManager加载驱动实现类的过程。符合JDBC 4.0以上版本的JDBC驱动都会在META-INF/services目录的java.sql.Driver文件中指定驱动实现类的完全限定名。
DriverManager类中定义了静态初始化代码块,代码如下:
如上面的代码所示,DriverManager类的静态代码块会在我们调用DriverManager的getConnection()方法之前调用。静态代码块中调用loadInitialDrivers()方法加载驱动实现类,该方法的关键代码如下:
如上面的代码所示,在loadInitialDrivers()方法中,通过JDK内置的ServiceLoader机制加载java.sql.Driver接口的实现类,然后对所有实现类进行遍历,这样就完成了驱动类的加载。驱动实现类会在自己的静态代码块中将驱动实现类的实例注册到DriverManager中,这样就取代了通过调用Class.forName()方法加载驱动的过程。
2.3.4 java.sql.DriverAction接口
前面我们了解到,Driver实现类在被加载时会调用DriverManager类的registerDriver()方法注册驱动。我们也可以在应用程序中显式地调用DriverManager类的deregisterDriver()方法来解除注册。JDBC驱动可以通过实现DriverAction接口来监听DriverManager类的deregisterDriver()方法的调用。
JDBC规范中不建议DriverAction接口的实现类在应用程序中被使用,因此DriverAction实现类通常会作为私有的内部类,从而避免被其他程序使用。
JDBC驱动的静态初始化代码块可以调用DriverManager.registerDriver(java.sql.Driver,java.sql.DriverAction)方法来确保DriverManager类的deregisterDriver()方法调用被监听,例如:
DriverAction用于监听驱动类被解除注册事件,是驱动提供者需要关注的范畴,作为JDBC的使用者,我们只需要了解即可。
2.3.5 java.sql.DriverManager类
DriverManager类通过Driver接口为JDBC客户端管理一组可用的驱动实现,当客户端通过DriverManager类和数据库建立连接时,DriverManager类会根据getConnection()方法参数中的URL找到对应的驱动实现类,然后使用具体的驱动实现连接到对应的数据库。
DriverManager类提供了两个关键的静态方法:
- registerDriver():该方法用于将驱动的实现类注册到DriverManager类中,这个方法会在驱动加载时隐式地调用,而且通常在每个驱动实现类的静态初始化代码块中调用。
- getConnection():这个方法是提供给JDBC客户端调用的,可以接收一个JDBC URL作为参数,DriverManager类会对所有注册驱动进行遍历,调用Driver实现的connect()方法找到能够识别JDBC URL的驱动实现后,会与数据库建立连接,然后返回Connection对象。
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>。
例如:jdbc:oracle:thin:@localhost:1521:orcl。
(2)MySQL
驱动程序类名:com.mysql.jdbc.Driver。
JDBC URL:jdbc: mysql://<host>:<port>/<database_name>。
例如:jdbc:mysql://localhost/sample。
(3)IBM DB2
驱动程序类名:com.ibm.db2.jcc.DB2Driver。
JDBC URL:jdbc:db2://<host>[:<port>]/<database_name>。
例如:jdbc:db2://localhost:5000/sample。
注意
JDBC URL不需要完全遵循RFC 3986, Uniform Resource Identifier (URI): Generic Syntax文档中定义的URI语法规范。
下面是使用DriverManager获取JDBC Connection对象的案例代码:
DriverManager类还提供了两个重载的getConnection方法:
- getConnection(String url):当数据库不需要用户名和密码时,我们可以调用该方法与数据库建立连接。
- getConnection(String url, java.util.Properties prop):如果建立数据库连接除了需要用户名、密码外,还需要一些额外的信息,我们可以使用Properties类来描述建立连接需要的所有配置信息。
2.3.6 javax.sql.DataSource接口
javax.sql.DataSource接口最早是由JDBC 2.0版本扩展包提供的,它是比较推荐的获取数据源连接的一种方式,JDBC驱动程序都会实现DataSource接口,通过DataSource实现类的实例,返回一个Connection接口的实现类的实例。
使用DataSource对象可以提高应用程序的可移植性。在应用程序中,可以通过逻辑名称来获取DataSource对象,而不用为特定的驱动指定特定的信息。我们可以使用JNDI(Java Naming and Directory Interface)把一个逻辑名称和数据源对象建立映射关系。
DataSource对象用于表示能够提供数据库连接的数据源对象。如果数据库相关的信息发生了变化,则可以简单地修改DataSource对象的属性来反映这种变化,而不用修改应用程序的任何代码。
DataSource接口可以被实现,提供如下两种功能:
- 通过连接池提高系统性能和伸缩性。
- 通过XADataSource接口支持分布式事务。
注意
DataSource接口的实现必须包含一个无参构造方法。
JDBC API中定义了一组属性来表示和描述数据源实现。具体有哪些属性,取决于DataSource对象的类型,包括DataSource、ConnectionPoolDataSource和XADataSource。表2-1是DataSource所有标准属性及其描述。
表2-1 DataSource标准属性
DataSource属性遵循JavaBeans 1.01规范中对JavaBean组件属性的约定,可以在这些属性的基础上增加一些特定的属性扩展(这些扩展的属性不能与标准属性冲突)。DataSource实现类必须为支持的每个属性提供对应的Getter和Setter方法,而且这些属性需要在创建DataSource对象时初始化。
DataSource对象的属性不建议被JDBC客户端直接访问,可以通过增强DataSource实现类的属性访问方法来实现,而不是在应用程序中使用DataSource接口时控制。此外,客户端所操作的对象可以是实现了DataSource接口的包装类,它的属性对应的Setter和Getter方法不需要暴露给客户端。一些管理工具如果需要访问DataSource实现类的属性,则可以使用Java的内省机制。
2.3.7 使用JNDI API增强应用的可移植性
JNDI(Java Naming and Directory Interface,Java命名和目录接口)为应用程序提供了一种通过网络访问远程服务的方式。本节我们学习如何通过JNDI API注册和访问JDBC数据源对象。读者如果需要了解更多JNDI相关细节,则可参考JNDI规范文档。
JNDI API的命名服务可以把一个逻辑名称和一个具体的对象绑定。使用JNDI API,应用程序可以通过与DataSource对象绑定的逻辑名称来获取DataSource对象,这种方式在很大程度上提高了应用的可移植性,因为当DataSource对象的属性(例如端口号、服务器地址等)被修改时,不会影响JDBC客户端代码。实际上,当修改DataSource的配置,使它连接到其他数据库时,应用程序是没有任何感知的。
接下来我们就以一个实际的案例介绍如何使用JNDI API提供一个命名服务,然后使用JNDI API查找该命名服务,代码如下:
如上面的代码所示,在MyBatis源码中提供了javax.sql.DataSource接口的实现,分别为UnpooledDataSource和PooledDataSource类。UnpooledDataSource未实现连接池功能,而PooledDataSource则采用装饰器模式对UnpooledDataSource功能进行了增强,增加了连接池管理功能。
上面的代码中,我们使用UnpooledDataSourceFactory创建了一个UnpooledDataSource实例,其中database.properties文件为数据源相关配置(读者可参考mybatis-chapter02项目中的database.properties文件内容),然后创建一个javax.naming.InitialContext实例,调用该实例的bind()方法创建命名服务,命名服务创建完成后就可以通过javax.naming.InitialContext实例的lookup()方法来查找服务了。
需要注意的是,JDK中只提供了JNDI规范,具体的实现由不同的厂商来完成。这里我们使用的是Apache Tomcat中提供的JNDI实现,因此需要在项目中添加相关依赖,例如:
在实际的Java EE项目中,JNDI命名服务的创建通常由应用服务器来完成。在应用程序中,我们只需要查找命名服务并使用即可。例如,在Apache Tomcat服务器中,我们可以通过如下代码配置JNDI数据源:
注意
JNDI规范文档:https://docs.oracle.com/cd/E17802_01/products/products/jndi/javadoc/。
2.3.8 关闭Connection对象
当我们使用完Connection对象后,需要显式地关闭该对象。Connection接口中提供了一个close()方法,用于关闭Connection对象;还提供了一个isClosed()方法,判断连接是否关闭;同时可以通过isValid()方法判断连接是否有效。
下面详细介绍这几个方法。
- java.sql.Connection#close():当应用程序使用完Connection对象后,应该显式地调用java.sql.Connection对象的close()方法。调用该方法后,由该Connection对象创建的所有Statement对象都会被关闭。一旦Connection对象关闭后,调用Connection的常用方法(例如createStatement()方法)将会抛出SQLException异常。
- java.sql.Connection#isClosed():Connection接口中提供的isClosed()方法用于判断应用中是否调用了close()方法关闭该Connection对象,这个方法不能用于判断数据库连接是否有效。
注意
有些JDBC驱动实现厂商对isClosed()方法做了增强,可以用它判断数据库连接是否有效。
但是为了程序的可移植性,需要判断连接是否有效时还是建议使用isValid()方法。
- java.sql.Connection#isValid():Connection接口提供的isValid()方法用于判断连接是否有效,如果连接依然有效,则返回true,否则返回false。当该方法返回false时,调用除了close()、isClosed()、isValid()以外的其他方法将会抛出SQLException异常。