本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;
}