Chapter 21. Remoting

Seam使用AJAX来为Web页面远程访问组件提供便捷方法。使用该框架几乎不需要预先的开发准备 —— 你只需要在组件中增加简单的注解,就可以通过AJAX来访问你的组件了。本章描述了建立一个支持AJAX的Web页面所必须的步骤,然后用更多细节继续解释Seam Remoting框架的特性。

21.1. 配置

要使用Remoting,必须先在 web.xml 文件中配置Seam Resource Servlet。

<servlet>
  <servlet-name>Seam Resource Servlet</servlet-name>
  <servlet-class>org.jboss.seam.servlet.SeamResourceServlet</servlet-class>
</servlet>

<servlet-mapping>
  <servlet-name>Seam Resource Servlet</servlet-name>
  <url-pattern>/seam/resource/*</url-pattern>
</servlet-mapping>

接下来是在Web页面引入必要的JavaScript。至少有两段脚本必须被引入。第一段脚本包含了支持远程功能的所有客户端框架代码。

<script type="text/javascript" src="seam/resource/remoting/resource/remote.js"></script>

第二段脚本包含了你希望调用的组件的存根和类型定义。它是根据组件的本地接口动态生成的,同时也包含了调用远程接口的方法时所用到的全部类的类型定义。 脚本中的名称反射到组件的名称。例如,如果有一个标有 @Name("customerAction") 的无状态会话Bean,那么脚本标签应该类似于此:

<script type="text/javascript"
          src="seam/resource/remoting/interface.js?customerAction"></script>

如果想在一个页面上访问多个组件,则需要把它们全部作为脚本标签的参数:

<script type="text/javascript"
        src="seam/resource/remoting/interface.js?customerAction&accountAction"></script>

或者,你也可以使用 s:remote 标签来引入这些必要的JavaScript,注意要通过逗号来分隔每个组件或者类的名称。

  <s:remote include="customerAction,accountAction"/>
    

21.2. Seam对象

客户端通过 Seam JavaScript对象与你的组件进行交互。 这个JavaScript对象在 remote.js 中定义,你将一直使用它来异步调用你的组件。 它被划分成两个功能域:Seam.Component 包含了与组件一起工作的方法,Seam.Remoting 包含了执行远程请求的方法。 熟悉这个对象的最简单方法是从一个简单的例子开始。

21.2.1. Hello World示例

让我们从这个简单的示例中逐步弄清楚 Seam 对象是怎样工作的。首先,我们创建一个名为 helloAction 的新的Seam组件。

@Stateless
@Name("helloAction")
public class HelloAction implements HelloLocal {
    public String sayHello(String name) {
        return "Hello, " + name;
    }
}

同时需要为这个新组件创建一个本地接口 —— 特别要注意 @WebRemote 注解,因为它是能远程访问我们方法所必须的。

@Local
public interface HelloLocal {
  @WebRemote
  public String sayHello(String name);
}

这是我们所编写的所有服务器端代码。接下来就是我们的Web页面 —— 创建一个新的页面然后引入以下脚本:

<s:remote include="helloAction"/>

为了完成一个完整的用户交互体验,我们增加一个按钮:

<button onclick="javascript:sayHello()">Say Hello</button>

同时,我们还需要增加更多脚本以使得按钮在被点击后真正能够做出相应的反应:

<script type="text/javascript">
  //<![CDATA[

  function sayHello() {
    var name = prompt("What is your name?");
    Seam.Component.getInstance("helloAction").sayHello(name, sayHelloCallback);
  }

  function sayHelloCallback(result) {
    alert(result);
  }

   // ]]>
</script>

到此已经全部完成了!部署这个应用程序并且浏览页面,点击按钮,按照提示输入一个名字,会出现一个hello消息框,这个消息框的出现表明了这次调用是成功的。 如果你想节省时间,你可以从Seam的 /examples/remoting/helloworld 目录中找到这个Hello World示例的所有源代码。

那我们的脚本到底做了什么呢?我们把它分解成更小的部分。 开始你可以从列出的JavaScript代码中看到我们已经实现了两个方法 —— 第一个方法负责提示用户输入姓名,然后产生一个远程请求。看一下下面这行:

Seam.Component.getInstance("helloAction").sayHello(name, sayHelloCallback);

这一行的第一部分 Seam.Component.getInstance("helloAction"),返回了一个代理,或者 helloAction 组件的存根。 我们可以通过这个存根调用组件的方法,这也是剩余部分即 sayHello(name, sayHelloCallback); 所做的事情。

这整行的代码是在调用我们组件的 sayHello 方法,并且传进来 name 作为参数。 第二个参数 sayHelloCallback 并不是我们组件的 sayHello 方法的参数, 相反它告诉Seam Remoting框架一旦它收到与请求对应的响应,则要把这个响应传递到JavaScript脚本的 sayHelloCallback 方法。 这个回调参数是完全可选的,因此当你调用一个返回值为 void 方法或者你不关心结果时,你可以不使用这个参数。

一旦 sayHelloCallback 方法收到了远程请求的响应,就会弹出一个警告消息,以显示方法调用的结果。

21.2.2. Seam.Component

Seam.Component JavaScript对象提供了许多客户端方法来与Seam组件一起工作, 它的两个主要方法是 newInstance() 和 getInstance(),将在下面的小节中讲解, 它们的主要区别是 newInstance() 总是创建一个组件类型的新实例,而 getInstance() 却返回一个单例的实例。

21.2.2.1. Seam.Component.newInstance()

使用newInstance()方法来创建一个实体或者JavaBean组件的一个新实例。 这个方法返回的对象将含有相同的获取/设置方法作为服务器端组成部分,或者你可以直接访问它的字段。以下面的Seam实体组件为例:

@Name("customer")
@Entity
public class Customer implements Serializable
{
  private Integer customerId;
  private String firstName;
  private String lastName;

  @Column public Integer getCustomerId() {
    return customerId;
  }

  public void setCustomerId(Integer customerId} {
    this.customerId = customerId;
  }

  @Column public String getFirstName() {
    return firstName;
  }

  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }

  @Column public String getLastName() {
    return lastName;
  }

  public void setLastName(String lastName) {
    this.lastName = lastName;
  }
}

为了创建一个客户端Customer对象,你要编写如下代码:

var customer = Seam.Component.newInstance("customer");

然后从这里起,你可以设置customer对象的字段:

customer.setFirstName("John");
// Or you can set the fields directly
customer.lastName = "Smith";

21.2.2.2. Seam.Component.getInstance()

使用 getInstance() 方法来获得一个Seam会话Bean组件的存根的引用,这个存根可以用来远程执行组件的方法。 这个方法返回特定组件的单例,因此使用同样的组件名调用它两次也将返回该组件的同一个实例。

接着看我们先前的例子,如果我们创建了一个新的 customer,同时我们想保存它, 我们需要把它传递给 customerAction 组件的 saveCustomer() 方法:

Seam.Component.getInstance("customerAction").saveCustomer(customer);

21.2.2.3. Seam.Component.getComponentName()

把一个对象传递到该方法,如果对象是组件则将返回它的组件名,否则将返回 null。

if (Seam.Component.getComponentName(instance) == "customer")
  alert("Customer");
else if (Seam.Component.getComponentName(instance) == "staff")
  alert("Staff member");

21.2.3. Seam.Remoting

Seam Remoting相关的大多数客户端功能都包含在 Seam.Remoting 对象中。 你不必直接调用它的大多数方法,但是非常有必要解释一下两个非常重要的方法。

21.2.3.1. Seam.Remoting.createType()

如果你的应用程序包含或者使用了不是Seam组件的JavaBean类,那么你就需要在客户端创建这些类型并把它们作为参数传递到组件的方法中。 使用 createType() 方法来创建你所需类型的实例,并以完整限定的Java类名传进来作为参数:

var widget = Seam.Remoting.createType("com.acme.widgets.MyWidget");

21.2.3.2. Seam.Remoting.getTypeName()

Seam.Remoting.getTypeName() 与 Seam.Component.getComponentName() 等价,但它是针对非组件类型的。 它将返回对象实例的类型名,如果不知道类型名,则返回 null。这个名称是完整限定的Java类名。

21.3. EL表达式求值

Seam Remoting也对EL表达式求值提供了支持,即提供了另外一种便利的方法来从服务器端获取数据。 通过使用 Seam.Remoting.eval(),一个EL表达式可以在服务器端被远程求值,并且将其结果返回给客户端的回调方法。 此方法接受两个参数,第一个是要被求值的EL表达式,第二个是调用表达式值的回掉方法。示例如下:

  function customersCallback(customers) {
    for (var i = 0; i < customers.length; i++) {
      alert("Got customer: " + customers[i].getName());
    }
  }

  Seam.Remoting.eval("#{customers}", customersCallback);
    

在这个例子中,表达式 #{customers} 将被Seam求值, 并且表达式的值(此处是Customer对象列表)被返回给 customersCallback() 方法。 一定要记住,以这种方式返回的对象必须导入他们的类型(通过 s:remote)这样才能与JavaScript一起工作。 因此,为了与 customer 对象列表一起工作,它需要导入 customer 类型。

<s:remote include="customer"/>

21.4. 客户端接口

在上节的配置中,接口或组件的存根通过 seam/resource/remoting/interface.js 被导入到我们页面里:

或者使用s:remote标签

<script type="text/javascript"
        src="seam/resource/remoting/interface.js?customerAction"></script>
<s:remote include="customerAction"/>

通过在页面中包含这段脚本,组件的接口定义,及执行组件的方法所需要的任何别的组件或者类型都将被生成并且可被Seam Remoting框架所使用。

这会生成两种客户存根:可执行的存根和类型存根。可执行的存根具有一定的行为,能被用来执行会话bean组件的方法。 相反,类型存根包含状态,也表示参数或者返回值的类型。

客户存根类型的生成依赖于Seam组件的类型。如果组件是会话Bean,那么将生成可执行的存根,否则如果组件是实体或者JavaBean,那么将生成类型存根。 这里也有一个例外;如果你的组件是JavaBean(例如它既不是会话Bean也不是实体Bean)并且任何它的方法被注解为@WebRemote, 那么将生成一个可执行的存根而不是一个类型存根。这允许你在非EJB环境中使用Remoting来调用JavaBean组件而不需要访问会话Bean。

21.5. 上下文

Seam Remoting上下文包含了附加的信息,并在发送和接收中作为远程请求/响应周期的一部分。目前它只包含了对话ID,但是将来它可能被扩展。

21.5.1. 设置和读取对话ID

如果你打算在对话范围内使用远程调用,那么你需要能从Seam Remoting上下文中读取或者设置对话ID。 在发起远程请求调用 Seam.Remoting.getContext().getConversationId() 后读取对话ID。 在发起请求前通过调用 Seam.Remoting.getContext().setConversationId() 来设置对话ID。

如果对话ID没有通过 Seam.Remoting.getContext().setConversationId() 显式地进行设置, 那么它将自动地被赋值为任意远程调用返回的第一个有效的对话ID。如果你的页面有多个对话,那么你需要在每次调用之前显式地设置对话ID。 如果你只是工作于单个对话中,那么你不需要额外做任何事情。

21.5.2. 当前对话范围内的远程调用

在某些情况下,可能会要求在当前视图的对话范围内发起一个远程调用,为此,你必须要在调用之前显式地设置对话ID。 以下一小段JavaScript代码将在当前视图会话ID远程调用的时候,设置会话ID。

Seam.Remoting.getContext().setConversationId( #{conversation.id} );

21.6. 批量请求

Seam Remoting允许在单个请求中执行多个组件的调用。只要能减少网络流量,那么极力推荐使用这个特性。

Seam.Remoting.startBatch() 方法将启动一个新的批处理,启动批处理后任何组件的调用都将进入队列,而不是立刻的发送。 当所有的组件调用都被加到批处理以后,Seam.Remoting.executeBatch() 方法将发送一个包含所有调用队列的请求到服务器,服务器将顺序地执行这些调用。 当这些调用被执行之后,单个响应将返回客户端,它包含了所有的返回值,同时回调函数(如果提供的话)也将按与执行相同的顺序被触发。

如果你通过 startBatch() 方法启动了一个新的批处理方法,然后你决定不发送它, 那么你需要调用 Seam.Remoting.cancelBatch() 方法,它将丢弃任何队队中的调用并退出批处理模式。

使用批处理的例子请见 /examples/remoting/chatroom。

21.7. 使用数据类型

21.7.1. 原生 / 基本 类型

这部分描述了基本数据类型的支持。一般来说,在服务器端这些值是与它们的原生类型或者相应的包装类相兼容的。

21.7.1.1. String

当设置字符串参数值时可以简单地使用Javascript字符串对象。

21.7.1.2. Number

支持Java语言支持的所有数字类型。在客户端数字值总是被序列化为字符串,在服务器端他们被转化成正确的目标类型。 进行转化时 Byte、Double、Float、Integer、Long 和 Short 的原生类型或者包装类型都被支持。

21.7.1.3. Boolean

Boolean在客户端表示为JavaScript Boolean值,在服务器端表示为Java Boolean。

21.7.2. JavaBeans

一般来说,JavaBean一般是Seam实体或者JavaBean组件,或者其他别的非组件类。 需要使用适当的方法(Seam组件使用 Seam.Component.newInstance(),其它使用 Seam.Remoting.createType())来创建对象的新实例。

注意只有这两个方法创建的对象才能被用作参数值,这些参数值不是本节所提到的有效类型之一。在不能确定参数类型的情况下,你可以使用如下的组件方法:

@Name("myAction")
public class MyAction implements MyActionLocal {
  public void doSomethingWithObject(Object obj) {
    // code
  }
}

在这个示例中你可能要传进 myWidget 组件的一个实例,然而 myAction 接口并没有包含 myWidget,因为它并没有被它的任何方法直接引用。 为了能够把参数传进来,需要显式地引入 MyWidget:

<s:remote include="myAction,myWidget"/>

这允许使用 Seam.Component.newInstance("myWidget") 创建 myWidget 对象,并允许创建后的对象传递到 myAction.doSomethingWithObject() 中。

21.7.3. Date和Time

日期值被序列化成字符串表示,并且精确到毫秒。在客户端,使用JavaScript日期对象来使用日期值。 在服务器端,使用 java.util.Date 类(或者派生类,如 java.sql.Date 或 java.sql.Timestamp)。

21.7.4. Enums 枚举类型

在客户端,Enum也被作为String处理。当为Enum参数设置值时,简单地使用Enum的字符串表示就行了。以下面的组件为例:

@Name("paintAction")
public class paintAction implements paintLocal {
  public enum Color {red, green, blue, yellow, orange, purple};

  public void paint(Color color) {
    // code
  }
} 

为了调用 paint() 方法,并且传递给参数color的值是 red,只要把red作为字符串传入就可以了:

Seam.Component.getInstance("paintAction").paint("red");

反过来也是成立的 —— 也就是说,如果一个组件方法返回Enum型参数(或者返回的对象图里包含一个Enum字段),那么在客户端仍将被表示为一个字符串。

21.7.5. Collections 集合

21.7.5.1. Bags

Bags囊括了所有的集合类型,包含arrays、collections、lists、sets,(但不包含Maps —— 见下一章),它在客户端的实现是JavaScript array。 当调用一个接收上述类型为参数的组件方法时,你的参数应该是JavaScript array。如果一个组件方法返回上述类型之一,那么返回值将是JavaScript array。 发生组件方法调用时,在服务器端Seam Remoting框架能够非常聪明地把Bag类型转化为适当的类型。

21.7.5.2. Maps

JavaScript并没有对Map的本地支持,Seam Remoting框架支持简单的Map实现。 通过创建 Seam.Remoting.Map对象以支持把Map用做远程调用的参数。

var map = new Seam.Remoting.Map();

这个Javascript实现提供了处理Map的基本方法:size()、isEmpty()、keySet()、values()、get(key)、put(key, value)、remove(key) 和 contains(key)。 每一个方法等同于Java中对应的方法。在Java中一些方法将返回集合对象,例如 keySet() 和 values(), 相应地,在JavaScript中包含key或者value对象的JavaScript Array对象也将被返回。

21.8. 调试

为了能够跟踪Bug,可以启用调试模式,在调试模式下,所有在客户端和服务器端发出和返回的数据包的内容都被显示在一个弹出窗口中。 为了启用调试模式,或者在JavaScript脚本中执行 setDebug() 方法:

Seam.Remoting.setDebug(true);

或者通过components.xml配置它:

<remoting:remoting debug="true"/>

如果要关闭调试模式,则需要调用 setDebug(false)。 如果你要在调试日志中记录一些自己定义的信息,那需要调用 Seam.Remoting.log(message)。

21.9. 加载消息

默认加载消息显示在屏幕的右上角,并且是可以修改的,它的表现形式可以自定义甚至可以关掉。

21.9.1. 修改信息

如果要把默认的“Please Wait...”消息改成其它内容,则需要设置 Seam.Remoting.loadingMessage 的值:

Seam.Remoting.loadingMessage = "Loading..."; 

21.9.2. 隐藏加载信息

如果要尽可能少的显示加载消息,可以通过覆写 displayLoadingMessage() 和 hideLoadingMessage() 的实现为反之不显示任何消息的函数:

// don't display the loading indicator
Seam.Remoting.displayLoadingMessage = function() {};
Seam.Remoting.hideLoadingMessage = function() {};

21.9.3. 自定义加载指示器

如果你需要覆写加载指示器以显示一个动画图标或者其他东西,那么你需要覆写 displayLoadingMessage() 和 hideLoadingMessage()。

  Seam.Remoting.displayLoadingMessage = function() {
    // Write code here to display the indicator
  };

  Seam.Remoting.hideLoadingMessage = function() {
    // Write code here to hide the indicator
  };

21.10. 控制返回数据

当远程方法被执行后,执行结果被序列化成一个XML响应并返回客户端。这个响应被客户端反射成一个JavaScript对象。 对于复杂的类型(例如Javabean),它们包含了其他对象的引用,所有这些被引用的对象也将被序列化为这个响应的一部分。 这些被引用的对象可能又引用了其他对象,同时还可能存在更深层次的引用关系。 如果不检查,这个对象图可能是巨大的,具体取决于对象间的关系。 作为一个次要的问题(除了响应可能很冗长之外),你可能也不希望把敏感的信息暴露给客户端。

Seam Remoting提供了一个简单的方式来限制对象图,即指定远程方法的 @WebRemote 注解中的 exclude 字段。 这个字段接受包含用.号指定的一个或多个路径的字符串数组。当调用一个远程方法时,执行结果的对象图中的对象如果与这些路径匹配,就将被从序列化结果包中去掉。

我们使用下面的 Widget 类来说明整个示例:

@Name("widget")
public class Widget
{
  private String value;
  private String secret;
  private Widget child;
  private Map<String,Widget> widgetMap;
  private List<Widget> widgetList;

  // getters and setters for all fields
}

21.10.1. 一般字段的约束

如果远程方法返回 Widget 实例,但你不想暴露 secret 字段,因为它包含一些敏感信息,你可以用如下的方式限制它:

@WebRemote(exclude = {"secret"})
public Widget getWidget(); 

值"secret"指向了返回对象的 secret 字段。现在假定我们不关心这个特殊字段会暴露给客户端。 相反,我们看到返回值 Widget 含有一个 child 字段,它也是 Widget。 我们该如何来隐藏 child 的值呢?我们可以在结果对象图中使用.号指定的字段路径来达到这个目的:

@WebRemote(exclude = {"child.secret"})
public Widget getWidget();

21.10.2. 集合和映射的约束

对象存在于一个对象图中的另一个方式是 Map 或者一些集合类(List、Set、Array 等等)。 集合类很简单,可以和其它别的字段一样来看待。例如如果 Widget 在它的 widgetList 字段里包含了一个 Widget 列表,为了限定这个列表中的 Widget 的 secret 字段的注解大概类似于这样:

@WebRemote(exclude = {"widgetList.secret"})
  public Widget getWidget();

如果要限定 Map 的键或者值,那么注解会有点不同。 在 Map 的字段名后面增加 [key] 将限定 Map 键对象,同时,[value] 将限定值对象。下面的例子描述了 widgetMap 字段怎么样限定了它们的 secret 字段:

@WebRemote(exclude = {"widgetMap[value].secret"})
public Widget getWidget(); 

21.10.3. 特定类型对象的约束

还有一个注解可用来限定对象的字段,不管字段出现在结果对象图的哪个位置它都起作用。 这个注解既可以使用组件的名称(如果对象是一个Seam组件),也可以是一个完全限定的类名(仅当对象不是Seam组件时),并且是以[]的形式来限定的。

@WebRemote(exclude = {"[widget].secret"})
public Widget getWidget(); 

21.10.4. 组合约束

限定也可以合并,用多个路径对对象图中的对象进行过滤:

@WebRemote(exclude = {"widgetList.secret", "widgetMap[value].secret"})
public Widget getWidget();

21.11. JMS消息

Seam Remoting利用实践经验对JMS消息提供了支持。这节描述了当前对JMS已有的支持,但是请记住在将来这可能发生变化。 因此现在并不推荐在产品环境中使用这个特性。

21.11.1. 配置

为能够订阅JMS主题,你必须先配置一个可以通过Seam Remoting订阅的主题列表。 需要在seam.properties、web.xml 或者 components.xml 的 org.jboss.seam.remoting.messaging.subscriptionRegistry.allowedTopics 中列出所有的主题。

<remoting:remoting poll-timeout="5" poll-interval="1"/>

21.11.2. 订阅JMS主题

下面的例子说明了如何来订阅一个JMS主题:

function subscriptionCallback(message)
{
  if (message instanceof Seam.Remoting.TextMessage)
    alert("Received message: " + message.getText());
}

Seam.Remoting.subscribe("topicName", subscriptionCallback);

Seam.Remoting.subscribe()方法具有两个参数,第一个参数是可被订阅的JMS主题名,第二个参数是接收到一个消息时要调用的回调函数。

支持两种类型的消息:Text消息和Object消息。 若要测试传给回调函数的消息类型,你可以调用 instanceof 操作符来测试这个消息是 Seam.Remoting.TextMessage 还是 Seam.Remoting.ObjectMessage。 TextMessage 的 text 字段是文本值, ObjectMessage 的 object 字段(或者调用 getObject() 方法得到的)是对象值。

21.11.3. 退订主题

如果要取消一个主题的订阅,则需要调用 Seam.Remoting.unsubscribe(),并且传进这个主题的名称:

Seam.Remoting.unsubscribe("topicName");

21.11.4. 调整轮询过程

你可以通过修改两个参数来控制轮询的发生方式。 第一个参数是Seam.Remoting.pollInterval,它控制新消息的并发轮询间隔。这个参数的单位是秒,默认值是10。

第二个参数是 Seam.Remoting.pollTimeout,它的单位也是秒。 它控制在超时和发送一个空的响应之前,发送到服务器端的请求等待新消息要等多久。 它的默认值是0秒,也就是说当服务器端被轮询到后,如果没有已经准备好的消息发送,则立即发送一个空的响应。

若要设置一个高的 pollTimeout 值,你应该非常谨慎;每一个请求都必须等待一个响应消息,也就是说服务器端线程在收到消息前,或者请求超时前是被阻塞的。 由于这个原因,如果很多请求同时被处理,会导致大量的线程被阻塞。

建议通过components.xml来设置这些选项,但是如果需要,也可以通过JavaScript来覆盖它们。 下面的例子说明了如何配置轮询以使得它更具侵略性。你应该在应用程序中为这些参数设置适当的值。

通过components.xml配置:

<remoting:remoting poll-timeout="5" poll-interval="1"/>

通过JavaScript配置:

// Only wait 1 second between receiving a poll response and sending the next poll request.
Seam.Remoting.pollInterval = 1;

// Wait up to 5 seconds on the server for new messages
Seam.Remoting.pollTimeout = 5;