作者:自由的猪 制作整理:左岸网络http://www.leftworld.net

第16章 资源连接

Dale Green著

Iceshape Zeng译

企业Bean和Web组件(WAR)都可以访问多种资源,包括数据库、邮件服务、JMS对象和URL资源等等。J2EE平台提供了访问这些资源的共同机制。本章介绍了J2EE平台下几种资源的连接方式,虽然都是一企业Bean为例,但是对Web组件也是一样的。

本章内容:

JNDI名和资源引用

在deploytool中配置资源引用

数据库连接

获得连接

连接池

邮件会话连接

运行ConfirmerEJB例子

URL资源连接

运行HTMLReaderEJB例子

一.JNDI名和资源引用

JNDI是Java命名和目录接口的首字母缩写。J2EE平台通过JDNI名来定位提供服务(资源访问)的对象。

JNDI名是由J2EE服务器提供的命名和目录服务绑定到特定对象的用户友好的访问名称,(当然,是不是用户友好的取决于你的命名。)因为J2EE组件通过JNDI API来访问这些服务,所以我们通常称这些对象访问名为JNDI名。例如jdbc/Cloudscape是Cloudscape数据库的JNDI名,设置好后,J2EE服务器在启动的时候从配置文件里读取该信息,并自动将该JNDI名添加到名字空间。

我们并不是直接通过JNDI查找到资源访问对象的。查找得到的是连接工厂。连接工厂“生产”出资源访问对象。数据库资源的连接工厂是javax.sql.DataSource对象,它可以创建java.sql.Connection数据库连接对象。

在代码中,我们并不是接通过JNDI名来查找资源,而是资源引用。具体地说就是通过资源引用来查找资源工厂。资源引用是资源查找中lookup方法的实际参数(当然JNDI名也可以),它在部署描述符中指定。例如下面将提到的例子中的资源引用名为:jdbc/SavingsAccountDB(对应lookup方法的参数为java:comp/env/jdbc/SavingsAccountDB)。

JNDI名和资源引用名是不相同的,所以需要你建立两者之间的映射关系。但是它可以降低组件和资源之间的耦合,这样当组件需要访问不同的资源时,可以不用改变资源引用名。(这里的好处实际上并没有想象的那么好,不过如果两种资源是相同类型的资源,如都是上面的javax.sql.DataSource类型,则完全可以不更改代码,单是如果是不相同的类型,即使资源引用名不改变夜毫无意义,因为无疑你需要更改代码。)这种灵活性是你更容易在已有组件的基础上装配应用程序。(因为引用名是属于使用资源的组件的部署描述符元素,而JNDI名是资源的部署描述符元素,实际上这种做法避免了JNDI命名冲突。)

在deploytool中配置资源引用

这里使用的是第5章的SavingsAccountEJB的例子,详细信息参考第5章第一节。

指定资源引用

1. 在树中选中SavingsAccountEJB节点

2. 选择Resource Refs页签

3. 点击Add按钮

4. 在Coded Name列输入jdbc/SavingsAccountDB。在SavingsAccountBean中数据库的引用编码为:

private String dbName =

"java:comp/env/jdbc/SavingsAccountDB";

java:comp/env是组件的JNDI上下文的名字(实际上这个上下文也作为一种资源来处理了,资源查找的过程可以是这样:jndictxt = ctxt.lookup(“java:comp/env”)然后用这个jndictxt来查找资源,ref = jndictxt.lookup("jdbc/SavingsAccountDB")。)jdbc/SavingsAccountDB是资源引用的JNDI名(The jdbc/SavingsAccountDB string is the JNDI name for the resource reference,这句话可能意味着资源引用实际上也跟资源一样处理成一种JNDI绑定对象了,但是实际上应该不是这样,因为在部署描述符中它是引用名元素。因为译者也不是高手,所以这里的具体实现细节有待读者自己研究了:)所以JDBC的DataSource对象的JNDI名就存储在java:comp/env/jdbc的上下文子对象中。(组件运行环境的上下文层次需要进一步了解)

5. 在Type列中选择javax.sql.DataSource。前面说过它是数据库连接工厂。

6. 在Authentication列选择Container。

7. 如果允许其他数据从DataSource中得到连接,选中Sharable复选框

配置完成,如图16-1

图 16-1 SavingsAccountEJB的Resource Refs 页签

映射资源引用名和JNDI名

1. 在树中选这SavingsAccountApp节点

2. 选择JNDI Names页签

3. 在References表里,选择资源引用的一行(Ref Type为Resource)。

4. 在JNDI Name列中输入JNDI名,本例为jdbc/Cloudscape。

这些工作也可以在SavingsAccountJAR节点的JNDI Names页签里完成。配置结束,如图16-2

图 16-2 SavingsAccountApp的JNDI Names 页签

二.数据库连接

企业Bean持久性机制决定了你是否要为处理数据库连接编写代码。不是采用CMP的企业Bean都必须自己编写连接处理的代码,包括BMP实体Bean和会话Bean。对于CMP实体Bean,deploytool工具会自动产生连接处理的代码。

获得连接

如何获得连接

以SaingsAccountBean为例:

1. 确定数据库名:

private String dbName =

"java:comp/env/jdbc/SavingsAccountDB";

2. 获得数据库连接工厂:

InitialContext ic = new InitialContext();

DataSource ds = (DataSource) ic.lookup(dbName);

3. 从工厂获得连接:

Connection con =  ds.getConnection();

获得连接的时机

编写企业Bean时,你需要决定连接保持的时间。通常有两种选择:持续连接,在企业Bean的生命周期中一直保持连接或者在每一个数据库操作期间保持连接。临时连接,仅在每一个数据操作期间保持连接。这两种连接方式将决定数据操作方法的实现。

持续连接

你可以设计一个企业Bean,在它的整个生命周期中保持数据库连接。因为它连接数据库和断开数据库连接都只进行一次,所以它的方法得到连接都很简单。但是它有一个缺点:其他组件在该Bean的生命周期内就不能使用该连接(它独占了一个连接资源)。会话Bean和实体Bean在不同的方法中获得持续连接。

会话Bean

EJB容器在会话Bean生命周期开始的时候调用ejbCreate方法在结束的时候调用ejbRemove方法。所以会话Bean的持续连接在ejbCreate方法中获得,并在ejbRemove方法中断开。如果是有状态会话Bean你也必须在ejbActive中取得连接并在ejbPassivate中断开连接。有状态会话Bean之所以需要这些额外的连接和断开动作,是因为EJB容器在它的生命周期内可能会钝化它。在钝化期间,该有状态会话Bean被保存在二级存储器(硬盘)中,但是数据库连接不能保存在这里。而无状态会话Bean不会被钝化,所以它不需要在ejbActive和ejbPassivate方法中获得和断开数据库连接。关于激活和钝化的更多信息请参考有状态会话Bean的生命周期(第3章第8节)。在j2eetutorial/examples/ejb/teller目录下的TellerBean.java是使用持续连接的有状态会话Bean的例子。

BMP实体Bean

当实例化一个实体Bean并把它放入Bean池中的时候EJB容器调用setEntityContext方法。相对地,在当实体Bean从Bean池中被移除并成为垃圾收集器的目标时EJB容器调用unsetEntityContext方法。所以BMP实体Bean的持续连接在setEntityContext方法中获得并在unsetEntityContext方法中断开。实体Bean的生命周期参考图3-5(第3章第8节)。在j2eetutorial/examples/ejb/savingsaccount目录下的SavingsAccountBean.java是使用持续连接的实体Bean的例子。

临时连接

只在使用时暂时占有连接可以使许多组件共用同一个资源连接。因为EJB容器管理一个数据库连接池,企业Bean可以快速地获得和释放连接。例如,一个商业方法可以得到连接,插入一行数据,然后释放连接。在会话Bean中,商业方法连接数据库会启动一个事务以保证数据完整性。

用deploytool指定数据库用户名和密码

下面的方法不适合CMP实体Bean,关于CMP实体Bean的数据库连接方法请参考第6章CMP例子中的相关内容。这种Claudscape数据库连接不需要指定用户名和密码,因为权限验证已被分开成为另一项服务。更多信息请参考第15章。

然而,很多其它类型的数据库系统却需要提供用户名和密码来获得连接。对于这些数据库系统如果调用无参数的getConnection方法,那么必须在deploytool中指定数据库的用户名和密码:

1.在树视图中选中企业Bean节点

2.选择Resource Refs页签

3.在Resource Factories Referenced in Code表格中选择对应的行,然后在页签下面的输入框中分别输入数据库的用户名和密码。

如果你想在程序中指定用户名和密码,就不需要deploytool来指定了,只是在或的数据库连接的时候要用它们作为getConnection方法的参数:

con = dataSource.getConnection(dbUser, dbPassword);

连接池

EJB容器管理数据库连接池,连接池对企业Bean是透明的。当企业Bean请求一个连     接时,容器从连接池中取出一个连接给企业Bean。因为连接早就建立好了,所以企业Bean可以更快的得到一个连接。因为企业Bean这样可以快速得到连接,所以可以在每次数据库操作结束后就马上释放连接。企业Bean占用一个连接只是很短的时间,因而同样的连接可以被很多的企业Bean使用。

三.邮件服务连接

如果你在某个网站上订购一件商品,就会收到一封确认订单的e-mail。本节的例子ConfirmBean类说明了如何用企业Bean来发送e-mail。该例子的源文件放在j2eetutorial/examples/src/ejb/confirmer目录下。进入j2eetutorial/examples目录用ant confirmer命令编译该例子,一个样本文件放在j2eetutorial/examples/ears目录下。

在ConfirmerBean的sendNotice方法中,lookup方法返回一个邮件会话对象。和数据库连接一样,邮件会话也是一种资源,和别的资源一样,需要将编码引用名(TheMailSession)关联到一个JNDI名。用该会话对象作为参数,sendNotice方法创建了一个空的Message对象,在调用Message对象的一些set方法后,sendNotice方法调用Transport类的send方法送这个消息对象上路了。源代码如下:

public void sendNotice(String recipient) {

   try {

       Context initial = new InitialContext();

       Session session =

         (Session) initial.lookup(

         "java:comp/env/TheMailSession");

       Message msg = new MimeMessage(session);

       msg.setFrom();

       msg.setRecipients(Message.RecipientType.TO,

          InternetAddress.parse(recipient, false));

       msg.setSubject("Test Message from ConfirmerBean");

       DateFormat dateFormatter =

         DateFormat.getDateTimeInstance(

         DateFormat.LONG, DateFormat.SHORT);

       Date timeStamp = new Date();

       String messageText = "Thank you for your order." + '\n' +

          "We received your order on " +

          dateFormatter.format(timeStamp) + ".";

       msg.setText(messageText);

       msg.setHeader("X-Mailer", mailer);

       msg.setSentDate(timeStamp);

       Transport.send(msg);

   } catch(Exception e) {

       throw new EJBException(e.getMessage());

   }

}

运行ConfirmerEJB例子

部署该应用程序

1. 在deploytool部署工具中打开j2eetutorial/examples/ears/ConfirmerApp.ear文件

2. 在Resource Refs页签中根据如下表格确定资源连接各项属性的值:

表16-1 Resource Refs页签属性值

属性

Coded Name

TheMailSession

Type

javax.mail.Session

Authentication

Application

From

(你的email地址)

Host

邮件服务器地址

User Name

连接邮件服务器的用户名

3.  部署该应用程序(ToolsàDeploy菜单)。在Introduction对话框中确定Return Client JAR复选框被选中。

运行客户端

1.在终端窗口中进入j2eetutorial/examples/ears目录

2.将APPCPATH环境变量设置为ConfirmerAppClient.jar所在目录

3.运行如下命令(将<recipient>替换为邮件的目的地址):

runclient -client ConfirmerApp.ear -name ConfirmerClient -textauth <recipient>

4.用户名:guest。密码:guest123。

错误处理

如果应用程序无法连接邮件服务器将抛出如下异常:

javax.mail.MessagingException: Could not connect to SMTP host

如果出现这个错误,请检查邮件服务器是否运行和邮件服务器地址是否正确(Resource Refs页签)。

四.URL资源连接

URL表明一个资源在Web中的位置。HTMLReaderBean类示例如何在企业Bean内部连接一个URL资源。该例子的源文件在j2eetutorial/examples/src/ejb/htmlreader目录下,进入j2eetutorial/examples目录执行ant htmlreader命令编译该例子。一个样本文件j2eetutorial/examples/ears在目录下。

HTMLReaderBean类的getContents方法返回包含HTML文件内容的字符串,该方法先查找跟url/MyURL引用名关联的java.net.URL对象,打开一个到该对象的连接,然后从InputStream对象中读出文件内容。在部署该应用程序之前,必须先将引用名(url/MyURL)和帮定该对象的JNDI名关联起来。GetContents方法的源代码如下:

public StringBuffer getContents() throws HTTPResponseException

{

   Context context;

   URL url;

   StringBuffer buffer;

   String line;

   int responseCode;

   HttpURLConnection connection;

   InputStream input;

   BufferedReader dataInput;

   try {

      context = new InitialContext();

      url = (URL)context.lookup("java:comp/env/url/MyURL"); 

      connection = (HttpURLConnection)url.openConnection();

      responseCode = connection.getResponseCode();

   } catch (Exception ex) {

       throw new EJBException(ex.getMessage());

   }

   if (responseCode != HttpURLConnection.HTTP_OK) {

      throw new HTTPResponseException("HTTP response code: " +

         String.valueOf(responseCode));

   }

   try {

      buffer = new StringBuffer();

      input = connection.getInputStream();

      dataInput =

          new BufferedReader(new InputStreamReader(input));

      while ((line = dataInput.readLine()) != null) {

         buffer.append(line);

         buffer.append('\n');

      } 

   } catch (Exception ex) {

       throw new EJBException(ex.getMessage());

   }

   return buffer;

}

运行HTMLReaderEJB例子

部署应用程序

1.在deploytool部署工具中打开j2eetutorial/examples/ears/HTMLReaderApp.ear文件

2.部署HTMLReaderApp应用程序。确定在Introduction对话框中选中了Return Client JAR复选框。

运行客户端

1.在终端窗口中进入j2eetutorial/examples/ears目录

2.设置APPCPATH环境变量为HTMLReaderAppClient.jar所在目录

3.执行如下命令:

runclient -client HTMLReaderApp.ear –name HTMLReaderClient -textauth

4.用户名:guest。密码:guest123。

5.运行结果在J2EE SDK安装目录的public_html子目录下的index.html文件中