写点什么

使用 Grails 和 Flex 开发 JEE 应用

2009 年 2 月 01 日

Java 平台已经逐渐发展为一个成熟可靠的企业应用平台,成熟的应用平台的一个标志则是它能够带动大量的衍生技术以及可以与其他技术集成的选项。本文将详细讲述怎样用 Grails 这项传统 JEE 应用开发的衍生技术,结合另一项完全不同但却可以在 Java 中使用的 Flex 技术来开发 JEE。这两个平台都能大幅度提高开发效率。两者相结合则在为 J2EE 应用创建富客户端的同时不影响整体的开发效率。

Grails 的前身是一个在 JVM 中运行的 web 应用,它使用 Groovy 以及其它几个著名的框架,比如 Spring 和 Hibernate。为了实现快速应用开发,它极为依赖“Convention over Configuration”原则。Groovy 拥有很多动态的特性,在定义组件间共同行为方面,功能非常强大。Grails 采用 plug-in 构架,因此很容易把它与其他框架集成,而且也很容易在应用间复用各自的功能。

Flex 是个 RIA 开发套件,由它创建的 SWF 应用只能在 FlashPlayer 下应用。这是 Adobe(前身为 MacroMedia)的一个新型 Flash 开发套件。除了拥有丰富的 widget 和把各种 widget 粘合在一起的强大的语言之外,它还能提供一些高端通信解决方案,分布式应用程序的开发因此变得相当容易。它使用两种语法:MXML 和 ActionScript。MXML 创建在 XML 语法之上,专门用来定义通用组件的用户接口;而 ActionScript 则用来定义组件之间的动态交互。

Grails 和 Flex 的集成——难题所在

要把 Grails 和 Flex 这两个建立在完全不同基础上的框架结合起来,首先会遇到诸多通信方面的问题:

  1. 一个框架中的组件如何才能在另一个框架中找到正确的通信对象?
    从本质上来说,Grails 实际是运行在服务器的 JVM 上的一个 web 应用框架。Flex 则是拥有客户端和(瘦)服务器组件的 RIA 平台,服务器组件以 web 应用的方式部署。因此,这两个框架之间的集成实际上在 web 应用容器内进行。
    用户在 Flex UI 发起的通信必须通过 Grails 组件来调用业务逻辑。那么,Flex UI 组件该如何找到正确的 Grails 组件呢?
  2. 框架间如何解析彼此的数据?
    Flex 采用 ActionScript 来描述数据,而 Grails 则采用 Java 和 Groovy 对象。Flex UI 向服务器发送的 ActionScript 对象应该被转述为应用程序能够理解的数据结构。这该如何实现?
  3. 某个用户的修改该如何与该应用程序的其他用户交互?
    这是多用户应用程序普遍存在的问题,但同时运用两个不同的框架使得问题更加复杂。难点在于 Grails 应用程序,用户通过 Flex UI 来启动这个应用,但如何通过 Flex UI 与其他用户通信,让他们知道该用户的这一动作呢?

在接下来的三个部分中,我们详细讨论上文提到的三个问题,寻找采用 Grails 和 Flex 的解决方案。

集成——寻找消息接收对象

一个框架中的组件如何才能在另一个框架中找到正确的通信对象呢?

具体到 Grails 和 Flex 的话,这个问题其实就是在问 Flex 组件怎样才能找到正确的 Grails 组件,进而发送请求数据,或者以用户的名义执行一些操作。为了更好的理解解决这个难点的方法,我们首先来了解一下 Flex 的通信子系统。

Flex 中的客户——服务器通信

Flex 的通信子系统可以分为客户和服务器两个部分。客户部分包含了那些允许应用程序发送或者接受消息的组件,比如 RemoteObject 和 Consumer 组件。这些组件与服务器部分特定的“服务”对象相关联,比如 RemotingService 和 MessagingService。客户组件及其相关联的服务器组件的结合能够支持典型的通信模式。比方说结合 Consumer、Producers 和 MessagingService,应用软件就能够使用 Publish-Subscribe 机制来通信。

客户和服务器件的通信通过信道(Channel)来完成。信道的实现方式并不唯一,所有信道中最重要的是 AMFChannel 和 RTMPChannel。 AMFChannel 建立在 HTTP 基础上,也就是说建立在请求-回复的构架上。它可以和 MessagingService 同时使用,从而支持 Publish-Subscribe 构架。这种结合下,信道定期从发布中读取新的消息,生成请求。RTMPChannel 在这样的配置下效率更高,它能够在 TCP/IP 的基础上支持客户与服务器间的连接。这样一来,客户与服务器之间能够立即发送或接受消息。遗憾的是,Adobe 免费开源的 Flex 实现—— BlazeDS 不包含这样的 RTMPChannel 实现。

Flex 中最重要的通信基础设施是 Destinations。Destination 是通信信道的服务器端终点。一个服务提供一个 Destination,而客户组件则通过这个 Destination 与这个服务相关联。关联的客户组件可以向 Destination 发送和读取消息。 Destinations 可以由 Factories 创建。

Grails 暴露的远程接口:服务

如何把 Flex 复杂的通信设施和 Grails 结合起来呢?Grails 能够识别几类对象:域对象、控制器、视图和服务。Grails 中的每个服务都是通过外部通信信道——比如 HTTP——展示某些功能或者服务的一个对象。而在 Flex 中,每个服务则与一个 Destination 相对应。

这恰恰就是针对 Grails 的 flex-plugin 所提供的解决方案。Grails 中所有注明向 Flex 展示的服务都将在 Flex 框架中以 Destination 的形式注册。Grails 通过一个特定的 Factory 把注明的服务添加到 Flex 中特别配置的 RemotingService。这个特定的 Factory 会在 Grails 使用的 Spring 上下文中定位到对应的服务。所有这些配置都可以在 services-config.xml 中找到,flex-plugin 会为 Grails 将这个文件复制到正确的地方。

复制代码
<b><span color="#0099ff">class</span></b> UserService {
<b><span color="#006699">static</span></b> expose = [<span color="#ff00cc">'flex-remoting'</span>]
<b><span color="#008000">def</span></b> List all() {
User.createCriteria().listDistinct {}
}
<b><span color="#008000">def</span></b> Object get(id) {
User.get(id);
}
<b><span color="#008000">def</span></b> List update(User entity) <b><span color="#006699">throws</span></b> BindException {
entity.merge();
<b><span color="#006699">if</span></b> (entity.errors.hasErrors()) {
<b><span color="#006699">throw new</span></b> BindException(entity.errors);
}
all();
}
<b><span color="#008000">def</span></b> List remove(User entity) {
entity.delete();
all();
}
}

这段配置将 UserService 展示给 flex 客户。下面这段 MXML 代码则是对前面这段配置的应用。RemoteObject 的 destination 是 userService,这个 userService 正是 Grails 中目标对象提供的服务名。服务对象的所有方法这下都可以作为远程操作调用。ActionScript 可以将这些操作像一般的方法那样调用,而方法调用的结果或错误也可以当作一般的 ActionScript 事件来处理。

复制代码
...
<span color="#006699"><mx:RemoteObject</span> id="<span color="#ff0000">service</span>" destination="<span color="#ff0000">userService</span>">
<span color="#006699"><mx:operation</span> name="<span color="#ff0000">all</span>" result="<span color="#ff0000">setList(event.message.body)</span>"/>
<span color="#006699"><mx:operation</span> name="<span color="#ff0000">get</span>" result="<span color="#ff0000">setSelected(event.message.body)</span>"/>
<span color="#006699"><mx:operation</span> name="<span color="#ff0000">update</span>"/>
<span color="#006699"><mx:operation</span> name="<span color="#ff0000">remove</span>"/>
<span color="#006699"></mx:RemoteObject</span>>
...

结论

flex-plugin 为 Grails 提供的针对集成的解决方案非常漂亮,易于使用而且几乎是自动化的。在 Convention-over-Configuration 概念下,Destinations 动态添加到 Flex 配置的时候使用命名规范。

数据转换

框架间如何互相转换对方的数据(本文中就是 Java 和 ActionScript 对象转换的问题)?

这个问题的关键之处在于两框架相交接的地方。Flex 包含 Java(web 服务器)和 ActionScript(客户端)两个组件。因此,Grails 和 Flex 之间的边界就在 web 服务器,而这个服务器在两个框架内实际上都是 Java 应用。

Flex 的 Java 组件只关注于与 Flex 客户间的通信。基于 ActionScript 对象的 AMF 协议就用于这样的数据通信。服务器内部的 Java 代码将数据转换成 ActionScript 对象,这些对象在信道上实现系列化。Flex 支持 Java 的基本类型,也支持其标准复杂类型(比如 Date 或者 Collection 类型)。由于 ActionScript 是门动态语言,因此它也支持随机对象结构。Java 对象域会转换成 ActionScript 对象的动态属性。但把这些非类型 ActionScript 对象转换成 Groovy 域对象的过程则没那么直接,它会默认生成一个 Map,将属性以 key- Value 对的形式存储到这个 Map 中。

Flex 创建与 Groovy 域对象拥有同样属性的 ActionScript 类,通过注解将两者互相关联起来,这样一来,数据转换更加方便。下面的这个例子就是这样一对关联的 Groovy-ActionScript。

Groovy ActionScript ```
class User implements Serializable {
String username
String password
String displayName
}

复制代码

[RemoteClass(alias=“User”)]
public class User {
public var id:*
public var version:*
public var username:String;
public var password:String = “”;
public var displayName:String;

public function toString():String {
return displayName;
}
}

复制代码
注解“RemoteClass”将 ActionScript 类链接到由 alias 属性指明的 Java(或 Groovy) 类。alias 这个属性应该包含完整的类名。Grails 中的领域类通常都添加到默认的类包。Grails 类中的所有属性都会复制到 ActionScript 类。这些属性的名字都应当完全一样。Grails 会为所有需要“id”和“version”的领域对象动态添加这两个属性,领域对象因此可以在与客户端交互的时候保留这两个信息。
### 结论
Flex 提供的与 Java(或 Groovy)间数据转换的解决方案会导致很多重复的代码。每个领域类都会被定义两次,一次用 Groovy(或 Java)定义,另一次用 ActionScript。但是这样一来,可以添加一些客户端特定代码,比如说那些单单用 ActionScript 编写的控制对象显示的代码。这也推动编辑器同时提供两种代码的自动完成功能。至于用于配置的注解则非常简便。
## 多用户
** 应用程序如何将某个用户所作的修改通知到其他用户?**
对于一个能同时支持多用户的应用程序来说,将某个用户对共享数据所做的修改通知到其他用户着实是个挑战。对于其他用户来说,这个过程可以看作是有服务器发起的通信。
单个中央结点(通常指服务器)向很多接收点(通常指客户)发起通信的时候,发布-注册(publish-subscribe)就非常实用。客户在服务器上注册之后,服务器上任何相关消息的发布,他们都会收到通知。
由于 Grails 可以使用 Java,自然就可以用到 JMS。JMS 是应用程序间通信的 Java 标准,它支持 publish-subscribe 技术,而且应用程序也可以通过适配器来集成 JMS。
### Grails 中的 JMS 配置
在众多标准中,有一个特别针对 Grails 的 [jms-plugin](http://grails.org/Jms+Plugin),它添加了很多有用的方法可以用来向 JMS 目的地对象、向所有的控制器和服务类发送消息。在上一章中提到的 UserService 就可以运用这些方法在数据发生变化时通过 JMS 向所有的用户发送更新消息。

class UserService {

def List update(User entity) throws BindException {
entity.merge(flush:true );
if (entity.errors.hasErrors()) {
throw new BindException(entity.errors)
}
sendUpdate();
all();
}
def List remove(User entity) {
entity.delete(flush:true );
sendUpdate();
all();
}
private def void sendUpdate() {
try {
sendPubSubJMSMessage(“tpc”,all(),[type:“User”]);
} catch (Exception e) {
log.error(“Sending updates failed.”, e);
}
}
}

复制代码
服务可以决定什么时候发送什么样的消息。无论用户什么时候更新或删除数据,都会发送一条包含了完整的数据列表的消息。这条消息会发送到特定话题,也就是这里的“tpc”。任何注册了这个话题的用户都将接收到新数据。列表中的对象类型(本例中也就是“User”)作为元数据添加到消息中,接收对象因此在服务器上注册的时候特别指明他们所关注的数据类型。
为了让 Grails 应用也能够采用 JMS,每个 JMS 都需要实现 provider。Apache 有个免费的开源实现,只需简单配置就能在 Grails 应用程序中使用。你所需要做的是把 ApacheMQ 类库添加到 Grails 应用的 lib 文件夹下,再将下列代码段复制到 connection factory 所使用的 conf/spring 文件夹下的 resources.xml 中。


<bean id="connectionFactory"

class="org.apache.activemq.pool.PooledConnectionFactory"

destroy-method=“stop”>

<property name=“connectionFactory”>

<bean class=“org.apache.activemq.ActiveMQConnectionFactory”>

<property name=“brokerURL” value=“vm://localhost”
/>



复制代码
### 在 Flex 中接收 JMS 消息
目前的 Flex 配置仅仅包含一个 RemotingService,依靠它来支持 request-response 类型的用户与 UserService 间交互。这个服务由 flex-plugin 向 Grails 中添加。除此之外,我们还需要一个 MessagingService 来支持 publish- subscribe 类型的交互。


<service id=“message-service” class=“flex.messaging.services.MessageService” messageTypes=“flex.messaging.messages.AsyncMessage”>



idclassdefaultid




javax.jms.ObjectMessage
ConnectionFactory
tpc
NON_PERSISTENT
DEFAULT_PRIORITY
AUTO_ACKNOWLEDGE
false


Context.PROVIDER_URL
vm://localhost


Context.INITIAL_CONTEXT_FACTORY
org.apache.activemq.jndi.ActiveMQInitialContextFactory


topic.tpc
tpc









复制代码
在 services-config.xml 文件中,我们需要添加下列这段包含了一个新的 MessagingService 和 JMSAdapter 的代码段。添加的这个适配器将服务中的 destination 链接到 JMS 资源。这个服务中还包含一个 destination 的配置,flex 代码中的用户可以通过注册获得这个 destination 的数据更新。Destination 中含有很多 JMS 特定的配置。大部分都是常用的 JMS 属性。initial- context-environment 中的“topic.tpc”属性是个可定制的 ActiveMQ 属性,这个属性将在上下文中注册一个 JNDI 名为 “tpc”的话题。


<mx:Consumer destination=“tpc” selector=“type = ‘User’
message=“setList(event.message.body)”/>

复制代码
Flex 客户端代码非常简单。消息根据选择器(selector)被发送到特定的 destination,而 Consumer 组件因此接受到所对应的 destination 中的消息。在这个例子中,我们通过筛选器指定 Consumer 所关注的消息是元数据“type”属性值为“User”的内容。无论消息是何时收到的,消息的内容,也就是 User-objects 列表会被置为可显示的内部列表。消息内容的处理和 RemoteObject 上“all”处理的返回值完全一样。
### 结论
Grails 和 Flex 中将数据变化传递给多用户的解决方案完全可以通过标准组件来实现。涉及到的组件数量很多,配置和实现因此相当复杂。如果配置正确的话,这个解决方案使用起来就非常方便。
## 合并解决方案
回顾前三章提出的解决方案,你会发现还可以把他们合并起来得到一个更为通用的解决方案来实现 Flex/Grails 应用程序客户与服务器间的关于领域状态信息的通信。本章节中,我们要讨论的就是这样一个更为通用的解决方案。
### 泛化服务器端代码
问题 1 和 3 的解决方案所需要的服务器端的代码可以合并到同一个 Groovy 服务中。我们将把它指明为针对 User 领域类的服务。通过 Groovy 这样一门动态语言,要把这样一个服务泛化到面向所有领域类的操作非常容易。

import org.codehaus.groovy.grails.commons.ApplicationHolder

class CrudService {
static expose = [‘flex-remoting’]

def List all(String domainType) {
clazz(domainType).createCriteria().listDistinct {}
}

def Object get(String domainType, id) {
clazz(domainType).get(id)
}

def List update(String domainType, Object entity)
throws BindException {
entity.merge(deepValidate:false, flush:true)
if (entity.errors.hasErrors()) {
throw new BindException(entity.errors)
}
sendUpdate(domainType);
all(domainType);
}

def List remove(String domainType, Object entity) {
entity.delete(flush:true);
sendUpdate(domainType);
all(domainType);

}
private def Class clazz(className) {
return ApplicationHolder.application.getClassForName(className);
}

private def void sendUpdate(String domainType) {
try {
sendPubSubJMSMessage(“tpc”, all(domainType), [type:domainType]);
} catch (Exception e) {
log.error(“Sending updates failed.”, e);
}
}
}

复制代码
要实现这个目的的关键在于让客户来决定返回的领域类型。出于这个目的,我们需要为所有服务引入一个参数,通过这个参数为服务器鉴定各个领域类型。很明显,对于这个参数来说,领域类型的类名是无非是最好的选择。为所有领域对象提供 C(reate)R(etrieve)U(pdate)D(elete) 操作的服务被称为 CrudService。
一旦有任何数据更改,CrudService 都会向 JMS 话题发送更新消息。这个更新消息包含了应用程序所知道的完整的领域对象列表。为了让用户来决定这是否是自己心仪的更新内容,领域类型的类名将以元数据方式添加到消息中。
### 客户端代码
解决方案 13 中的客户端 ActionScript 代码也可以综合到同一个类中。这个类的实例可以用来管理客户端某个特定领域类型的所有实例集。

public class DomainInstancesManager
{
private var domainType : String;
public function EntityManager(domainType : String, destination : String) {
this.domainType = domainType;
initializeRemoteObject();
initializeConsumer(destination);
}

private var _list : ArrayCollection = new ArrayCollection();
public function get list () : ArrayCollection {
return _list;
}
private function setList(list : *) : void {
_list.removeAll();
for each (var o : * in list) {
_list.addItem(o);
}
}

internal static function defaultFault(error : FaultEvent) : void {
Alert.show("Error while communicating with server: " + error.fault.faultString);
}

}

复制代码
实现客户端的 ActionScript 基本上包含两个组件:简化 request-response 对话的 RemoteObject 和用于 producer-subscriber 对话的 Consumer 组件。上一章节中,这些对象通过 MXML 代码实现初始化,但他们也可以通过 ActionScript 来创建。上面这段代码段显示的是这两个组件共同使用的结构:包含了实例和错误处理的列表。包含实例的列表根据任何一个通信组件发送的消息而更新。


private var consumer : Consumer;
private function initializeConsumer(destination : String) : void {
this.consumer = new Consumer();
this.consumer.destination = destination;
this.consumer.selector = “type =’” + domainType + “’”;
this.consumer.addEventListener(MessageEvent.MESSAGE, setListFromMessage);
this.consumer.subscribe();
}

private function setListFromMessage(e : MessageEvent) : void {
setList(e.message.body);
}

复制代码
这里这段代码显示的是 Consumer 如何通过 ActionScript 来构建,这段代码用来接收服务器端发送的消息。Consumer 的 selector 属性仅仅用来接收那些包括了元数据中所指明的领域类型的消息。无论什么时候接收到这样的消息,event handler 都会被调用,并且列表也会得到更新。
接下来这段代码段将 RemoteObject 设置为 request-response 型通信的一个结点。所有必要的操作都作为操作属性而添加到 RemoteObject 上,客户因而很容易调用这些操作。


private var service : RemoteObject;
private var getOperation : Operation = new Operation();
public function initializeRemoteObject() {
this.service = new RemoteObject(“crudService”);

复制代码
<b><span color="#9999ff">var</span></b> operations:Object = <b><span color="#006699">new</span> </b> Object();
operations[<span color="#800000"><b>"all"</b></span>] = <b><span color="#006699">new</span> </b> Operation();
operations[<span color="#800000"><b>"all"</b></span>].addEventListener(ResultEvent.RESULT, setListFromInvocation);
operations[<span color="#800000"><b>"get"</b></span>] = getOperation
operations[<span color="#800000"><b>"remove"</b></span>] = <b><span color="#006699">new</span> </b> Operation()
operations[<span color="#800000"><b>"remove"</b></span>].addEventListener(ResultEvent.RESULT, setListFromInvocation);
operations[<span color="#800000"><b>"update"</b></span>] = <b><span color="#006699">new</span> </b> Operation()
operations[<span color="#800000"><b>"update"</b></span>].addEventListener(ResultEvent.RESULT, setListFromInvocation);
<b><span color="#006699">this</span> </b>.service.operations = operations;
<b><span color="#006699">this</span> </b>.service.addEventListener(FaultEvent.FAULT, defaultFault);
// Get the instances from the server.
<em>this</em>.service.all(domainType);
}

public function get(id : *, callback : Function) : void {
var future: AsyncToken = getOperation.send(domainType, id);
future.addResponder(new CallbackResponder(callback));
}

public function update(entity : Object) : void {
service.update(domainType, entity);
}

public function remove(entity : Object) : void {
service.remove(domainType, entity);
}

private function setListFromInvocation(e : ResultEvent) : void {
setList(e.message.body);
}

复制代码
大部分方法都将任务委派到服务的其中一个操作。所有这些操作都不会阻塞其它操作,同时它们都是异步操作。服务的返回值无论什么时候都会由注册的事件处理器(eventhandler,本例中为 setListFromInvocation)来处理,这个处理器同时还会更新列表。由于返回值在很多地方都会用到,“getOperation”就显得有点特别。CallbackResponder 只有注册了调用才能得到该调用的返回值。答复方也将调用一个 Function 来处理刚接收到的消息的内容。

import mx.rpc.IResponder;
import mx.rpc.events.ResultEvent;

public class CallbackResponder implements IResponder {
private var callback : Function;
function CallbackResponder(callback : Function) {
this .callback = callback;
}

public function result(data : Object) : void {
callback(ResultEvent(data).message.body);
}

public function fault(info : Object) : void {
DomainInstancesManager.defaultFault(info);
}
}

复制代码
### 使用通用的类包
怎样使用这个通用的类包呢?我们来看一个例子,这个例子中我们要实现的是在第二个解决方案中提到的管理 User 对象的实例。下面这段 MXML 代码定义了一个 PopUpDialog,这个 PopUpDialog 可以用来编辑系统中 Users 的详细信息。这个对话框的外观就如左图所示。实例变量 “manager”为 User 领域类型初始为一个 DomainInstanceManager 实例。界面中包含了所有捆绑到这个 manager 的 list 属性的用户的列表。它显示了用户的 displayName 值。

<mx:TitleWindow xmlns:mx=“ http://www.adobe.com/2006/mxml ” xmlns:users=“users.*” title=“User Manager”>
mx:Script
<![CDATA[
import crud.DomainInstancesManager;
import mx.managers.PopUpManager;
[Bindable]
private var manager : DomainInstancesManager = new DomainInstancesManager(“User”, “tpc”);

复制代码
<span color="#006699"><b>private</b></span> <b><span color="#008000">function</span></b> resetForm() : <span color="#006699"><b>void</b></span> {
selectedUser = <span color="#006699"><b>new</b></span> User();
secondPasswordInput.text = "";
}
<span color="#006699"><b>private</b></span> <b><span color="#008000">function</span></b> setSelected(o : Object) : void
{
selectedUser = User(o);
secondPasswordInput.text = selectedUser.password;
}
]]>

</mx:Script>
<users:User id=“selectedUser
displayName="{displayNameInput.text}"
username="{usernameInput.text}"
password="{passwordInput.text}"/>
<mx:List height=“100%” width=“200” dataProvider="{manager.list}" labelField=“displayName
itemClick=“manager.get(User(event.currentTarget.selectedItem).id, setSelected)”/>
<mx:VBox height=“100%” horizontalAlign=“right”>
mx:Form
<mx:FormItem label=“Display Name”>
<mx:TextInput id=“displayNameInput” text="{selectedUser.displayName}"/>
</mx:FormItem>

<mx:FormItem
label=“User Name”>
<mx:TextInput id=“usernameInput” text="{selectedUser.username}"/>
</mx:FormItem>

<mx:FormItem
label=“Password”>
<mx:TextInput id=“passwordInput” text="{selectedUser.password}" displayAsPassword=“true”/>
</mx:FormItem>

<mx:FormItem
label=“Password”>
<mx:TextInput id=“secondPasswordInput” text="" displayAsPassword=“true”/>
</mx:FormItem>

</mx:Form>

<mx:HBox
width=“100%”>
<mx:Button label=“New User” click="{resetForm()}"/>
<mx:Button label=“Update User” click="{manager.update(selectedUser);resetForm()}"/>
<mx:Button label=“Remove User” click="{manager.remove(selectedUser);resetForm()}"/>
</mx:HBox>

<mx:Button
label=“Close” click=“PopUpManager.removePopUp(this)”/>
</mx:VBox>

</mx:TitleWindow>

复制代码
一旦点击列表中的数据项,你就可以从服务端读取对应的 user 对象的数据,这些数据存储在界面的“ selectedUser”中。这个属性在 MXML 中定义,因此很容易用来与表单中的域绑定。 “selectedUser”属性的属性和表单中的 input 域是双向绑定,所以“selectedUser”属性值的改变(由服务器端的事件引发的修改)会影响到 input 域,而 input 域的值的改变(由用户输入值所引发的修改)也会影响到“selectedUser”属性值。界面上的按钮是链接到 manager 的方法,这个方法的参数就是“selectedUser”属性值。方法调用的结果会影响到 manager 维护的表单,也会影响到界面上显示的列表内容,因为这两者也是互相绑定的。
![](https://static001.infoq.cn/resource/image/c0/fa/c0c3b2133298287e8fdc40a2855cc2fa.jpg)
### 注意事项
需要注意的是,在使用这个通用类库的时候,你需要在客户端维护一个包含了系统所识别的某个特定类型的所有对象的列表。有些你所期望使用的引用数据和数据本身可能会在实例的数量上有一定的限制,这没什么问题。另外还有一些数据你可能不是必须的,甚至不可能维护一个完整的列表,这时候你可以在这个完整的表单的子集上采用同样的原理。
有趣的是,无论客户何时修改数据(无论是保存、更新或是删除领域对象),他都会得到一个包含了新的列表的回复。他还会接收到一个消息表明其他用户也都收到了更新的列表。因此,用户会因为自己的每个修改而收到两条更新消息。第一条(针对他请求的回复)可以被丢弃,但这条消息会添加到系统中,因为直接回复常常比通过 JMS 发送消息更费时间。
另外一个值得提及的是,由于消息中包含的更新(本例中就是完整的列表)来自于不同的信道,这个模型中可能存在并发问题。消息有可能被延迟,用户可能在收到一个更新消息之后再收到收到上一个更新的消息。这也意味着用户看到的是失去实效的数据。解决这个问题的一个方法是在消息中添加一个序列号,然后在每次接收消息的时候通过检验这个序列号来查看这条信息是否是最新的。
### 结论
通用的类包以易于使用的形式包含了前面几章中讨论的解决方案。
本文中提到的解决方案能够为开发使用 Flex 和 Grails 的 JEE 应用程序提供坚固的基础。采用这个工具箱的 JEE 开发人员的开发将可以更快、更敏捷,也许更重要的是开发将变得更有趣!
## 关于作者
Maarten Winkels 是具有五年 Java 和 JEE 开发经验的软件开发工程师和咨询师。他最近从荷兰搬迁到印度,宣传 Xebia 所提供的分布式敏捷开发过程。 Xebia 是一家专于 Java 技术、海外 Agile 项目、Agile 咨询和培训、IT 构架和审核的公司。请参考:<http://www.xebia.com/>
** 阅读英文原文 **:[Writing JEE applications with Grails and Flex](http://www.infoq.com/articles/flex-grails)。
- - - - - -
给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 [editors@cn.infoq.com](mailto:editors@cn.infoq.com)。也欢迎大家加入到 [InfoQ 中文站用户讨论组](http://groups.google.com/group/InfoQChina) 中与我们的编辑和其他读者朋友交流。
2009 年 2 月 01 日 20:067877
用户头像

发布了 71 篇内容, 共 16.4 次阅读, 收获喜欢 3 次。

关注

评论

发布
暂无评论
发现更多内容

架构师训练营第一期-第四周学习总结

卖猪肉的大叔

极客大学架构师训练营

架构师训练营 1 期 - 第四周 - 系统架构

三板斧

极客大学架构师训练营

第四周学习代码系统架构总结

三板斧

Flink处理函数-6-4

小知识点

scala 大数据 flink

5张表的sql整懵阿里p7:你们能看明白自己写的啥吗?

小Q

MySQL 数据库 学习 调优 mycat

从格力直播看品牌商的渠道变革

boshi

数字化转型 品牌 直播带货 优化业务

第四周作业

icydolphin

极客大学架构师训练营

聊聊前端 UI 组件:组件体系

欧雷

前端工程 组件化 前端工程化

spring-boot-route(十七)使用aop记录操作日志

Java旅途

Spring Boot aop

学习笔记丨数据结构与算法之贪心算法

Liuchengz.

贪心算法

架构师训练营第一期-第四周课后作业

卖猪肉的大叔

极客大学架构师训练营

架构师训练营第三周总结

薛凯

2020.10.05-2020.10.11 学习总结

icydolphin

极客大学架构师训练营

架构师训练营第四周课后练习

薛凯

架构师训练营第一周总结

薛凯

LeetCode题解:22. 括号生成,递归先生成再过滤,JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

第四周 系统架构作业

钟杰

极客大学架构师训练营

轻量级业务中台开发框架,以DDD思想为基础,融合中台核心要素,赋能中台建设

高鹏

中台 业务中台 DDD 框架 中台架构

架构师训练营第四周作业

Shunyi

极客大学架构师训练营

架构师训练营第三周课后练习

薛凯

架构师训练营第四周总结

薛凯

京东区块链之供应链应用篇:溯源应用结合区块链能碰撞出什么火花?

京东智联云开发者

区块链 供应链

有符号类型引发的奇怪现象

jiangling500

如何设计一个牛逼的API接口

Java旅途

Spring Boot API

架构师训练营第一周课后练习

薛凯

Java 中的反射是什么

Rayjun

Java 反射

阿里云服务器搭建

时间是一个人最好的证明

阿里云 服务器 域名

ARTS Week13

丽子

COSCon'20 & Apache Roadshow 来了,数据技术专场欢迎您

海豚调度

SpringBoot整合Jpa项目(含Jpa 原生sql语句介绍)

小Q

Java 架构 微服务 springboot jpa

Go 语言内存管理三部曲(二)解密栈内存管理

网管

go 堆栈 内存管理 内存布局

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

使用Grails和Flex开发JEE应用-InfoQ