【IT168 技术】当前对 Web 技术的要求在不断增加。它们必须能够管理用户帐户、上传内容和流媒体格式。这个要求需要 RIA 开发人员探寻这样的技术,即能精简开发流程同时提供广受追捧的功能。开发人员面对的难题就是,如何选择合适的技术集合来提供这些服务。
Adobe Flex 是一个客户端技术,它为开发人员提供丰富的 API 集合来创建 GUI、绘制图形、播放和流放媒体、连接到 Web 服务。在服务器端,Java 技术提供的功能包括关系型数据库管理系统(RDBM)的连接、服务请求的多线程处理以及随需求增加而进行的非常好的伸缩。将这两种技术结合使用可提供一个满足 RIA 应用程序需求的强大的技术堆栈。
本文展示如何编写一个简单而强大的 RIA,能使用客户端的 Flex、服务器端 Java 技术以及后端数据库的 MySQL。
样例应用程序
样例应用程序(来自下面的 下载 部分)提供一个丰富的 UI,支持通过 Adobe Flash® (SWF) 应用程序创建、读取、更新和删除(CRUD) 联系信息。这个三层的 Web 架构如 图 1 所示,其中客户端由嵌入在一个 Web 页面中的 SWF 文件表示,服务器应用程序在一个 Java servlet 容器(本例中为 Apache Tomcat)内运行,且数据库是 MySQL。这三层共同创建一个功能分布式应用程序。
▲图1 Contacts 应用程序
对于 Flash 应用程序与 Java servlet 容器之间的通信,Adobe BlazeDS 框架提供对象远程调用 — 即一种允许 Adobe ActionScript™ 对象与 Java 对象相互调用的 PRC 形式。Java 服务器应用程序与关系数据库之间的通信由 Hibernate Object Relational Mapping (ORM) 框架处理。Hibernate 允许将 Java 对象转换为 SQL代码,反之亦然。
应用程序:服务器层
第一步是要创建一个 Java 类,它包含存储联系信息所需的信息。样例应用程序包含一个带基本信息的简单模型。Contact 对象所需的属性和数据类型是:
- String firstName
- long id
- String lastName
- String phoneNumber
- long serialVersionUID
+ Contact()
+ Contact(String first, String last, String email, String number)
+ String getEmailAddress()
+ String getFirstName()
+ long getId()
+ String getLastName()
+ String getPhoneNumber()
+ void setEmailAddress(String address)
+ void setFirstName(String first)
+ void setId(long newId)
+ void setLastName(String last)
+ void setPhoneNumber(String number)
+ String toString()
注释业务对象
Java Contact 类被看作是一个充当业务对象的 POJO(简单 Java 对象),这意味着它代表业务领域特征和行为。Contact 对象内的数据需要存留到数据库中。解决方案就是使用一个 ORM 框架,比如 Hibernate,它在对象与数据库表记录之间的来回映射中执行大量工作。如果使用了 Java Persistence API (JPA) 注释,完成 ORM 仅需要少量代码。 清单 1 显示了带注释的 Java 类 Contact。
清单 1. Java Contact 类
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
@Entity
@Table(name="contact")
@NamedQueries( {
@NamedQuery(name = "contact.findAll", query = "from Contact"),
@NamedQuery(name = "contact.getById", query =
"select c from Contact c where c.id = :id")
} )
public class Contact {
private static final long serialVersionUID = 123456789L;
public Contact() {
firstName = "N/A";
lastName = "N/A";
emailAddress = "N/A";
phoneNumber = "N/A";
}
public Contact(String first, String last, String email, String number) {
firstName = first;
lastName = last;
emailAddress = email;
phoneNumber = number;
}
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", nullable = false, updatable=false)
private long id;
@Column(name = "lastName", nullable = false, unique = false)
private String lastName;
@Column(name = "firstName", nullable = false, unique = false)
private String firstName;
@Column(name = "emailAddress", nullable = false, unique = false)
private String emailAddress;
@Column(name = "phoneNumber", nullable = false, unique = false)
private String phoneNumber;
public void setPhoneNumber(String number) { phoneNumber = number; }
public String getPhoneNumber() { return phoneNumber; }
public String getEmailAddress() { return emailAddress; }
public void setEmailAddress(String address) { emailAddress = address; }
public String getFirstName() { return firstName; }
public void setFirstName(String first) { firstName = first; }
public String getLastName() { return lastName; }
public void setLastName(String last) { lastName = last; }
public long getId() { return id; }
public void setId(long newId) { id = newId; }
@Override
public String toString() {
return id + " " + firstName + " " + lastName + " " + emailAddress
+ " " + phoneNumber;
}
}
类很简单,但对于注释则有很多内容需要解释:
@Column:将属性标记为数据库内的一个列,可含有列名,不管它是否唯一且是否可为空
@Entity:将类作为一个实体 bean 声明,表明它是一个要留存的 POJO
@GeneratedValue:指定生成主键的策略;包括 AUTO、IDENTITY、SEQUENCE 和 TABLE
@Id:指明属性为每个 Java 对象的唯一标识符(即主键)
@NamedQueries:列出一组命名查询
@NamedQuery:将预定义查询声明为一个字符串常量,以供执行时引用。
@Table:将 Java 类指定为数据库内的一个表
每次需要留存一个内存中的 Java 对象时,Hibernate 将任何 Java 对象的状态信息转换为一个 SQL 更新。同样地,带结果集的 SQL 语句用于填充 Java 对象。因此,可将所有对象保存为数据库内的记录,且可检索所有记录并将其转换回 Java 对象。
注释告知 Hibernate 一个类中的哪些内容可以考虑留存。但它们只是类的一部分。
业务服务:数据库连接
执行 ORM 时需要有一个服务类来执行对 Hibernate 的调用。清单 2 显示了 ContactsService 类,它充当应用程序服务。
清单 2. ContactsService 类
private static Logger logger = Logger.getLogger(ContactsService.class);
private static final String PERSISTENCE_UNIT = "contacts";
private static EntityManagerFactory emf = null;
static {
logger.info("LOADING CONTACTSSERVICE CLASS.");
emf = Persistence.createEntityManagerFactory(PERSISTENCE_UNIT);
}
public ContactsService() {
super();
}
public void addContact(Contact c) {
if(c == null) {
return;
}
EntityManager em = emf.createEntityManager();
logger.info("PERSISTENCE ENTITYMANAGER ACQUIRED.");
logger.info("ABOUT TO ADD CONTACT: fName: " + c.getFirstName()
+ ", lName: " + c.getLastName() + ", email:" + c.getEmailAddress()
+ ", phone: " + c.getPhoneNumber());
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
em.merge(c);
tx.commit();
} catch (Exception e) {
logger.error("CONTACT APP PERSISTING ERROR: " + e.getMessage());
tx.rollback();
} finally {
logger.info("CONTACT APP CLOSING ENTITY MANAGER.");
em.close();
}
}
public void editContact(Contact c) {
logger.info("CONTACT TO UPDATE: " + c);
addContact(c);
}
public void deleteContact(Long id) {
logger.info("ABOUT TO DELETE CONTACT");
EntityManager em = emf.createEntityManager();
logger.info("PERSISTENCE ENTITYMANAGER ACQUIRED.");
Query contactByIdQuery = em.createNamedQuery("contact.getById");
contactByIdQuery.setParameter("id", id);
Contact c = (Contact) contactByIdQuery.getSingleResult();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
em.remove(c);
tx.commit();
} catch (Exception e) {
logger.error("CONTACT APP PERSISTING ERROR: " + e.getMessage());
tx.rollback();
} finally {
logger.info("CONTACT APP CLOSING ENTITY MANAGER.");
em.close();
}
}
public List<Contact> getContacts() {
logger.info("ABOUT TO RETRIEVE CONTACTS");
EntityManager em = emf.createEntityManager();
logger.info("PERSISTENCE ENTITYMANAGER ACQUIRED.");
Query findAllContactsQuery =
em.createNamedQuery("contact.findAll");
List<Contact> contacts = findAllContactsQuery.getResultList();
if (contacts != null) {
logger.debug("CONTACT APP RETRIEVED: " + contacts.size()
+ " CONTACT(S)");
}
return contacts;
}
}
每个方法都获取对 EntityManager 的一个引用,它提供对内存缓存的支持。缓存是一个能改进效率的强大特性,因为从一个数据库收发数据是一项很昂贵的操作。您必须确保所创建的每个缓存都服务于数据库,或在不需要时予以回滚。
在 JPA 中,缓存的既定术语是持久化上下文(persistence context),且它由 EntityManager 类表示。每个持久化上下文管理一组实体,这些实体是标有 @Entity 注释的 Java 对象。EntityManagerFactory 类表示一个 持久化单元(persistence unit),它负责配置到数据存储(例如,关系数据库)的连接、管理实体类型(即给定上下文内需要映射到数据存储的所有类),并最后提供持久化上下文的实例(即一个 EntityManager)。
试用 DB2 Express 9 数据库服务器免费版DB2 Express-C 被设计为可在几分钟内启动并运行,它易于使用和嵌入,包含自管理功能,且嵌入了 DB2 for Linux®,UNIX® and Windows® 的所有核心功能,比如 pureXML™。DB2 Express-C 像其他 DB2 Express 版本一样提供相同的核心数据服务器基础功能,且提供一个坚实的基础来构建和部署使用 C/C++、Java、.NET®、PHP、Ruby on Rails、Python 和其他编程语言开发的应用程序。
尽管创建一个持久化上下文的过程很省时,但创建一个持久化单元的过程却很费时。建立到数据存储的连接、查找标注为实体的所有类、配置持久化逻辑以将这些类绑定到数据存储中的实体,整个过程不可能快速完成。因此,您需要在应用程序启动时创建一个 EntityManagerFactory 实例。对于持久化上下文,要务必确保在销毁掉一个 EntityManager 之后再创建另一个。另一个要遵循的重要规则就是 entitymanager-per-request 模式。该模式将数据库调用(例如,请求和更新)组合起来,这样就可以将它们一次性发送出去。这样做可以确保充分利用 JPA 的缓存机制。
下一个需求就是客户端。
应用程序:客户端层
Flex 框架允许您创建可在 Adobe Flash Player 中播放的应用程序。Flex 包括:
称为 MXML 的一个声明性 XML UI 语言
ActionScript 编程语言
用于创建 UI、Web 连接和许多其他特性的运行时库
用于将应用程序编译为 SWF 文件的开发工具
本文引用的客户端应用程序使用 Flex 第 4 版。在探讨客户端应用程序之前,要了解如何创建 Flex 应用程序,以及它们如何在 Flash Player 中作为可执行程序存在,这很重要。
首先,您可以使用 MXML 标记和 ActionScript 代码创建应用程序。常用的工作流是使用 MXML 格式创建 GUI 的主要部分(呈现),然后使用 ActionScript 代码执行事件处理和业务逻辑。由于 MXML 和 ActionScript 都是基于文本的,创建 Flash 应用程序只需要一个标准文本编辑器和 Flex SDK。
其次,编写完 Flex 应用程序之后,使用 MXML 编译器编译代码。然后 MXML 编译器创建可在 Web 浏览器内运行(通过 Flash Player 浏览器插件)的 SWF 文件。
最后,Flash 应用程序在使用时间轴范例的 ActionScript Virtual Machine 2 (AVM2) 中运行。该范例将执行动作分成帧 — 就像电影一样。您在编译时指定 Flash 应用程序中的每秒帧数。此外,Flash Player 将执行动作分成以下已排序的任务:
Flash Player 事件,比如计时器和鼠标事件
用户代码
预渲染逻辑,其中 Flash Player 试图确定是否因数据值变更而更新 GUI
与数据值变更相关的用户代码
Flash Player 渲染
如果要渲染的每秒帧数很少,那么就可以执行大部分用户代码。但是,如果帧频很高(例如,每秒 60 帧),Flash Player 就不太可能执行多数用户代码,因为用户代码执行的时间可能比给定时间更长。在为 Flash Player 编写代码时,记住这一点很重要。
MXML
MXML 是一个强大的声明性 XML 格式,有助于:
因 XML 格式的声明性质而最大限度地降低构建 GUI 所需的代码量
通过明确分离表示逻辑和交互逻辑降低 GUI 代码的复杂度
在进行软件开发时推进设计模式的使用
清单 3 显示了 MXML Application 类。
清单 3. ContactsApp 类
xmlns:contact="bcit.contacts.*" creationComplete="initPage();"
layout="vertical" frameRate="30" pageTitle="Contacts Example"
horizontalAlign="center" verticalAlign="middle"
backgroundColor="#A9C0E7">
<mx:Style>
.mainBoxStyle {
borderStyle: solid;
paddingTop: 5px;
paddingBottom: 5px;
paddingLeft: 5px;
paddingRight: 5px;
}
.textMessages {
fontWeight: bold;
}
</mx:Style>
<mx:RemoteObject id="remotingService" showBusyCursor="false"
destination="contacts" fault="handleFault(event);"
result="handleResult(event);"/>
<mx:Script>
<![CDATA[
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.collections.ArrayCollection;
import bcit.contacts.dto.Contact;
[Bindable]
private var contacts:ArrayCollection = new ArrayCollection();
// For more on the Bindable metadata tag, see the devguide_flex3.pdf
// document, page 1249 (1257 in PDF page numbering)
[Bindable]
private var message:String = "Status: Ready";
private var contact:Contact;
public function setControlBarValid(valid:Boolean):void {
if(valid) {
// if the selected item is -1, then no item is selected but at
// the same time the fields are valid which means the user chose
// to add a contact, not update one
if(contactsDataGrid.selectedIndex == -1) {
createButton.enabled = valid;
} else {
editButton.enabled = valid;
}
} else {
// else nothing is valid
createButton.enabled = false;
editButton.enabled = false;
}
}
private function initPage():void {
editContactForm.setApp(this);
contact = new Contact();
getAllContacts();
resetPage();
}
private function createContact():void {
contact = editContactForm.getContact();
remotingService.addContact(contact);
message = "Status: Contact Added";
getAllContacts();
}
private function editContact():void {
var id:Number = contact.id;
contact = editContactForm.getContact();
contact.id = id;
remotingService.editContact(contact);
message = "Status: Contact Edited";
getAllContacts();
}
private function deleteContact():void {
if(contactsDataGrid.selectedItem != null) {
var c:Contact = contactsDataGrid.selectedItem as Contact;
// no sense in sending the whole contact - just send the id
// to cut down on bandwidth
remotingService.deleteContact(c.id);
message = "Status: Contact Deleted";
}
getAllContacts();
}
private function getAllContacts():void {
loadButton.enabled = false;
remotingService.getContacts();
loadButton.enabled = true;
resetPage();
}
private function populateFormWithContact():void {
contact = contactsDataGrid.selectedItem as Contact;
editContactForm.setContact(contact);
editButton.enabled = true;
deleteButton.enabled = true;
}
private function resetPage():void {
editContactForm.clearForm();
contact = new Contact();
createButton.enabled = false;
editButton.enabled = false;
deleteButton.enabled = false;
contactsDataGrid.selectedIndex = -1;
}
private function handleFault(e:FaultEvent):void {
message = "Status: Error"
+ "\nFault code: " + e.fault.faultCode
+ "\nFault detail: " + e.fault.faultDetail
+ "\nFault string: " + e.fault.faultString;
}
private function handleResult(e:ResultEvent):void {
// can get the results by accessing e.result property
//mx.controls.Alert.show(e.toString());
contacts = e.result as ArrayCollection;
var number:int = contacts.length;
//if(number == 1) {
// message = "Status: Retrieved 1 contact";
//} else {
// message = "Status: Retrieved " + contacts.length + " contacts";
//}
}
]]>
</mx:Script>
<mx:VBox styleName="mainBoxStyle">
<mx:Text id="titleText" text="Single click to select a contact"/>
<contact:ContactsDataGrid id="contactsDataGrid" dataProvider="{contacts}"
itemClick="populateFormWithContact();"
doubleClick="populateFormWithContact();"/>
<contact:EditContactForm id="editContactForm"/>
<mx:ControlBar horizontalAlign="center">
<mx:Button label="List" id="loadButton" click="getAllContacts()"
toolTip="Retrieve contacts from the server"/>
<mx:Button label="Add" id="createButton" click="createContact()"
toolTip="Create a new contact"/>
<mx:Button label="Update" id="editButton" click="editContact()"
toolTip="Edit a selected contact"/>
<mx:Button label="Delete" id="deleteButton" click="deleteContact()"
toolTip="Delete a selected contact"/>
<mx:Button label="Clear Form" id="clearButton" click="resetPage()"
toolTip="Clear the form"/>
</mx:ControlBar>
<mx:TextArea text="{message}" styleName="textMessages" wordWrap="true"
verticalScrollPolicy="auto" horizontalScrollPolicy="off" editable="false"
width="100%"/>
</mx:VBox>
</mx:Application>
关于清单 3,这里还有几点说明:
一个 MXML 文档的根元素是 Application 类的一个子类。
mx:Style 元素允许 CSS 属性定义 UI 组件的本地样式。样式设计通过使用本地样式定义(见 清单 3)、对外部样式表的引用、组件内的内联样式以及 ActionScript 中的 setStyle 方法完成。
RemoteObject 类表示与服务器执行远程操作的一个 HTTP 服务对象。
mx:Script 元素包括 CDATA 区域中的 ActionScript 代码块。
有一个布局(即 VBox 类)。
每次在应用程序中声明一个 UI 组件(例如,TextArea)时,就产生一个实例变量,稍后可使用组件的 id 属性在应用程序内引用该变量。
数据绑定通过大括号执行(例如, 将 TextArea 元素的 text 属性绑定到 ActionScript message 实例变量)。
ActionScript
MXML 定义 GUI,而 ActionScript 提供用于处理事件、绑定数据(通过 [Bindable] 元数据标记)的行为,以及调用远程服务的能力。在 清单 3 中,createContact、editContact、deleteContact 和 getAllContacts 方法都在服务器端调用远程方法。调用远程方法时,ActionScript 就有机会通过声明回调函数处理结果和任何错误了。在 清单 3 中,handleResult 函数将结果作为一个 Object 接收,并将其投射到一个 ArrayCollection。BlazeDS 将 List 转化为客户端上的一个 ArrayCollection。
清单 4 展示了 ActionScript 类 Contact,创建该类的目的在于表示 Flash 端的联系对象。
清单 4. ActionScript Contact 类
[RemoteClass(alias="bcit.contacts.Contact")]
public class Contact {
public function Contact() { id = -1; }
public var id:Number;
public var lastName:String;
public var firstName:String;
public var emailAddress:String;
public var phoneNumber:String;
public function toString():String {
return id + ", " + firstName + " " + lastName + " " + emailAddress
+ " " + phoneNumber;
}
}
}
这些 ActionScript 对象被发送到服务器端,BlazeDS 在此发挥其魔力并将 ActionScript 对象转化为 Java 对象。ActionScript Contact 类被看作一个 Data Transfer Object (DTO)。
配置应用程序
应用程序也依赖于为服务器声明设置规范的配置文件。这个应用程序内的两个主要配置对象是 Hibernate 和 BlazeDS。
配置 Hibernate
您可以使用标准 JPA 配置文件 persistence.xml 配置 Hibernate,如 清单 5 所示。
清单 5. persistence.xml 配置文件的一个子集
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
<persistence-unit name="contacts" transaction-type="RESOURCE_LOCAL">
<properties>
<property name="hibernate.dialect"
value="org.hibernate.dialect.MySQLDialect" />
<property name="hibernate.default_schema" value="contacts" />
<property name="hibernate.connection.driver_class"
value="com.mysql.jdbc.Driver" />
<property name="hibernate.connection.url"
value="jdbc:mysql://localhost:3306/contacts" />
<property name="hibernate.archive.autodetection" value="class, hbm"/>
<property name="hibernate.connection.username" value="root"/>
<property name="hibernate.connection.password" value="root"/>
</properties>
</persistence-unit>
</persistence>
persistence.xml 文件必须位于 Web 应用程序的 WEB-INF/classes/META-INF 文件夹中,Hibernate 才能读取到它。具备了这个条件之后,Hibernate 需要以下信息:
数据库方言(即它与哪个数据库对话,因为很多数据库都有略微不同的 SQL 方言)
经由默认架构的表空间
用于连接数据库的数据库驱动器
数据库 URL
自动检测功能应检测什么(例如,注释类、Hibernate 映射 XML 文件等)
用户名和密码
其他信息也有助于提高 Hibernate 的性能,但不是必需的。
配置 BlazeDS
BlazeDS 有 4 个配置文件:
messaging-config.xml:定义发布-订阅消息传送信息
proxy-config.xml:为 HTTP 和 Web 服务提供代理服务信息
remoting-config.xml:定义用于远程服务的信息,比如本文应用程序的配置文件
services-config.xml:引用其他配置文件且提供安全约束、通道和日志记录的优异配置文件
清单 6 展示了 services-config.xml 文件。注意,对于本文应用程序,只有 remoting-config.xml 文件是相关的,因为应用程序仅使用了 BlazeDS 远程服务。
清单 6. services-config.xml 配置文件的子集
<services-config>
<services>
<service-include file-path="remoting-config.xml" />
<service-include file-path="messaging-config.xml" />
<service-include file-path="proxy-config.xml" />
<default-channels>
<channel ref="contacts-amf"/>
</default-channels>
</services>
<channels>
<channel-definition id="contacts-amf" class="mx.messaging.channels.AMFChannel">
<endpoint url="http://localhost:8080/contacts/messagebroker/amf"
class="flex.messaging.endpoints.AMFEndpoint"/>
<properties>
<polling-enabled>false</polling-enabled>
</properties>
</channel-definition>
</channels>
<logging>
<target class="flex.messaging.log.ConsoleTarget" level="Error">
<properties>
<prefix>[BlazeDS] </prefix>
<includeDate>false</includeDate>
<includeTime>false</includeTime>
<includeLevel>false</includeLevel>
<includeCategory>false</includeCategory>
</properties>
<filters>
<pattern>Endpoint.*</pattern>
<pattern>Service.*</pattern>
<pattern>Configuration</pattern>
</filters>
</target>
</logging>
</services-config>
services-config.xml 配置文件引用其他配置文件(如果存在)、配置 BlazeDS 日志记录并建立任何通道。一个通道 是对协议的一个抽象,供客户端与服务器通信时使用。本文应用程序使用没有轮询的标准 AMF 协议。轮询 是指客户端持续与服务器通信,以确保连接始终成立 — 不过在本应用程序中不需要。
通道端点指定服务器 URL。该端点是编译项目所必需的;客户端 Flash 应用程序将其作为一个硬编码值使用,因此它知道要连接到哪个服务器上。您实际上可以在 MXML 或 ActionScript 中定义端点 URL。
最后,remoting-config.xml 配置文件(如 清单 7 所示)指定适配器类,用于处理远程操作和响应远程调用的实际类。(本例中是将 bcit.contacts.ContactsService 类作为对远程请求的响应者提供。)
清单 7. remoting-config.xml 配置文件的子集
<service id="remoting-service"
class="flex.messaging.services.RemotingService">
<adapters>
<adapter-definition id="java-object" default="true"
class="flex.messaging.services.remoting.adapters.JavaAdapter"/>
</adapters>
<default-channels>
<channel ref="contacts-amf"/>
</default-channels>
<destination id="contacts">
<properties>
<source>bcit.contacts.ContactsService</source>
<!--<scope>application</scope>-->
</properties>
</destination>
</service>
结束语
本文向您展示了如何编写一个运行在 Tomcat 内,并响应联系信息请求的 Java 服务器端 Web 应用程序。您学习了如何同时使用 MXML 和 ActionScript 编写一个 Flex 应用程序,以创建一个客户端 Flash 应用程序。MySQL 充当数据存储,而 Hibernate — 一个 ORM 框架 — 用于将 Java 对象转换成能查询和更新 MySQL 数据库的 SQL 语句。最后,BlazeDS 框架允许 Flash 应用程序进行远程过程调用并在 Java 服务器端 Web 应用程序执行远程调用。