用EJB开发订阅信息系统
作者:板桥banq
上页
5.5 表现层的设计和实现
上节中已经实现了Strut的Action类CustomerAction,这里将在Strut框架进行其他的操作步骤,JBuilder 8以后版本整合了Strut框架,因此利用JBuilder提供的可视化开发可以提高速度。
5.5.1 创建ActionForm
在JBuilder中选择New→web appication→Action Form,在下一步画面中加入如下参数:String id、String firstname 和String lastName等,完成后的CustomerForm基本与Customer类似,CustomerForm代码主要如下(和Customer相同部分省略)。
public class CustomerForm extends ActionForm {
…
public ActionErrors validate(ActionMapping actionMapping,
HttpServletRequest httpServletRequest) {
ActionErrors errors = new ActionErrors();
if ( (id == null) || (id.length() < 1)) {
errors.add("id", new ActionError("error.id.required"));
}
return errors;
}
public void reset(ActionMapping actionMapping,
HttpServletRequest httpServletRequest) {
this.action = "create";
this.id = null;
this.firstName = null;
this.lastName = null;
}
}
在validate加入了对id的检查,如果id为空,报错。error.id.required的设置需要在ApplicationResources.properties中输入:
error.id.required=you must input id
# Errors
errors.footer=<hr>
errors.header=<h3><font color="red">Validation Error</font></h3>
You must correct the following error
errors.footer和errors.header是出错信息的头部和尾部信息定义。
生成ApplicationResources.properties后,需要在struts-config.xml中加入:
<message-resources parameter="com.jdon.samples.ApplicationResources" />
当然,别忘记在具体JSP中加入<struts:errors/>来显示出错信息。
5.5.2 创建Action 类
Action类主要是接受处理前台表单输入的数据,CustomerAction代码已经在前面章节罗列出,该Action类有两个步骤:
将CustomerForm中的数据导入CustomerDTO,CustomerForm中数据是JSP页面用户输入的值。
将CustomerDTO对象传送访问EJB层,实现customer数据的插入数据库操作。
CustomerAction处理成功后,将页面导向createCustomOk页面,这就需要在struts-config.xml中配置createCustomOk指向的页面。
5.5.3 创建JSP页面
createCustomer.jsp代码如下:
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>
<%@ taglib uri="/WEB-INF/struts-template.tld" prefix="template" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<html:html>
<head><title>/createCustomer.jsp</title></head>
<body><center><h3>客户资料输入</h3>
<p>
<html:form action="/customerAction.do" method="POST">
<table border=10>
<tr>
<td>Customer ID: </td>
<td><html:text property="id"/></td>
</tr>
<tr>
<td>First Name: </td>
<td><html:text property="firstName"/></td>
</tr>
<tr>
<td>Last Name: </td>
<td><html:text property="lastName"/></td>
</tr>
</table>
<html:submit property="submit" value="Submit"/><br>
<html:reset value ="Reset"/>
</html:form></center></body></html:html>
提交customerAction.do 指向的CustomerAction类处理成功后,createCustomerOk.jsp代码如下:
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>
<html:html>
<html><head><title>createCustomerOK</title></head>
<body bgcolor="#ffffff">
<h3>
成功创建新客户
<!-- 显示customerForm中的两个字段 -->
<bean:write name="customerForm" property="lastName" />
<bean:write name="customerForm" property="firstName" />
</h3><center>
</body></html></html:html>
本项目中其他JSP和Action相关程序实现可参考源码。
5.6 调试配置和运行
EJB层和Web层开发完成后,可以进行各自的单元测试。通过使用JBuilder等IDE工具提供的EJB Test工具可以直接建立一些客户端对EJB实现测试。当然,EJB需要部署到EJB容器中才能运行,而各个EJB容器在遵循J2EE标准的总体框架下有些细微的区别。
本项目使用JBoss作为J2EE服务器,MySQL作为数据库服务器。目前需要对这两者进行配置,使得JBoss具备访问MySQL数据库的能力。
5.6.1 JBoss和MySQL的配置
各个版本JBoss和MySQL的配置有些细节的区别,总体可以掌握下列几点。
在JBoss/Docs/JCA下有各个数据库的配置文件,对于不同JBoss版本,这个名称不一样,JBoss 3.0的是mysql-service.xml,而JBoss 3.2.1的是mysql-ds.xml。将这个配置文件复制到server/default/deploy下,然后开始修改。
配置MySQL的JDBC驱动,有两步:首先要将MySQL的Java JDBC复制到JBoss的server/default/lib中。这样可以让JBoss获得对MySQL数据库驱动操作能力。
注意MySQL-service.xml中MySQL的JNDI名称是MySQLDS,在EJB的配置文件中要用到这个JNDI Name,用时的写法是java:/MySQLDS。
修改mysql-service.xml或mysql-ds.xml中相关配置为你自己的配置,如数据库名称,数据库访问用户名和密码,教程示例中有一个MySQL-service.xml,可以参考一下。
只要上面几步正确配置,你的JBoss就基本可以了,更深入的是配置JAAS和Role以及修改login-config.xml等,这些是高级教程,可暂时不必理会。
针对本项目,为了让EJB代码在JBoss中运行起来,还需要将JBoss的DataSource告诉EJB代码,这样,EJB代码才能通过DataSource寻找到JBoss提供的MySQL数据库访问通道(在这个通道中包括JBoss提供的事务机制、数据库连接池等)。
首先需要让JBoss具备访问MySQL的能力,上面建议步骤中最重要的是编辑mysql-service.xml或mysql-ds.xml,以mysql-ds.xml为例,需要增加下列语句:
<local-tx-datasource>
<jndi-name>CmpSUBDS</jndi-name>
<connection-url>jdbc:mysql://localhost:3306/cmpcustomer</connection-url>
<driver-class>com.mysql.jdbc.Driver</driver-class>
<user-name>访问数据库用户名</user-name>
<password>访问数据库密码</password>
</local-tx-datasource>
其中cmpcustomer为数据库名,而CmpSUBDS是该数据库源的JNDI名称,可以自己取名。
第二步需要让EJB代码中的CMP获取这个数据库源,那么就是将这个数据库源的JNDI名称告诉CMP。在EJB代码的配置文件中,有和具体EJB容器相关的配置文件。本项目中的jbosscmp-jdbc.xml则是关于CMP和JDBC相关的配置,在该配置文件中加入:
<defaults>
<datasource>java:/CmpSUBDS</datasource>
<datasource-mapping>MySQL</datasource-mapping>
</defaults>
java:/CmpSUBDS是数据库源CmpSUBDS的JNDI名称,前面加的java:/可以说是JBoss的约定写法。
至此,配置连接工作完成,可以将本项目部署到JBoss进行调试运行了。
5.6.2 JNDI配置
在JBoss中部署J2EE应用程序,其EJB的配置文件共有3个:ejb-jar.xml、jboss.xml和jbosscmp-jdbc.xml,这些配置是将程序代码系统和容器服务器联系在一起的纽带。
其中最关键的,也是比较复杂的是有关JNDI和EJB引用reference的对应关系设置。因为EJB是分布式的,所以并没有按照一般思维习惯直接将JNDI和EJB对应起来,而是在JNDI和EJB之间加入了一个EJB引用reference,这样整个系统做到了灵活部署,但是也增加了复杂性。
例如,在Session Bean名为CustomerManagerBean中,有对实体Bean Customer的调用,那么这种调用者EJB在一个分布式的环境中,如何寻找到被调用者EJB呢?
在CustomerManagerBean的ejbCreate()方法中,使用了下列语句:
chome = (CustomerHome) sl.getLocalHome(JNDINames.CUSTOMER_HOME);
JNDINames.CUSTOMER_HOME的值是java:comp/env/ejb/Customer,核心词是ejb/Customer,这个值是任意指定的,设置了ejb/Customer,就需要在ejb-jar.xml中将其真正导向实体Bean Customer。
ejb/Customer在EJB规定中只是一个环境变量,所以在代码中调用时需要加上java:comp/env。如果ejb/Customer是普通环境变量,只要在ejb-jar.xml加入:
<env-entry>
<description />
<env-entry-name>ejb/Customer</env-entry-name>
<env-entry-type>java.lang.String</env-entry-type>
<env-entry-value>this is Customer</env-entry-value>
</env-entry>
那么,系统在运行时,会将env-entry-value的值取出。但是在本项目中,ejb/Customer是要指向一个EJB实体Bean,而不是一个普通的环境变量,那么就不能使用env-entry,需要采取ejb-local-ref或ejb-ref,如下:
<session>
<display-name>CustomerManager</display-name>
<ejb-name>CustomerManager</ejb-name>
<local-home>com.jdon.samples.ejb.CustomerManagerLocalHome</local-home>
<local>com.jdon.samples.ejb.CustomerManagerLocal</local>
<ejb-class>com.jdon.samples.ejb.CustomerManagerBean</ejb-class>
<session-type>Stateless</session-type>
<transaction-type>Container</transaction-type>
<!-- 从这里开始定义ejb/Customer -->
<ejb-local-ref>
<description />
<ejb-ref-name>ejb/Customer</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<local-home>com.jdon.samples.ejb.CustomerHome</local-home>
<local>com.jdon.samples.ejb.CustomerLocal</local>
<ejb-link>Customer</ejb-link>
</ejb-local-ref>
</session>
在上面配置中,ejb-ref-name是JNDI调用的名称,ejb-ref-type表示被调用者的类型,Local-home和local分别是被调用实体Bean的Home接口和本地接口。
ejb-link的值是被调用EJB的名称,这个名称是使用ejb-name定义的。在本例中,ejb-link的值是Customer,那它就是实体Bean Customer中ejb-name定义的,看看Customer实体Bean的配置:
<entity>
<display-name>Customer</display-name>
<!-- 调用者ejb-link的值指向了这里 -->
<ejb-name>Customer</ejb-name>
<local-home>com.jdon.samples.ejb.CustomerHome</local-home>
<local>com.jdon.samples.ejb.CustomerLocal</local>
<ejb-class>com.jdon.samples.ejb.CustomerBean</ejb-class>
<persistence-type>Container</persistence-type>
…
</entity>
在一个大型的J2EE项目中,可能有ejb-name相同的情况发生。那么调用时,如果不使用ejb-link,那么就必须修改相同的ejb-name为惟一不同的名称,但是使用ejb-link可以避免这种修改。
EJB的部署描述器ejb-jar.xml定义了被调用者EJB引用reference的相关情况,但是没有定义JNDI。因为所有EJB定位都是通过JNDI才能寻找到,而JNDI是与具体容器产品相关的,因此EJB引用和JNDI之间的关系必须在一个与具体容器产品相关的配置文件中定义,对于JBoss,这个配置文件是jboss.xml,如下:
<session>
<ejb-name>CustomerManager</ejb-name>
<local-jndi-name>CustomerManagerLocal</local-jndi-name>
<ejb-local-ref>
<ejb-ref-name>ejb/Customer</ejb-ref-name>
<local-jndi-name>Customer</local-jndi-name>
</ejb-local-ref>
</session>
<entity>
<ejb-name>Customer</ejb-name>
<local-jndi-name>Customer</local-jndi-name>
</entity>
其中ejb-local-ref与ejb-jar.xml中ejb-local-ref是对应一致的,在jboss.xml中主要是将ejb/Customer指向实体Bean Csutomer真正的JNDI名称。在本例中Customer的JNDI名称是Customer(Customer的JNDI名称也是可以自己定义的)。
为了使得JNDI名称便于管理,在大型系统中,JNDI也是分组管理的。例如,如果这里Customer的JNDI取名为cmpSUB/Customer,表示是cmpSUB组件下的EJB,那么jboss.xml的写法如下:
<session>
<ejb-name>CustomerManager</ejb-name>
<local-jndi-name>CustomerManagerLocal</local-jndi-name>
<ejb-local-ref>
<ejb-ref-name>ejb/Customer</ejb-ref-name>
<local-jndi-name>cmpSUB/Customer</local-jndi-name> <!-- 变化点 -->
</ejb-local-ref>
</session>
<entity>
<ejb-name>Customer</ejb-name>
<!-- 定义Customer EJB新的JNDI名称 -->
<local-jndi-name>cmpSUB/Customer</local-jndi-name>
</entity>
如果这个EJB是一个BMP实体Bean Customer,那么在这里直接定义数据库连接池的JNDI。
<entity>
<ejb-name>Customer </ejb-name>
<jndi-name>cmpSUB/Customer</jndi-name>
<resource-ref>
<res-ref-name>jdbc/BmpDataSource</res-ref-name>
<jndi-name> java:/CmpSUBDS</jndi-name>
</resource-ref>
</entity>
5.6.3 部署和发布
由于本项目包括Web层和EJB层,相应的文件是.war和.jar,可以将这两个文件直接复制到JBoss/server/default/deploy下。如果把这两个文件打包成一个.ear文件,部署起来可能更方便。
通过JBuilder可以迅速建立一个EAR包,选择new →Enterprise的EAR,将需要的Web包和EJB包加入,如果需要其他EJB组件,也可以在这里一起加入。Rebuilder整个项目将生成如CmpSUB.ear这样的发布包,直接复制到JBoss/server/default/deploy下。这时会注意到JBoss控制台进行EAR发布的信息,分别部署其中的EJB和Web。
部署时会发生各种配置性问题,如connection failure等,这表示无法与数据库实现连接,那么就要检查数据库是否启动?是否授权在mysql-ds.xml设置的用户访问数据库?是否授权数据库接受JBoss所在的主机IP或域名访问等,这些环节要逐步确定。
部署发布出现的问题基本是系统级的问题,需要对操作系统、JBoss平台系统以及数据库系统等各方面有综合的掌握和了解。
如果JBoss控制台没有ERROR信息数据,最后显示Deployed package…表示本项目已经成功部署发布在JBoss服务器中了。
5.6.4 调试和测试
|
在浏览器输入http://localhost:8080/subweb/createCustomer.jsp,出现如图5-14所示的运行结果。
这时可以进入系统整合测试,整合测试相比部署时出现的问题更多。这时需要依靠Logger的日志输出,或者依赖Junit单元测试。个别按照正常逻辑不可思议发生的问题,可以使用JBuilder的断点调试功能追踪系统中实际运行的路线,可能发现一些潜在的问题。
这里有一些调试经验供参考:
如果在运行时出现某类Not Found,但是打开发布包.ear(用winrar 3.0以上版本)以及war会发现该类确实是在发布包中。这个问题是J2EE服务器处理EJB包和Web包不同顺序造成的。JBoss一般是首先部署EJB包,因此寻找某个类一般是到EJB包中寻找,如果没有发现,就报Not Found。这种情况最容易发生在Web层使用classloader技术时,因此解决办法是将该类打包在EJB包中。
有时出现一些非常奇怪的问题,如代码程序已经修改,但是运行调试时这些修改好像没有发生作用。这种情况经常发生在EJB包中,因此,必要时删除JBoss目录下的server/default/tmp目录,它是JBoss的实际工作目录。
5.7 小结
EJB的实战应用和其理论相差比较大,本章主要是通过一个小型的订阅信息系统介绍了EJB相关实战知识。
对于有丰富JavaBeans开发经验的程序员来说,JavaBeans应用基本分两大类:功能JavaBeans和数据JavaBeans。前者是实现一定功能的,有一定行为的JavaBeans;而后者则只是包含数据的、没有功能的JavaBeans。这也是通常说的Transfer Object、Value Object或者POJO(无格式普通Java 对象,英文是Plain Old Java Object)。
对于实现一定功能的JavaBeans,由于性能并发要求,需要对象池等技术来支持,还需要事务机制来保证一个完整的功能处理过程,这些技术往往是大多数应用系统都必需的,一些开源项目如OFBiz、Jive等都通过软件自身实现这些功能。如OFBiz通过UtilCache实现了数据缓存;Jive同样为提高性能,针对每个数据对象实现了缓存。
但是,在企业应用中,如果让开发者自己实现这些通用性的底层功能,势必影响开发效率,而且可能导致重用和维护等问题。特别是事务锁的设计有一定的难度,并不是所有程序员都能轻松掌握的。
从某种意义上,EJB的Session Bean可被认为是一种可以实现一定功能的JavaBeans,更主要的是它可以跨服务器运行,从而大大地提高了应用系统的处理性能;它支持事务处理机制,同时拥有自己的安全授权体系(将在后面章节“用户安全管理系统”介绍)。
当然,EJB的使用也是有其一定界限的,例如大量数据读或动态查询时,通过取消不必要的事务跟踪机制,可以大幅度提高查询性能,也可以使用DAO+JDBC直接操作数据库等方式(将在后面章节“网上商店系统”介绍)。
在实际应用中,如果将EJB的Session Bean看成是一种提供一定功能的服务(Service),那么,这将会扩大EJB的运用范围。客户端只需要通过包含数据的普通JavaBeans Object和这些Service实现互动交互。在后面章节“EJB方法调用服务框架”中将介绍这种互动机制的实现。
首页
