第4章 利用JDBC和JTA访问数据库和管理全局事务
所有企业级信息化系统都需要持久化地保存海量数据,这些数据通常都会保存在关系数据库(RDBMS)中。Java为访问关系数据库提供了JDBC API,使用这些API可以非常方便地连接数据库、操作数据库中的数据。需要指出的是,传统JDBC操作每次进行数据库访问时都需要连接数据库,而连接数据库是系统开销很大的一项操作。
为了更好地管理数据库连接,实际项目中通常都会考虑使用数据源来管理数据库连接,在基于Spring的轻量级Java EE企业应用中,通常会选择让Spring容器来管理数据源。当然,如果应用服务器可以管理数据源,那也是一种不错的选择。主流应用服务器都可以很好地管理数据源,WebLogic、JBoss等应用服务器都支持容器管理数据源,本章将会介绍如何在这两个应用服务器中配置和管理数据源。
应用服务器除了处理数据源之外,另一个非常有吸引力的地方就是JTA全局事务支持,通过使用应用服务器的JTA全局事务支持,允许应用程序跨越多个事务性资源,并可以通过JTA全局事务来保证多个事务性资源的一致性。本章也会详细介绍WebLogic、JBoss两个服务器的JTA事务支持,并详细介绍事务隔离、事务传播相关方面的知识。对部署到应用服务器中的应用程序来说,应用程序可以无须理会事务管理代码,而是让应用服务器来管理事务,这种事务管理方式就是所谓的容器管理事务(CMT)。本章将会简单介绍CMT和BMT(Bean管理事务),其中CMT更加简单、易用,而且具有很好的可移植性。
4.1 JDBC和容器管理的数据源
JDBC(Java Database Connectivity)提供了连接、访问数据的基本API,开发者可通过JDBC进行简单的数据访问。但实际应用中往往是通过数据源来管理数据库连接,客户端代码则通过数据源来获取数据库连接,然后再访问数据库。JBoss、WebLogic等应用服务器都提供了数据源管理功能,由这些服务器管理的数据源就是容器管理的数据源。
4.1.1 JDBC概述
与其他数据库编程环境相比,JDBC为数据库开发提供了标准的API,所以使用JDBC开发的数据库应用可以跨平台运行,而且可以跨数据库(如果全部使用标准的SQL)。也就是说,如果使用JDBC开发一个数据库应用,则该应用既可以在Windows平台上运行,也可以在UNIX等其他平台上运行;既可以使用MySQL数据库,也可以使用Oracle等数据库,而程序无须进行任何修改。通过使用JDBC,就可以使用同一种API访问不同的数据库系统。换言之,有了JDBC API,就不必为访问Sybase数据库学习一组API,为访问Oracle数据库又学习一组API……开发人员使用标准的API编写应用程序,然后根据不同的数据库,加入不同的数据库驱动程序即可工作。
提示
最早的时候,Sun公司希望自己开发一组Java API,程序员可以通过这组标准的API来访问程序,从而实现相同的程序可以跨所有数据库系统。但后来Sun发现这个目标具有不可实现性:因为数据库系统太多了,而且各数据库系统的内部特性又各不相同。后来Sun就制定了一组标准API,它们只是接口,没有提供实现类——这些实现类由各数据厂商提供实现,这些实现类就是驱动程序。而程序员使用JDBC时只要面向标准的JDBC API编程即可,当进行数据库切换时只要更换不同的实现类即可,这是面向接口编程的典型应用。
为了使JDBC程序可以跨平台、跨数据库运行,则需要不同数据库厂商提供相应的驱动,应用程序的开发者面向通用的JDBC API编程,而底层的实现则由各数据库厂商提供,当应用程序需要在不同数据库之间切换时,改变底层数据库驱动即可。
由此可见,数据库驱动程序是JDBC程序和数据库之间的转换层,即数据库驱动程序。数据库驱动程序负责将JDBC调用映射成特定的数据库调用。
大部分数据库系统,例如Oracle和Sybase等,都有相应的JDBC驱动程序,当需要连接某个特定的数据库时,必须有相应的数据库驱动程序。但对于一些特殊的数据库,例如Access,可能需要使用JDBC-ODBC桥进行访问。
提示
ODBC的全称是Open Database Connectivity,即开放数据库连接。ODBC和JDBC很像,严格地说,应该说JDBC模仿了ODBC的设计。ODBC也允许应用程序可以通过一组通用的API访问不同的数据库管理系统,从而使得基于ODBC的应用程序可以在不同数据库之间切换。同样,ODBC也需要各数据库厂商提供相应的驱动,而ODBC则负责管理这些驱动。
相对于ODBC而言,JDBC更加简单。总结起来,JDBC比ODBC多了几个优势:
ODBC更复杂,ODBC中有几个命令需要配置很多复杂的选项;而JDBC则采用简单、直观的方式来管理数据库连接。
JDBC比ODBC安全性更高,更易部署。
总体来说,JDBC的驱动通常有如下4种类型:
第一种JDBC驱动:称为JDBC-ODBC桥,这种驱动是最早实现的JDBC驱动程序,主要目的是为了快速推广JDBC。这种驱动程序将JDBC API映射到ODBC API。JDBC-ODBC也需要驱动,这种驱动由Sun公司提供实现。
第二种JDBC驱动:直接将JDBC API映射成数据库特定的客户端API。这种驱动程序包含特定数据库的本地代码,用于访问特定数据库的客户端。
第三种JDBC驱动:支持三层结构的JDBC访问方式。主要用于Applet阶段,通过Applet访问数据库。
第四种JDBC驱动:是纯Java的,直接与数据库实例交互。这种驱动是智能的,它知道数据库使用的底层协议。这种驱动是目前最流行的JDBC驱动。
注意
Sun公司提供的JDBC-ODBC桥驱动程序不是多线程的。也就是说,不适合在要求并行访问数据库的情况下使用。其固有的性能和扩展能力非常有限,因此,企业级的应用中不应该使用该类型的连接。
通常建议选择第四种JDBC驱动,这种驱动避开了本地代码,减少了应用开发的复杂性,也减少了产生冲突和出错的可能。但如果对性能有严格的要求,可以考虑使用第二种JDBC驱动,但使用这种驱动势必增加编码和维护的困难。
4.1.2 使用JDBC执行数据库访问
使用JDBC执行数据库访问主要依赖于JDBC API提供的几个核心接口,这些核心接口都位于JDK类库的java.sql包下。如下核心接口是JDBC编程中最常用的。
DriverManager:用于管理JDBC驱动的服务类。程序中使用该类的主要功能是获取Connection对象。
Connection:代表数据库连接对象,每个Connection代表一个物理连接会话。要想访问数据库,必须先获得数据库连接。
Statement:用于执行SQL语句的工具接口。该对象既可以用于执行DDL、DCL语句,也可以用于执行DML语句,还可以用于执行SQL查询。当执行SQL查询时,返回查询到的结果集。
PreparedStatement:预编译的Statement对象。PreparedStatement是Statement的子接口,它允许数据库预编译SQL语句(这些SQL语句通常带有参数),以后每次只改变SQL命令的参数,避免数据库每次都需要编译SQL语句,因此性能更好。相对于Statement而言,使用PreparedStatement执行SQL语句时,无须重新传入SQL语句,因为它已经预编译了SQL语句。
ResultSet:结果集对象。该对象包含访问查询结果的方法,ResultSet可以通过列索引或列名获得列数据。
如果不使用容器管理数据源,传统JDBC编程的步骤如下:
1 加载数据库驱动。
2 使用DriverManager获取数据库连接(Connection对象)。
3 通过Connection对象创建Statement对象或PreparedStatement对象,它们可用于执行SQL语句。
4 使用Statement或PreparedStatement执行SQL语句。
5 获取SQL语句的执行结果:如果执行的SQL语句是查询语句,执行结果将返回一个ResultSet对象,该对象里保存了SQL语句查询的结果,程序可以通过操作该ResultSet对象取出查询结果;如果执行的SQL语句是DML语句,执行结果是一个int型整数,它记录了受SQL语句影响的记录条数。
6 回收数据库资源,包括关闭ResultSet、Statement和Connection等资源。
数据库连接的建立及关闭是极耗系统资源的操作,在多层结构的应用环境中,这种资源的耗费对系统性能影响尤为明显。通过前面介绍的方式(通过DriverManager获取连接)获得的数据库连接,一个数据库连接对象均对应一个物理数据库连接,每次操作都打开一个物理连接,使用完后立即关闭连接。这种频繁地打开、关闭连接将造成系统的性能低下。
为了改善这种频繁地打开、关闭数据库连接的情形,实际应用中往往采用数据库连接池作为解决方案。数据库连接池的思路是:当应用程序启动时,系统主动建立足够的数据库连接,并将这些连接组成一个连接池。每次应用程序请求数据库连接时,无须重新打开连接,而是从池中取出已有的连接使用,使用完后,不再关闭数据库连接,而是直接将该连接归还给连接池。通过使用连接池,将大大提高程序运行效率。
对于共享资源的情况,有一个通用的设计模式:资源池(Resource Pool),用于解决资源的频繁请求﹑释放所造成的性能下降。为了解决数据库连接的频繁请求、释放,JDBC 2.0规范引入了数据库连接池技术。数据库连接池是Connection对象的工厂。数据库连接池的常用参数如下:
连接池的初始连接数。
连接池的最大连接数。
连接池的最小连接数。
连接池每次增加的容量。
JDBC的数据库连接池使用javax.sql.DataSource来表示,DataSource只是一个接口,该接口通常由商用服务器(如WebLogic、WebSphere)等提供实现,也有一些开源组织提供实现(如DBCP和C3P0等)。
提示
DataSource实际上应该代表数据源,它包含连接池和连接池管理两个部分,但习惯上我们也经常把DataSource称为连接池。
数据源对象(DataSource)的管理有两种方式:应用程序管理数据源和容器管理数据源。一般来说,应用程序管理的数据源相对复杂一些,使用起来也稍微烦琐一点;容器管理的数据源具有很好的全局性,因此使用起来非常方便,但需要应用服务器支持才行,不过绝大部分应用服务器,即使连Tomcat这种Web服务器都支持容器管理的数据源。
如果使用容器管理的数据源,那么步骤稍微简单一点,因为容器管理的数据源中已经包含了大量的数据库连接,客户端代码只要通过该数据源即可获得对数据库的访问,因此可以省略12 两个步骤。
提示
如果读者对JDBC编程还不熟悉,则建议先阅读疯狂Java体系的《疯狂Java讲义》的第13章有关内容。
4.1.3 使用WebLogic服务器管理的数据源
在使用容器管理的数据源之前,先要在容器中完成数据源配置,一旦在容器中完成了数据源配置,客户端代码就可通过该数据源的JNDI名称来获得数据源对象,然后再通过数据源来获得数据库连接。获得数据库连接之后,接下来的编程步骤与普通JDBC编程完全相同。
下面先以WebLogic服务器为例来介绍如何使用容器管理的数据源。在WebLogic服务器中配置数据源的步骤如下。
1 启动WebLogic服务器之后,使用浏览器进入WebLogic服务器的控制台,单击WebLogic服务器控制台左边“域结构”面板中的“base_domain->服务->JDBC->数据源”节点,可以看到如图4.1所示的页面。
图4.1 WebLogic服务器中管理数据源的界面
2 图4.1中并未列出任何数据源,这是因为还未在该服务器上配置任何数据源。如果需要重新配置一个新数据源,则应该单击“新建”按钮,出现如图4.2所示的界面。
3 在图4.2中为新配置数据源填入合适的信息,笔者以配置连接到MySQL数据库的数据源为例,按图4.2所示的方式填写即可。关键要在最下面的数据库驱动列表中选择合适的数据库驱动。单击“下一步”按钮,将看到如图4.3所示的页面。
图4.2 输入数据源相关信息
图4.3 为数据源设置全局事务选项
4 图4.3用于设置该数据源的全局事务选项。由于MySQL数据库驱动本身不支持XA事务,因此如果开发者勾选了第一个复选框,则必须在下面3个单选钮中选择一种实现机制来模拟全局事务。如果使用Oracle等数据库,它们的数据库驱动本身就支持XA事务,也就无须进行任何模拟了。如果使用的是像Oracle这种本身支持XA规范的驱动,将看到如图4.4所示的页面。
图4.4 对支持XA规范的驱动自动启用2PC协议
在该页面中输入合适的全局事务选项之后,单击“下一步”按钮,将看到如图4.5所示的页面。
图4.5 为数据源设置连接数据库的基本信息
提示
关于全局事务、XA事务和两阶段提交等概念的介绍请参考本章下一节的介绍。
5 在如图4.5所示页面中输入数据库连接的基本信息,告诉WebLogic服务器连接到哪个数据库,以及连接数据库的用户名、密码等信息。输入完成后单击“下一步”按钮将进入下一个页面,在该页面中开发者可以测试数据库连接是否成功,不测试亦可,直接单击“下一步”按钮,将看到如同4.6所示的页面。
图4.6 选择将数据源部署到哪个服务器
6 在如图4.6所示页面中勾选需要部署的服务器,然后单击“完成”按钮,部署完成。部署完成后系统再次返回如图4.1所示的页面,在该页面中将看到刚刚配置的数据源:firstds。
7 当firstds数据源配置完成后,在如图4.1所示页面中单击firstds链接,将进入firstds的详细配置页面,如图4.7所示。
图4.7 数据源管理界面
通过如图4.7所示的页面对该数据源可进行更详细的配置,例如填入连接池内最大连接数、初始连接数等,配置完成即表明该数据源配置完成。
配置完成后我们进入WebLogic服务器的配置目录(不是安装目录!笔者将WebLogic服务器配置在F:\base_domain目录下),在该目录的config\jdbc子目录下可以看到新增了一个firstds-jdbc.xml文件,该文件内容如下(笔者增加了简单的注释):
<?xml version='1.0' encoding='UTF-8'?> <jdbc-data-source xmlns="http://xmlns.oracle.com/weblogic/jdbc-data-source" xmlns:sec="http://xmlns.oracle.com/weblogic/security" xmlns:wls="http://xmlns.oracle.com/weblogic/security/wls" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http:// xmlns.oracle.com/weblogic/jdbc-data-source http://xmlns.oracle.com/weblogic/jdbc-data- source/1.0/jdbc-data-source.xsd"> <name>firstds</name> <!-- 数据库连接的基本参数 --> <jdbc-driver-params> <url>jdbc:mysql://127.0.0.1:3306/auction</url> <driver-name>com.mysql.jdbc.Driver</driver-name> <properties> <property> <name>user</name> <value>root</value> </property> </properties> <password-encrypted>{AES}mIsmYQtwMji9HMhdrZ0N+sV5jXg+j0PSOJq MA7Vvm8s= </password-encrypted> </jdbc-driver-params> <!-- 数据库连接池的配置参数 --> <jdbc-connection-pool-params> <initial-capacity>2</initial-capacity> <max-capacity>15</max-capacity> <capacity-increment>1</capacity-increment> <test-table-name>item</test-table-name> <statement-cache-size>10</statement-cache-size> <statement-cache-type>LRU</statement-cache-type> </jdbc-connection-pool-params> <!-- 数据源相关参数:JNDI名和全局事务的模拟协议 --> <jdbc-data-source-params> <jndi-name>firstds</jndi-name> <global-transactions-protocol>EmulateTwoPhaseCommit </global-transactions-protocol> </jdbc-data-source-params> </jdbc-data-source>
通过上面配置文件不难发现,前面我们通过图形界面所做的配置实际上就是为了得到这个文件。如果读者熟悉这个配置文件,完全可以不按前面介绍的“下一步”、“下一步”的方式来配置该数据源,直接编辑该文件来配置数据源即可。
提示
位于config/jdbc目录下的每个xxx-jdbc.xml文件代表一个数据源,其中xxx就是该数据源的名称。在图形界面控制台每新增一个数据源,这里将会新增一个xxx-jdbc.xml配置文件。
除此之外,config目录下的config.xml文件中也增加了引用该数据源的配置信息,代码如下所示:
<jdbc-system-resource> <name>javaee</name> <!-- 指定数据源配置文件的位置 --> <descriptor-file-name>jdbc/javaee-jdbc.xml</descriptor-file-name> </jdbc-system-resource>
接下来我们可以在WebLogic服务器中部署一个Web应用,示范如何在Web应用中通过数据源的JNDI来访问该数据源。如下JSP代码示范了如何访问WebLogic服务器管理的数据源。
程序清单:codes\04\4.1\dsTest\test.jsp
<% //初始化Context,使用InitialContext初始化Context Context ctx = new InitialContext(); //通过JNDI查找数据源,该JNDI就是部署数据源所指定的JNDI DataSource ds = (DataSource)ctx.lookup("firstds"); //获取数据库连接 Connection conn = ds.getConnection(); //获取Statement Statement stmt = conn.createStatement(); //执行查询,返回ResultSet对象 ResultSet rs = stmt.executeQuery("select * from item"); while(rs.next()) { out.println(rs.getString(1) + " " + rs.getString(2) + "<br/>"); } rs.close(); stmt.close(); conn.close(); %>
使用浏览器访问该页面,将看到如图4.8所示的页面。
图4.8 使用WebLogic服务器管理的数据源
4.1.4 使用JBoss服务器管理的数据源
下面以JBoss服务器为例来介绍如何使用容器管理的数据源。在JBoss服务器中配置数据源的步骤如下。
1 启动JBoss服务器之后,使用浏览器进入JBoss服务器的管理控制台,单击JBoss服务器控制台左边导航面板中的“JBossAS Servers-> JBossAS 5 (default)->Resources->Datasources”节点,该节点下包含3个子节点:
Local Tx Datasources:用于配置局部事务控制的数据源。
No Tx Datasources:用于配置无事务控制的数据源。
XA Datasources:用于配置基于XA事务的全局事务控制的数据源。
2 从上面介绍不难看出,JBoss服务器将局部事务数据源和全局事务数据源分开管理:如果数据库驱动支持XA事务,而且应用程序需要采用全局事务控制,就应该考虑配置XA Datasources;如果数据库驱动不支持XA事务,或者应用程序无须全局事务控制,则可考虑配置Local Tx Datasources;一般来说,很少配置No Tx Datasources。本节以配置连接MySQL数据库的数据源为例,那么应该单击“Local Tx Datasources”节点开始配置局部数据源,将看到如图4.9所示的页面。
图4.9 配置局部事务的数据源
3 在图4.9中可以看到已有一个名为DefaultDS的数据源,这是JBoss服务器自带的一个数据源配置。单击“Add a new resource”按钮开始配置新数据源,系统将出现如图4.10所示的选择数据源模板页面。
图4.10 选择合适的数据源模板
4 由于此处我们配置连接MySQL数据库的数据源,因此从如图4.10所示的下拉列表中选择“defaut (Local Tx Datasource)”列表项,然后单击“Continue”按钮即可看到如图4.11所示的页面。
图4.11 为数据源填写属性
5 在如图4.11所示页面中为新增的数据源填写合适的属性,填写完成后单击“Save”按钮,数据源配置完成。配置完成后再次返回如图4.9所示的页面,从该页面的数据源列表中可以看到新增的firstds数据源。
需要指出的是,虽然配置过程只要这几步就可以完成,由于我们在图4.11中指定连接MySQL数据库的驱动类是com.mysql.jdbc.Driver,但JBoss服务器默认并没有提供该驱动类,因此需要手动将MySQL驱动的JAR包(如mysql-connector-java-3.1.10-bin.jar)文件复制到JBoss的server\default\lib路径下,JBoss服务器将会自动加载该路径下JAR包中的驱动类。
可能有读者会感到奇怪:既然JBoss服务器配置数据源时需要将MySQL驱动的JAR复制到服务器中,为何WebLogic服务器配置数据源时不需要复制呢?这很简单,只是因为WebLogic服务器默认已经包含了绝大部分主流数据库驱动的JAR包。
配置完成后,我们在JBoss的server\default\deloy目录下找到firstds-ds.xml文件,该文件名为“数据源名-ds.xml”的形式,该文件内容如下(笔者添加了简单注释):
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <datasources> <local-tx-datasource> <!-- 数据源的JNDI名 --> <jndi-name>firstds</jndi-name> <rar-name>jboss-local-jdbc.rar</rar-name> <use-java-context>true</use-java-context> <connection-definition>javax.sql.DataSource</connection-definition> <jmx-invoker-name>jboss:service=invoker,type=jrmp</jmx-invoker-name> <!-- 数据源连接池的最小连接数、最大连接数 --> <min-pool-size>1</min-pool-size> <max-pool-size>20</max-pool-size> <blocking-timeout-millis>30000</blocking-timeout-millis> <idle-timeout-minutes>30</idle-timeout-minutes> <prefill>false</prefill> <background-validation>false</background-validation> <background-validation-millis>0</background-validation-millis> <validate-on-match>true</validate-on-match> <statistics-formatter>org.jboss.resource.statistic.pool .JBossDefaultSubPoolStatisticFormatter</statistics-formatter> <isSameRM-override-value>false</isSameRM-override-value> <allocation-retry>0</allocation-retry> <allocation-retry-wait-millis>5000</allocation-retry-wait-millis> <application-managed-security xsi:type="securityMetaData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/> <metadata/> <local-transaction/> <!-- 连接数据库的用户名、密码 --> <user-name>root</user-name> <password>32147</password> <prepared-statement-cache-size>0</prepared-statement-cache-size> <share-prepared-statements>false</share-prepared-statements> <set-tx-query-timeout>false</set-tx-query-timeout> <query-timeout>0</query-timeout> <use-try-lock>0</use-try-lock> <driver-class>com.mysql.jdbc.Driver</driver-class> <!-- 连接数据库的URL --> <connection-url>jdbc:mysql://127.0.0.1:3306/auction</connection-url> </local-tx-datasource> </datasources>
通过上面配置文件不难看出,与WebLogic服务器的策略相似,当通过管理控制台来配置数据源时,实际上JBoss就为我们生成了这个文件。如果读者熟悉这个配置文件,完全可以不按前面介绍的“下一步”、“下一步”的方式来配置该数据源,直接编辑该文件来配置数据源即可。
提示
位于server\default\deloy目录下的每个xxx-ds.xml文件代表一个数据源,其中xxx就是该数据源的名称。在管理控制台每新增一个数据源,这里将会新增一个xxx-ds.xml配置文件。除此之外,开发者还可进入JBoss服务器的docs\examples\jca目录下,会看到连接各种数据库的配置文件范本,开发者可以将所需要的范本复制到server\default\deloy目录下进行简单修改,也可以添加新的数据源。
接下来我们可以在JBoss中部署一个Web应用,示范如何在Web应用中通过数据源的JNDI来访问该数据源。如下JSP代码示范了如何访问JBoss服务器管理的数据源。
程序清单:codes\04\4.1\dsTest.war\test.jsp
<% //初始化Context,使用InitialContext初始化Context Context ctx = new InitialContext(); //JBoss中实际JNDI名字必须在配置的JNDI之前加上java:/前缀 DataSource ds = (DataSource)ctx.lookup("java:/firstds"); //获取数据库连接 Connection conn = ds.getConnection(); //获取Statement Statement stmt = conn.createStatement(); //执行查询,返回ResultSet对象 ResultSet rs = stmt.executeQuery("select * from item"); while(rs.next()) { out.println(rs.getString(1) + " " + rs.getString(2) + "<br/>"); } rs.close(); stmt.close(); conn.close(); %>
使用浏览器访问该页面,同样会看到如图4.8所示的页面。