本Java教程适用于JDK 8。本页面描述的示例和实践不利用后续版本中引入的改进,并可能使用不再可用的技术。
请参阅Java语言更改以获取Java SE 9及后续版本中更新的语言功能摘要。
请参阅JDK发行说明以获取有关所有JDK版本的新功能、增强功能和已删除或弃用选项的信息。
本节介绍了DataSource
对象,它是获取与数据源连接的首选方法。除了其他优点之外,DataSource
对象还可以提供连接池和分布式事务。这些功能对于企业数据库计算至关重要,特别是对于企业JavaBean(EJB)技术。
本节将向您展示如何使用DataSource
接口获取连接以及如何使用分布式事务和连接池。在您的JDBC应用程序中,这些操作都只需要进行很少的代码更改。
部署使这些操作成为可能的类的工作,通常由系统管理员使用工具(如Apache Tomcat或Oracle WebLogic Server)完成,这取决于要部署的DataSource
对象的类型。因此,本节的大部分内容都是关于如何设置环境,以便程序员可以使用DataSource
对象获取连接。
本节涵盖以下主题:
在建立连接一节中,您学习了如何使用DriverManager
类获取连接。本节将向您展示如何使用DataSource
对象获取与数据源的连接,这是首选方式。
由实现DataSource
的类实例化的对象表示特定的DBMS或其他数据源,例如文件。一个DataSource
对象表示特定的DBMS或其他数据源,例如文件。如果一个公司使用多个数据源,它将为每个数据源部署一个单独的DataSource
对象。DataSource
接口由驱动程序供应商实现。它可以以三种不同的方式实现:
DataSource
实现产生不参与连接池或分布式事务的标准Connection
对象。DataSource
实现产生参与连接池的Connection
对象,即可以回收的连接。DataSource
实现产生可用于分布式事务的Connection
对象,即访问两个或多个DBMS服务器的事务。JDBC驱动程序至少应包括一个基本的DataSource
实现。例如,Java DB JDBC驱动程序包括实现org.apache.derby.jdbc.ClientDataSource
,而对于MySQL,则是com.mysql.jdbc.jdbc2.optional.MysqlDataSource
。如果您的客户端运行在Java 8紧凑配置文件2上,则Java DB JDBC驱动程序是org.apache.derby.jdbc.BasicClientDataSource40
。本教程的示例要求紧凑配置文件3或更高版本。
支持分布式事务的DataSource
类通常还实现了连接池的支持。例如,由EJB供应商提供的DataSource
类几乎总是同时支持连接池和分布式事务。
假设前面示例中繁荣的The Coffee Break商店连锁的老板决定通过互联网进一步扩张销售咖啡。考虑到预计的大量在线业务,老板肯定需要连接池。打开和关闭连接涉及大量开销,老板预计这个在线订购系统将需要大量的查询和更新操作。使用连接池,可以反复使用一组连接,避免为每次数据库访问创建新连接的开销。此外,老板现在有了第二个包含最近收购的咖啡烘焙公司数据的DBMS。这意味着老板希望能够编写同时使用旧的DBMS服务器和新的DBMS服务器的分布式事务。
连锁店老板已重新配置计算机系统以为新的更大的客户群提供服务。老板购买了最新的JDBC驱动程序和与之配套的EJB应用服务器,以便能够使用分布式事务并获得连接池带来的性能提升。有许多与最近购买的EJB服务器兼容的JDBC驱动程序可用。老板现在拥有一个三层架构,中间层是一个新的EJB应用服务器和JDBC驱动程序,第三层是两个DBMS服务器。发送请求的客户端计算机是第一层。
系统管理员需要部署DataSource
对象,以便The Coffee Break的编程团队可以开始使用它们。部署一个DataSource
对象包括三个任务:
DataSource
类的实例首先,考虑最基本的情况,即使用DataSource
接口的基本实现,即不支持连接池或分布式事务的实现。在这种情况下,只需要部署一个DataSource
对象。一个DataSource
的基本实现会产生与DriverManager
类产生的相同类型的连接。
假设一家只想要一个基本实现的DataSource
的公司从JDBC供应商DB Access,Inc购买了一个驱动程序。该驱动程序包括实现了DataSource
接口的com.dbaccess.BasicDataSource
类。下面的代码片段创建了BasicDataSource
类的一个实例,并设置了其属性。在部署了BasicDataSource
的实例后,程序员可以调用方法DataSource.getConnection
来获取连接到公司的数据库CUSTOMER_ACCOUNTS
。首先,系统管理员使用默认构造函数创建BasicDataSource
对象ds
。然后,系统管理员设置了三个属性。请注意,以下代码通常由部署工具执行:
com.dbaccess.BasicDataSource ds = new com.dbaccess.BasicDataSource(); ds.setServerName("grinder"); ds.setDatabaseName("CUSTOMER_ACCOUNTS"); ds.setDescription("Customer accounts database for billing");
变量ds
现在表示安装在服务器上的数据库CUSTOMER_ACCOUNTS
。由BasicDataSource
对象ds
产生的任何连接都将是到数据库CUSTOMER_ACCOUNTS
的连接。
设置了属性后,系统管理员可以将BasicDataSource
对象注册到一个JNDI(Java命名和目录接口)命名服务中。通常使用的特定命名服务由系统属性决定,在此处未显示。以下代码片段将BasicDataSource
对象注册并绑定到逻辑名称jdbc/billingDB
:
Context ctx = new InitialContext(); ctx.bind("jdbc/billingDB", ds);
此代码使用了JNDI API。第一行创建了一个InitialContext
对象,它作为一个名称的起始点,类似于文件系统中的根目录。第二行将BasicDataSource
对象ds
与逻辑名称jdbc/billingDB
关联或绑定在一起。在下一个代码片段中,您将给命名服务提供此逻辑名称,它会返回BasicDataSource
对象。逻辑名称可以是任何字符串。在这种情况下,公司决定将名称billingDB
作为CUSTOMER_ACCOUNTS
数据库的逻辑名称。
在前面的示例中,jdbc
是初始上下文的子上下文,就像根目录下的目录是子目录一样。名称jdbc/billingDB
类似于路径名,路径中的最后一个项类似于文件名。在这种情况下,billingDB
是给BasicDataSource
对象ds
的逻辑名称。子上下文jdbc
保留用于将逻辑名称绑定到DataSource
对象,因此jdbc
始终是数据源的逻辑名称的第一部分。
在系统管理员部署了一个基本的DataSource
实现后,程序员就可以使用它了。这意味着程序员可以提供绑定到DataSource
类实例的逻辑数据源名称,JNDI命名服务将返回该DataSource
类的实例。然后可以在该DataSource
对象上调用getConnection
方法来获取与其代表的数据源的连接。例如,程序员可以编写以下两行代码来获取一个产生连接到数据库CUSTOMER_ACCOUNTS
的DataSource
对象。
Context ctx = new InitialContext(); DataSource ds = (DataSource)ctx.lookup("jdbc/billingDB");
第一行代码获取一个初始上下文作为检索DataSource
对象的起点。当您向lookup
方法提供逻辑名称jdbc/billingDB
时,该方法将返回系统管理员在部署时绑定到jdbc/billingDB
的DataSource
对象。因为lookup
方法的返回值是一个Java Object
,所以在将其赋值给变量ds
之前,我们必须将其转换为更具体的DataSource
类型。
变量ds
是实现DataSource
接口的com.dbaccess.BasicDataSource
类的实例。调用ds.getConnection
方法将产生与CUSTOMER_ACCOUNTS
数据库的连接。
Connection con = ds.getConnection("fernanda","brewed");
getConnection
方法只需要用户名和密码,因为变量ds
的属性中包含与CUSTOMER_ACCOUNTS
数据库建立连接所需的其余信息,如数据库名称和位置。
由于其属性,DataSource
对象是获取连接的比DriverManager
类更好的选择。程序员不再需要在应用程序中硬编码驱动程序名称或JDBC URL,这使得它们更具可移植性。此外,DataSource
属性使代码维护更简单。如果发生更改,系统管理员可以更新数据源属性,而不必担心更改每个与数据源建立连接的应用程序。例如,如果数据源被移动到另一台服务器,系统管理员只需将serverName
属性设置为新的服务器名称。
除了可移植性和易于维护之外,使用DataSource
对象来获取连接还可以提供其他优势。当DataSource
接口被实现以与ConnectionPoolDataSource
实现一起工作时,该DataSource
类的所有连接都将自动成为连接池连接。类似地,当DataSource
实现与XADataSource
类一起工作时,它所产生的所有连接都将自动成为可以在分布式事务中使用的连接。下一节将展示如何部署这些类型的DataSource
实现。
系统管理员或其他从事该工作的人可以部署一个DataSource
对象,以便产生连接池连接。为此,他或她首先部署一个ConnectionPoolDataSource
对象,然后部署一个与其配合工作的DataSource
对象。将ConnectionPoolDataSource
对象的属性设置为表示将产生连接的数据源。在将ConnectionPoolDataSource
对象注册到JNDI命名服务之后,将部署DataSource
对象。通常只需为DataSource
对象设置两个属性:description
和dataSourceName
。给dataSourceName
属性赋值是之前部署的ConnectionPoolDataSource
对象的逻辑名称,该对象包含了建立连接所需的属性。
使用部署好的ConnectionPoolDataSource
和DataSource
对象,可以在DataSource
对象上调用DataSource.getConnection
方法并获得一个连接池连接。此连接将是ConnectionPoolDataSource
对象属性中指定的数据源。
下面的示例描述了The Coffee Break的系统管理员如何部署一个实现提供连接池连接的DataSource
对象。系统管理员通常会使用一个部署工具,因此本节中显示的代码片段是部署工具将执行的代码。
为了获得更好的性能,The Coffee Break公司从DB Access公司购买了一个包含com.dbaccess.ConnectionPoolDS
类的JDBC驱动程序,该类实现了ConnectionPoolDataSource
接口。系统管理员创建了这个类的一个实例,设置了其属性,并将其注册到JNDI命名服务。The Coffee Break从其EJB服务器供应商Application Logic, Inc购买了其DataSource
类com.applogic.PooledDataSource
。该类通过使用底层支持提供的ConnectionPoolDataSource
类com.dbaccess.ConnectionPoolDS
来实现连接池。
必须首先部署ConnectionPoolDataSource
对象。以下代码创建了一个com.dbaccess.ConnectionPoolDS
的实例并设置其属性:
com.dbaccess.ConnectionPoolDS cpds = new com.dbaccess.ConnectionPoolDS(); cpds.setServerName("creamer"); cpds.setDatabaseName("COFFEEBREAK"); cpds.setPortNumber(9040); cpds.setDescription("COFFEEBREAK DBMS的连接池");
在部署了ConnectionPoolDataSource
对象之后,系统管理员会部署DataSource
对象。以下代码将com.dbaccess.ConnectionPoolDS
对象cpds
注册到JNDI命名服务中。请注意,与cpds
变量关联的逻辑名称在jdbc
的子上下文下添加了pool
子上下文,类似于在分层文件系统中将一个子目录添加到另一个子目录中。类com.dbaccess.ConnectionPoolDS
的任何实例的逻辑名称始终以jdbc/pool
开头。Oracle建议将所有ConnectionPoolDataSource
对象放在jdbc/pool
子上下文下:
Context ctx = new InitialContext(); ctx.bind("jdbc/pool/fastCoffeeDB", cpds);
接下来,部署实现与cpds
变量和其他com.dbaccess.ConnectionPoolDS
类实例交互的DataSource
类。以下代码创建了该类的一个实例并设置了其属性。请注意,对于com.applogic.PooledDataSource
的这个实例,仅设置了两个属性。设置了description
属性,因为它始终是必需的。设置的另一个属性dataSourceName
给出了cpds
的逻辑JNDI名称,它是com.dbaccess.ConnectionPoolDS
类的一个实例。换句话说,cpds
表示将为DataSource
对象实现连接池的ConnectionPoolDataSource
对象。
以下代码(可能由部署工具执行)创建了一个PooledDataSource
对象,设置了其属性,并将其绑定到逻辑名称jdbc/fastCoffeeDB
:
com.applogic.PooledDataSource ds = new com.applogic.PooledDataSource(); ds.setDescription("用于产生到COFFEEBREAK的连接池连接"); ds.setDataSourceName("jdbc/pool/fastCoffeeDB"); Context ctx = new InitialContext(); ctx.bind("jdbc/fastCoffeeDB", ds);
此时,已经部署了一个DataSource
对象,应用程序可以从中获取到数据库COFFEEBREAK
的连接池连接。
连接池是数据库连接对象的缓存。这些对象表示应用程序用于连接数据库的物理数据库连接。在运行时,应用程序从连接池请求连接。如果连接池包含满足请求的连接,则将连接返回给应用程序。如果找不到连接,则创建一个新的连接并返回给应用程序。应用程序使用连接在数据库上执行一些工作,然后将对象返回到连接池。连接随后可用于下一个连接请求。
连接池促进了连接对象的重用,减少了创建连接对象的次数。连接池显著提高了数据库密集型应用的性能,因为创建连接对象在时间和资源方面都是昂贵的。
现在这些DataSource
和ConnectionPoolDataSource
对象已经部署,程序员可以使用DataSource
对象获取一个连接池连接。获取连接池连接的代码与获取非连接池连接的代码是相同的,如下面两行所示:
ctx = new InitialContext(); ds = (DataSource)ctx.lookup("jdbc/fastCoffeeDB");
变量ds
表示一个DataSource
对象,用于生成到数据库COFFEEBREAK
的连接池连接。你只需要检索一次这个DataSource
对象,因为你可以使用它生成需要的任意多个连接池连接。调用ds
变量上的getConnection
方法会自动产生一个连接池连接,因为ds
变量所代表的DataSource
对象被配置为生成连接池连接。
连接池对程序员来说通常是透明的。在使用连接池连接时,只需要做两件事:
使用DataSource
对象而不是DriverManager
类来获取连接。在下面的代码中,ds
是一个已经实现和部署为创建连接池连接的DataSource
对象,username
和password
是表示具有访问数据库权限的用户的凭据的变量:
Connection con = ds.getConnection(username, password);
使用finally
语句关闭连接池连接。下面的finally
块将出现在应用于使用连接池连接的代码的try/catch
块之后:
try { Connection con = ds.getConnection(username, password); // ... 用连接池连接con的代码 } catch (Exception ex) { // ... 处理异常的代码 } finally { if (con != null) con.close(); }
除此之外,使用连接池连接的应用与使用常规连接的应用是相同的。当进行连接池时,应用程序员可能注意到的唯一其他事情是性能更好。
以下示例代码获取一个DataSource
对象,该对象生成到数据库COFFEEBREAK
的连接,并使用它更新表COFFEES
中的价格:
import java.sql.*; import javax.sql.*; import javax.ejb.*; import javax.naming.*; public class ConnectionPoolingBean implements SessionBean { // ... public void ejbCreate() throws CreateException { ctx = new InitialContext(); ds = (DataSource)ctx.lookup("jdbc/fastCoffeeDB"); } public void updatePrice(float price, String cofName, String username, String password) throws SQLException{ Connection con; PreparedStatement pstmt; try { con = ds.getConnection(username, password); con.setAutoCommit(false); pstmt = con.prepareStatement("UPDATE COFFEES " + "SET PRICE = ? " + "WHERE COF_NAME = ?"); pstmt.setFloat(1, price); pstmt.setString(2, cofName); pstmt.executeUpdate(); con.commit(); pstmt.close(); } finally { if (con != null) con.close(); } } private DataSource ds = null; private Context ctx = null; }
此代码示例中的连接参与了连接池,因为以下条件成立:
ConnectionPoolDataSource
的类的实例。DataSource
的类的实例,并且为其dataSourceName
属性设置的值是先前部署的ConnectionPoolDataSource
对象绑定的逻辑名称。请注意,尽管此代码与您之前看到的代码非常相似,但在以下方面存在差异:
它导入了javax.sql
、javax.ejb
和javax.naming
包,以及java.sql
包。
DataSource
和ConnectionPoolDataSource
接口位于javax.sql
包中,JNDI构造函数InitialContext
和方法Context.lookup
属于javax.naming
包。此特定示例代码是以EJB组件的形式编写的,使用了javax.ejb
包中的API。此示例的目的是展示您如何使用池化连接与非池化连接的方式相同,因此无需担心理解EJB API。
它使用DataSource
对象获取连接,而不是使用DriverManager
工具。
它使用finally
块确保连接被关闭。
获取和使用池化连接与获取和使用常规连接类似。当扮演系统管理员角色的人正确部署了ConnectionPoolDataSource
对象和DataSource
对象时,应用程序使用该DataSource
对象获取池化连接。然而,应用程序应使用finally
块关闭池化连接。为简单起见,上面的示例使用了finally
块,但没有使用catch
块。如果try
块中的方法抛出异常,默认情况下会抛出异常,并且finally
子句将在任何情况下执行。
DataSource
对象可以部署以获取在分布式事务中可以使用的连接。与连接池类似,必须部署两个不同的类实例:一个是XADataSource
对象,另一个是与其配套使用的DataSource
对象。
假设The Coffee Break企业购买的EJB服务器包含DataSource
类com.applogic.TransactionalDS
,它可以与com.dbaccess.XATransactionalDS
等XADataSource
类一起使用。它能与任何XADataSource
类一起工作,使得EJB服务器在JDBC驱动程序之间具有可移植性。当部署DataSource
和XADataSource
对象时,所产生的连接将能够参与分布式事务。在这种情况下,com.applogic.TransactionalDS
类的实现使得所产生的连接也是池化连接,这通常是作为EJB服务器实现的一部分提供的DataSource
类的情况。
首先必须部署XADataSource
对象。以下代码创建了com.dbaccess.XATransactionalDS
类的一个实例并设置其属性:
com.dbaccess.XATransactionalDS xads = new com.dbaccess.XATransactionalDS(); xads.setServerName("creamer"); xads.setDatabaseName("COFFEEBREAK"); xads.setPortNumber(9040); xads.setDescription("COFFEEBREAK DBMS的分布式事务");
以下代码将com.dbaccess.XATransactionalDS
对象xads
注册到JNDI命名服务。注意,将逻辑名称与xads
关联时,在jdbc
下添加了子上下文xa
。Oracle建议com.dbaccess.XATransactionalDS
类的任何实例的逻辑名称始终以jdbc/xa
开头。
Context ctx = new InitialContext(); ctx.bind("jdbc/xa/distCoffeeDB", xads);
接下来,部署与xads
和其他XADataSource
对象交互的DataSource
对象。请注意,DataSource
类com.applogic.TransactionalDS
可以与任何JDBC驱动程序供应商的XADataSource
类配套使用。部署DataSource
对象涉及创建com.applogic.TransactionalDS
类的一个实例并设置其属性。将dataSourceName
属性设置为与com.dbaccess.XATransactionalDS
关联的逻辑名称jdbc/xa/distCoffeeDB
。这是实现DataSource
类的分布式事务功能的XADataSource
类。以下代码部署了DataSource
类的一个实例:
com.applogic.TransactionalDS ds = new com.applogic.TransactionalDS(); ds.setDescription("用于生成到COFFEEBREAK的分布式事务连接"); ds.setDataSourceName("jdbc/xa/distCoffeeDB"); Context ctx = new InitialContext(); ctx.bind("jdbc/distCoffeeDB", ds);
现在,已经部署了com.applogic.TransactionalDS
和com.dbaccess.XATransactionalDS
类的实例,应用程序可以在TransactionalDS
类的实例上调用getConnection
方法,以获取可用于分布式事务的COFFEEBREAK
数据库连接。
要获取可用于分布式事务的连接,必须使用已正确实现和部署的DataSource
对象,如在部署分布式事务部分所示。使用这样的DataSource
对象,调用其上的getConnection
方法。获得连接后,可以像使用其他连接一样使用它。由于jdbc/distCoffeesDB
已经与JNDI命名服务中的XADataSource
对象关联,下面的代码会生成一个可用于分布式事务的Connection
对象:
Context ctx = new InitialContext(); DataSource ds = (DataSource)ctx.lookup("jdbc/distCoffeesDB"); Connection con = ds.getConnection();
在连接参与分布式事务时,对连接的使用有一些次要但重要的限制。事务管理器控制分布式事务何时开始以及何时提交或回滚,因此应用程序代码不应调用Connection.commit
或Connection.rollback
方法。同样,应用程序也不应调用Connection.setAutoCommit(true)
方法,该方法会启用自动提交模式,因为这样会干扰事务管理器对事务边界的控制。这就解释了在分布式事务的范围内创建的新连接默认情况下禁用自动提交模式的原因。请注意,这些限制仅适用于连接参与分布式事务时;连接不参与分布式事务时没有限制。
对于以下示例,假设已发货一份咖啡订单,这会触发对位于不同数据库管理系统服务器上的两个表的更新。第一个表是一个新的INVENTORY
表,第二个是COFFEES
表。由于这些表位于不同的数据库管理系统服务器上,涉及到它们的事务将是一个分布式事务。下面示例中的代码获取连接,更新COFFEES
表,然后关闭连接,是分布式事务的第二部分。
请注意,代码并没有显式地提交或回滚更新,因为分布式事务的范围由中间层服务器的基础系统基础设施控制。此外,假设用于分布式事务的连接是一个连接池连接,应用程序使用finally
块来关闭连接。这样可以确保即使抛出异常,也会关闭有效的连接,从而确保连接被返回到连接池以进行回收。
以下代码示例演示了一个企业Bean,它是一个实现了可以被客户端调用的方法的类。此示例的目的是演示分布式事务的应用程序代码与其他代码没有任何区别,只是它不调用Connection
的commit
、rollback
或setAutoCommit(true)
方法。因此,您不需要担心理解所使用的EJB API。
import java.sql.*; import javax.sql.*; import javax.ejb.*; import javax.naming.*; public class DistributedTransactionBean implements SessionBean { // ... public void ejbCreate() throws CreateException { ctx = new InitialContext(); ds = (DataSource)ctx.lookup("jdbc/distCoffeesDB"); } public void updateTotal(int incr, String cofName, String username, String password) throws SQLException { Connection con; PreparedStatement pstmt; try { con = ds.getConnection(username, password); pstmt = con.prepareStatement("UPDATE COFFEES " + "SET TOTAL = TOTAL + ? " + "WHERE COF_NAME = ?"); pstmt.setInt(1, incr); pstmt.setString(2, cofName); pstmt.executeUpdate(); stmt.close(); } finally { if (con != null) con.close(); } } private DataSource ds = null; private Context ctx = null; }