服务器通信

大多数 GWT 应用程序在某个时刻都需要与后端服务器交互。GWT 提供了几种通过 HTTP 与服务器通信的不同方法。您可以使用 GWT RPC 框架透明地调用 Java servlet,并让 GWT 处理对象序列化等底层细节。或者,您可以使用 GWT 的 HTTP 客户端类 来构建和发送自定义 HTTP 请求。

注意: 要逐步了解如何在示例 GWT 应用程序中与服务器通信,请参阅教程 与服务器通信

  1. 服务器端代码
  2. 远程过程调用
  3. RPC 管道图
  4. 创建服务
  5. 实现服务
  6. 实际调用
  7. 可序列化类型
  8. 自定义序列化
  9. 处理异常
  10. 架构视角
  11. 部署 RPC
  12. 发出 HTTP 请求
  13. 习惯异步调用
  14. 直接评估 RPC

另请参阅 请求工厂 文档。

服务器端代码

发生在 Web 服务器中的所有操作称为服务器端处理。当在用户浏览器中运行的应用程序需要与服务器交互(例如,加载或保存数据)时,它会使用 远程过程调用 (RPC) 通过网络发出 HTTP 请求。在处理 RPC 时,您的服务器正在执行服务器端代码。

GWT 提供了一种基于 Java Servlet 的 RPC 机制来访问服务器端资源。此机制包括生成高效的客户端和服务器端代码,以使用 延迟绑定 通过网络 序列化 对象。

提示: 虽然 GWT 将 Java 转换为 JavaScript 用于客户端代码,但 GWT 不会干预您在服务器上运行 Java 字节码的能力。服务器端代码不需要可翻译,因此您可以自由使用任何您觉得有用的 Java 库。

GWT 不限制您使用这种 RPC 机制或服务器端开发环境。您可以自由地与其他 RPC 机制集成,例如使用 GWT 提供的 RequestBuilder 类、JSNI 方法或第三方库的 JSON。

远程过程调用

AJAX 应用程序与传统 HTML Web 应用程序之间的根本区别在于,AJAX 应用程序不需要在执行时获取新的 HTML 页面。由于 AJAX 页面实际上更像是在浏览器中运行的应用程序,因此无需从服务器请求新的 HTML 来进行用户界面更新。但是,与所有客户端/服务器应用程序一样,AJAX 应用程序通常确实需要在执行时从服务器获取数据。跨网络与服务器交互的机制称为进行远程过程调用 (RPC),有时也称为服务器调用。GWT RPC 使客户端和服务器可以通过 HTTP 来回传递 Java 对象变得容易。如果使用得当,RPC 使您能够将所有 UI 逻辑移至客户端,从而极大地提高性能,减少带宽,减少 Web 服务器负载并提供流畅的用户体验。

从客户端调用的 服务器端 代码通常称为服务,因此进行远程过程调用的行为有时称为调用服务。但需要明确的是,在这种情况下,服务一词与更通用的“Web 服务”概念不同。特别是,GWT 服务与简单对象访问协议 (SOAP) 无关。

RPC 管道图

本节概述了调用服务所需的活动部件。每个服务都有一系列小的辅助接口和类。其中一些类(例如服务代理)是在幕后自动生成的,您通常不会意识到它们的存在。辅助类的模式对于您实现的每个服务都是相同的,因此花点时间熟悉每个层在服务器调用处理中的术语和用途是一个好主意。如果您熟悉传统的远程过程调用 (RPC) 机制,您将已经认识大多数这些术语。 img

创建服务

要定义 RPC 接口,您需要

  1. 为您的服务定义一个接口,该接口扩展 RemoteService 并列出所有 RPC 方法。
  2. 定义一个类来实现服务器端代码,该代码扩展 RemoteServiceServlet 并实现您在上面创建的接口。
  3. 为您的服务定义一个异步接口,以便从客户端代码调用。

同步接口

要开始开发新的服务接口,请创建一个 客户端 Java 接口,该接口扩展 RemoteService 标记接口。

package com.example.foo.client;

import com.google.gwt.user.client.rpc.RemoteService;

public interface MyService extends RemoteService {
  public String myMethod(String s);
}

此同步接口是服务规范的权威版本。对 服务器端 上此服务的任何实现都必须扩展 RemoteServiceServlet 并实现此服务接口。

package com.example.foo.server;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.example.foo.client.MyService;


public class MyServiceImpl extends RemoteServiceServlet implements
    MyService {

  public String myMethod(String s) {
    // Do something interesting with 's' here on the server.
    return s;
  }
}

提示: 不可能直接从客户端调用此版本的 RPC。您必须为所有服务创建异步接口,如下所示。

异步接口

在您实际尝试从客户端进行远程调用之前,您必须创建另一个客户端接口(一个异步接口),该接口基于原始服务接口。继续上面的示例,在客户端子包中创建一个新的接口

package com.example.foo.client;

interface MyServiceAsync {
  public void myMethod(String s, AsyncCallback<String> callback);
}

异步方法调用的本质要求调用者传递一个回调对象,该对象可以在异步调用完成时收到通知,因为根据定义,调用者不能被阻塞,直到调用完成。出于同样的原因,异步方法没有返回值;它们通常返回 void。如果您希望对挂起请求的状态有更多控制,请返回 Request。在异步调用完成后,所有返回给调用者的通信都将通过传递的回调对象进行。

命名规范

请注意上面的示例中使用了后缀 Async 和引用 AsyncCallback 类的参数。服务接口及其异步对应方之间的关系必须遵循某些命名规范。GWT 编译器依赖于这些命名规范才能生成实现 RPC 的正确代码。

  • 服务接口必须具有相应的异步接口,该接口具有相同的包和名称,并在其后附加 Async 后缀。例如,如果服务接口名为 com.example.cal.client.SpellingService,则异步接口必须称为 com.example.cal.client.SpellingServiceAsync
  • 同步服务接口中的每个方法必须在异步服务接口中具有相应的 AsyncCallback 参数作为最后一个参数的相应方法。

有关如何实现异步回调的更多详细信息,请参阅 AsyncCallback

实现服务

每个服务最终都需要执行一些处理,以便对客户端请求做出响应。这种 服务器端 处理发生在服务实现中,服务实现基于众所周知的 servlet 架构。服务实现必须扩展 RemoteServiceServlet 并且必须实现关联的服务接口。请注意,服务实现实现服务的异步版本接口。

每个服务实现最终都是一个 servlet,但它不是扩展 HttpServlet,而是扩展 RemoteServiceServletRemoteServiceServlet 自动处理在客户端和服务器之间传递的数据的序列化以及调用服务实现中预期的方法。

在开发过程中测试服务

GWT 开发模式包含 Jetty 的嵌入式版本,它充当用于测试的开发时 servlet 容器。这使您能够在使用 Java 调试器在开发模式下运行应用程序时调试服务器端代码和客户端代码。要自动加载服务实现,请在 web.xml 中配置您的 servlet。

例如,假设您有一个模块 com.example.foo.Foo,并且您定义了一个 RPC 接口 com.example.foo.client.MyService,用 @RemoteServiceRelativePath("myService") 进行注释。然后,您使用类 com.example.foo.server.MyServiceImpl 为您为 com.example.foo.client.MyService 创建的接口实现一个 servlet,该类扩展了 RemoteServiceServlet。最后,您将以下几行添加到 web.xml

<!-- Example servlet loaded into servlet container -->
<servlet>
  <servlet-name>myServiceImpl</servlet-name>
  <servlet-class>
    com.example.foo.server.MyServiceImpl
  </servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>myServiceImpl</servlet-name>
  <url-pattern>/com.example.foo.Foo/myService</url-pattern>
</servlet-mapping>

看一下 url-pattern 中的值。第一部分必须与您的 GWT 模块的名称匹配。如果您的模块具有 rename-to 属性,您将使用重命名的值;无论哪种方式,它都必须与您的 GWT 模块所在的 war 目录中的实际子目录(模块基本 URL)匹配。第二部分必须与您在注释 com.example.foo.client.MyServiceRemoteServiceRelativePath 注释中指定的 value 匹配。

在开发模式下测试客户端代码和服务器端代码时,请确保将 gwt-servlet.jar 的副本放入您的 war/WEB-INF/lib 目录中,并确保您的 Java 输出目录设置为 war/WEB-INF/classes。否则,嵌入式 Jetty 服务器将无法正确加载您的 servlet。

常见陷阱

以下是一些在尝试使 RPC 运行时常见的错误

  • 当您启动开发模式时,您会在控制台中看到一个 ClassNotFoundException 异常,并且嵌入式服务器返回一个错误。这很可能意味着您的 GWT 模块中 servlet 元素引用的类不在 war/WEB-INF/classes 中。请确保将服务器类编译到此位置。如果您使用的是 webAppCreator 生成的 Ant build.xml,它应该自动为您执行此操作。
  • 当您启动开发模式时,您会在控制台中看到一个 NoClassDefFoundError: com/google/gwt/user/client/rpc/RemoteService 异常,并且嵌入式服务器返回一个错误。这很可能意味着您忘记将 gwt-servlet.jar 复制到 war/WEB-INF/lib 中。如果您使用的是 webAppCreator 生成的 Ant build.xml,它应该自动为您执行此操作。稍后,如果您需要其他服务器端库,您还需要将这些库的副本添加到 war/WEB-INF/lib 中。
  • 在运行 RPC 调用时,开发模式显示异常 NoServiceEntryPointSpecifiedException: Service implementation URL not specified。此错误意味着您没有在服务接口中指定 @RemoteServiceRelativePath,并且您也没有通过调用 ServiceDefTarget.setServiceEntryPoint() 手动设置目标路径。
  • 如果调用 RPC 调用失败并出现 404 StatusCodeException,您的 web.xml 可能配置错误。确保您指定了 @RemoteServiceRelativePath,并且 web.xml 中指定的 <url-pattern> 与此值匹配,并在此值之前添加您的 GWT 输出目录在 war 目录中的位置。

将服务部署到生产环境中

如果开发模式正常工作,并且您已使用 GWT 编译器编译了应用程序并测试了生产模式是否正常工作,那么您应该能够将 war 目录的内容部署到适合您应用程序的任何 servlet 容器中。有关如何将应用程序部署到 servlet 容器的示例,请参阅本指南中的 示例部署

实际进行调用

从客户端进行 RPC 的过程始终涉及相同的步骤

  1. 使用 GWT.create() 实例化服务接口。
  2. 创建一个异步回调对象,以便在 RPC 完成时收到通知。
  3. 进行调用。

示例

假设您想调用服务接口上定义的方法,如下所示

// The RemoteServiceRelativePath annotation automatically calls setServiceEntryPoint()
@RemoteServiceRelativePath("email")
public interface MyEmailService extends RemoteService {
  void emptyMyInbox(String username, String password);
}

其对应的异步接口将如下所示

public interface MyEmailServiceAsync {
  void emptyMyInbox(String username, String password,
      AsyncCallback<Void> callback);
}

客户端调用将如下所示

public void menuCommandEmptyInbox() {
  // (1) Create the client proxy. Note that although you are creating the
  // service interface proper, you cast the result to the asynchronous
  // version of the interface. The cast is always safe because the
  // generated proxy implements the asynchronous interface automatically.
  //
  MyEmailServiceAsync emailService = (MyEmailServiceAsync) GWT.create(MyEmailService.class);

  // (2) Create an asynchronous callback to handle the result.
  //
  AsyncCallback callback = new AsyncCallback() {
    public void onSuccess(Void result) {
      // do some UI stuff to show success
    }

    public void onFailure(Throwable caught) {
      // do some UI stuff to show failure
    }
  };

  // (3) Make the call. Control flow will continue immediately and later
  // 'callback' will be invoked when the RPC completes.
  //
  emailService.emptyMyInbox(fUsername, fPassword, callback);
}

缓存实例化的服务代理是安全的,以避免为后续调用创建它。例如,您可以在模块的 onModuleLoad() 方法中实例化服务代理,并将结果实例保存为类成员。

public class Foo implements EntryPoint {
      private MyEmailServiceAsync myEmailService = (MyEmailServiceAsync) GWT.create(MyEmailService.class);

      public void onModuleLoad() {
        // ... other initialization
      }

      /**
       * Make a GWT-RPC call to the server.  The myEmailService class member
       * was initialized when the module started up.
       */
      void sendEmail (String message) {
          myEmailService.sendEmail(message, new AsyncCallback<String>() {

            public void onFailure(Throwable caught) {
              Window.alert("RPC to sendEmail() failed.");
            }

            public void onSuccess(String result) {
              label.setText(result);
            }
          });
      }
    }

可序列化类型

GWT 支持序列化的概念,这意味着允许将数据对象的内容从运行的代码的一部分中移出,并将其传输到另一个应用程序,或者将其存储在应用程序外部以供日后使用。GWT RPC 方法参数和返回类型必须在客户端和服务器应用程序之间通过网络传输,因此它们必须可序列化

可序列化类型必须符合某些限制。GWT 非常努力地使序列化尽可能轻松。虽然关于序列化的规则很微妙,但在实践中,行为很快就变得直观了。

提示:虽然术语非常相似,但 GWT 的“可序列化”概念略有不同,与基于标准 Java 接口 Serializable 的序列化不同。所有关于序列化的引用都指的是 GWT 概念。有关背景信息,请参阅 FAQ 主题 GWT RPC 系统是否支持使用 java.io.Serializable?

如果以下任一条件为真,则类型是可序列化的,并且可以在服务接口中使用

  • 该类型是基本类型,例如 charbyteshortintlongbooleanfloatdouble
  • 该类型是 StringDate 或基本类型包装器(例如 CharacterByteShortIntegerLongBooleanFloatDouble)的实例。
  • 该类型是枚举。枚举常量仅以名称的形式序列化;不会序列化任何字段值。
  • 该类型是可序列化类型的数组(包括其他可序列化数组)。
  • 该类型是可序列化的用户定义类。
  • 该类型至少有一个可序列化子类。
  • 该类型具有 自定义字段序列化器

java.lang.Object 不可序列化,因此您不能期望在网络上传输 Object 类型集合。从 GWT 1.5 开始,大多数用例可以使用 Java 泛型来替换 Object 实例的使用。强烈建议这样做,既可以减少客户端代码的大小,又可以提供针对某些拒绝服务攻击的安全性。

可序列化的用户定义类

如果满足以下所有条件,则用户定义的类是可序列化的

  1. 它可分配给 IsSerializableSerializable,要么是因为它直接实现了这些接口之一,要么是因为它派生自实现了这些接口的超类
  2. 所有非 final、非 transient 实例字段本身都是可序列化的,并且
  3. 从 GWT 1.5 开始,它必须具有默认(零参数)构造函数(具有任何访问修饰符)或根本没有构造函数。

transient 关键字得到认可,因此在 RPC 中不会交换 transient 字段中的值。声明为 final 的字段也不会在 RPC 中交换,因此它们通常也应该标记为 transient

多态性

GWT RPC 支持多态参数和返回类型。但是,为了充分利用多态性,您仍然应该在定义服务接口时尽可能具体。更高的特异性允许 编译器 在优化应用程序以减少大小方面做得更好。此外,服务器端反序列化使用从方法签名中提取的泛型类型信息来验证它反序列化的值是否为正确的类型(以防止某些安全攻击)。

原始类型

集合类(例如 java.util.Setjava.util.List)很棘手,因为它们以 Object 实例的形式运行。为了使集合可序列化,您应该通过正常的类型参数指定它们预期包含的对象的特定类型(例如,Map<Foo,Bar> 而不是仅仅 Map)。如果您使用原始集合或映射,您将获得膨胀的代码,并且容易受到拒绝服务攻击。

序列化增强类

服务器端持久性 API(例如 Java 数据对象 (JDO)Java 持久性 API)允许服务器端代码将 Java 对象存储在持久性数据存储中。当对象附加到数据存储时,对对象字段值所做的更改将反映在数据存储中,并且即使在对象被销毁或 Java VM 重新启动后,这些更改仍然可用。

许多持久性 API 通常通过将类源代码或字节码传递给称为“增强器”的工具来实现。增强器可能会在类定义中添加额外的静态或实例字段,以实现持久性功能。请注意,这些增强与服务器端相关;GWT 不为客户端代码提供持久性。但是,使用服务器端增强会导致客户端和服务器之间的类定义不同,因为增强器仅在服务器上运行。由于这些差异,早期版本的 GWT 无法对增强类执行 RPC。

从 GWT 2.0 版本开始,GWT RPC 机制现在处理了一些常见的持久性形式。如果满足以下任一条件,GWT 将认为一个类是增强的

  • 该类使用 JDO javax.jdo.annotations.PersistenceCapable 注释进行注释,其中 detachable=true
  • 该类使用 JPA javax.persistence.Entity 注释进行注释。* 完全限定的类名被列为应用程序中 .gwt.xml 模块文件中 rpc.enhancedClasses 配置属性的值之一。

增强类的 GWT RPC 机制对持久化实现做出了几项假设。

  • 对象必须处于分离状态(即,对其字段的更改不应影响持久化存储,反之亦然),此时它被传递到或从 RemoteService 的方法返回。
  • 既不是静态也不是瞬态的增强字段必须使用普通的 Java 序列化机制进行序列化。
  • 如果持久化实现要求对增强实例字段的更改具有其他副作用(例如,如果静态字段需要同时更新),则必须为实例字段存在一个具有适当名称的设置方法(如下所述)。

当从服务器到客户端传输增强对象时,正常的 GWT RPC 机制用于非增强字段。服务器上存在但客户端上不存在的非瞬态、非静态字段在服务器上使用 Java 序列化进行序列化,字段名称和值组合成一个单一的编码值,传递给客户端。客户端存储此编码值,但除了将对象传回服务器时,不会使用它。

当增强对象从客户端传送到服务器时,编码值(如果存在)将被发送到服务器,在那里它被解码成单独的字段名称和值。对于每个字段“xxxYyy”,如果存在名为“setXxxYyy”的设置方法(注意“set”后面的第一个字母的大小写),它将使用字段值作为其参数调用。否则,字段将被直接设置。

自定义序列化

具有特定序列化要求的类可以利用自定义字段序列化器。示例包括具有需要参数的构造函数的类,或数据以不同于类结构的形式更有效地序列化的类。在现有的 GWT 类中,大多数 Java 集合类都有自定义序列化器,以及一些日志记录和异常处理类。

Foo 的自定义字段序列化器必须命名为 Foo_CustomFieldSerializer,并且必须位于与其序列化的类相同的包中。在无法将序列化器放在适当包中的情况下,可以将序列化器放在特定的 GWT 序列化包中,com.google.gwt.user.client.rpc.core。例如,java.util.HashMap 的自定义字段序列化器是 com.google.gwt.user.client.rpc.core.java.util.HashMap_CustomFieldSerializer

自定义字段序列化器应扩展 CustomFieldSerializer<T> 类,将要序列化的类作为类型参数。例如

public final class HashMap_CustomFieldSerializer extends CustomFieldSerializer<HashMap>

所有自定义字段序列化器类必须实现 serializeInstancedeserializeInstance 方法。可选地,如果一个类不能使用默认实例化(例如,它没有默认构造函数)或想要自定义实例化(例如,实例化不可变对象),它可以覆盖默认的 instantiateInstancehasCustomInstantiateInstance 方法。有关这些方法的示例,请参阅内置的自定义序列化器。

类型检查的自定义序列化

一些拒绝服务攻击利用了 RPC 消息中的方法参数必须在方法被调用之前被反序列化这一事实,因此在方法因无效参数而抛出异常之前被反序列化。从 GWT 版本 2.4 开始,提供了对方法参数在反序列化时的服务器端类型检查的支持。正确使用类型检查可以防止参数替换攻击。

为了支持类型检查,必须提供自定义字段序列化器的服务器特定版本。自定义序列化器类必须位于与客户端序列化器具有相同限定名称的服务器包中,将“client”替换为“server”。Foo 的实际序列化器类必须命名为 Foo_ServerCustomFieldSerializer。例如,方法 test.com.google.gwt.user.client.rpc.TypeCheckedGenericClass 的客户端序列化器是 test.com.google.gwt.user.client.rpc.TypeCheckedGenericClass_CustomFieldSerializer,而服务器类型检查版本是 test.com.google.gwt.user.server.rpc.TypeCheckedGenericClass_ServerCustomFieldSerializer

服务器自定义字段序列化器应扩展 ServerCustomFieldSerializer<T> 类,将要序列化的类作为类型参数。例如

public final class HashMap_ServerCustomFieldSerializer extends ServerCustomFieldSerializer<HashMap>

所有服务器自定义字段序列化器类必须实现客户端 CustomFieldSerializer 方法,以及额外的 deserializeInstance 方法。此外,当客户端代码覆盖它时,类必须覆盖默认的 instantiateInstance 方法。

有关类型检查的示例,请参阅现有的序列化器,例如 HashMap_ServerCustomFieldSerializer

处理异常

进行 RPC 会打开各种错误的可能性。网络故障、服务器崩溃以及在处理服务器调用时出现问题。GWT 允许您根据 Java 异常处理这些条件。与 RPC 相关的异常分为两类:已检查异常和未检查异常。

请记住,您要定义的任何自定义异常,就像任何其他客户端代码一样,只能由 GWT 模拟的 JRE 库 支持的类型组成。

已检查异常

服务接口 方法支持 throws 声明,以指示哪些异常可能从服务实现抛回客户端。调用者应实现 AsyncCallback.onFailure(Throwable) 以检查服务接口中指定的任何异常。

意外异常

InvocationException

RPC 根本可能无法到达 服务实现。这可能由于多种原因发生:网络可能断开连接、DNS 服务器可能不可用、HTTP 服务器可能没有在侦听,等等。在这种情况下,InvocationException 将传递到您的 AsyncCallback.onFailure(Throwable) 实现中。该类被称为 InvocationException,因为问题出在调用尝试本身,而不是服务实现。

如果调用确实到达了服务器,但在调用正常处理期间发生了一个未声明的异常,则 RPC 也会以调用异常失败。这种情况可能发生在许多原因:必要的服务器资源(如数据库)可能不可用、由于服务实现中的错误而可能抛出 NullPointerException,等等。在这些情况下,InvocationException 会在应用程序代码中抛出。

IncompatibleRemoteServiceException

另一种类型的故障可能是由客户端和服务器之间的不兼容引起的。这最常发生在对 服务实现 的更改被部署到服务器,但过时的客户端仍然处于活动状态时。有关更多详细信息,请参阅 IncompatibleRemoteServiceException

当客户端代码收到 IncompatibleRemoteServiceException 时,它最终应尝试刷新浏览器以获取最新的客户端。

架构视角

在您的应用程序架构中,有各种方法可以处理服务。首先了解 GWT RPC 服务 不打算取代 J2EE 服务器,也不打算为您的应用程序提供公共 Web 服务(例如 SOAP)层。从根本上说,GWT RPC 仅仅是“从客户端到服务器”的一种方法。换句话说,您使用 RPC 来完成应用程序的一部分,但这些任务无法在客户端计算机上完成。

从架构上讲,您可以通过两种不同的方法使用 RPC。区别在于品味和应用程序的架构需求。

简单客户端/服务器部署

考虑服务定义的第一种也是最直接的方法是将它们视为应用程序的整个后端。从这个角度来看,客户端代码 是您的“前端”,所有在服务器上运行的服务代码都是“后端”。如果您采用这种方法,您的服务实现往往会成为更通用的 API,而不是与一个特定的应用程序紧密耦合。您的服务定义可能会直接通过 JDBC 或 Hibernate 甚至服务器文件系统中的文件访问数据库。对于许多应用程序来说,这种视图是合适的,而且它可以非常高效,因为它减少了层级数量。

多层部署

在更复杂的、多层架构中,您的 GWT 服务定义可以只是轻量级网关,它们调用到后端服务器环境(如 J2EE 服务器)。从这个角度来看,您的服务可以被视为应用程序用户界面的“服务器端”。服务不是通用的,而是为用户界面的特定需求而创建的。您的服务成为“后端”类的“前端”,这些类是通过将调用串联到更通用的后端服务层而编写的,例如,作为 J2EE 服务器集群来实现。如果您需要您的后端服务在与您的 HTTP 服务器不同的物理计算机上运行,这种架构是合适的。

部署 RPC

GWT 开发模式嵌入式 Web 服务器仅用于调试您的 Web 应用程序。一旦您的 Web 应用程序准备好部署,就该部署到生产服务器了。如果您的 Web 应用程序只包含静态内容,那么几乎任何 Web 服务器都可以。但是,大多数 GWT Web 应用程序都会使用 RPC 和 Java Servlet。对于这些类型的应用程序,您需要选择一个 Servlet 容器(也称为 Web 容器或 Web 引擎)来运行后端。GWT 没有提供在生产环境中使用的 Servlet 容器,但有许多不同的产品可用。以下是少数商业和非商业产品:

在下面的示例中,我们将展示如何使用 Apache HTTPD 和 Apache Tomcat 作为 Web 引擎将您的服务器端组件部署到生产服务器。虽然 Web 服务器和 Servlet 容器的实现有很多,但 Servlet API 规范定义了大多数 Web 引擎遵循的项目目录的标准结构,并且从 1.6 开始,GWT 的本机输出格式遵循了此规范。

提示:有关最新 Servlet 规范文档,请参阅 Java Community Process 网站。它将描述 Servlet 的一般工作原理以及在配置您的 Servlet 以进行部署时使用的约定。

Apache Tomcat 的简单示例

在开始之前,请确保您的应用程序在开发模式下正常工作。

部署应用程序的最简单方法是使用单个服务器来处理您的静态内容和您的 Servlet 类。如果您使用 webAppCreator 创建了您的项目,您可以简单地在项目目录中运行 ant war。Ant build.xml 文件应该自动执行以下操作:

  1. 将所有必要的库复制到 war/WEB-INF/lib 中。如果您在 gwt-servlet.jar 之外添加了其他库依赖项,则可能需要更新您的 build.xml 文件。
  2. 将您的 Java 源文件编译到 war/WEB-INF/classes 中。这对于在 Web 服务器上运行您的服务器代码是必需的。
  3. 在您的 GWT 模块上运行 GWT 编译器。这会生成您需要的全部 GWT 输出文件。
  4. 将 war 目录的内容压缩成一个 .war 文件。

现在将你的 .war 文件复制到 Tomcat 的 /webapps 文件夹中。如果你有默认的配置设置,它应该会自动解压缩 .war 文件。

如果 Tomcat 处于默认配置状态,并在端口 8080 上运行,你应该可以通过在 Web 浏览器中输入 url http://<hostname>:8080/MyApp/MyApp.html 来运行你的应用程序。

如果你遇到任何问题,请查看 Tomcat 日志文件,该文件位于 Tomcat 安装的 logs 目录中。如果你的网页显示,但 RPC 调用似乎没有通过,请尝试在 Tomcat 上开启访问日志。你可能会发现,客户端使用的 URL 没有被 Tomcat 注册,或者在声明 RPC 服务时,在 setServiceEntryPoint(URL) 调用中设置的 URL 路径与 web.xml 文件中映射的 <url-pattern> URL 映射之间存在配置错误。

将 Tomcat 与 Apache HTTPD 和代理一起使用

上面的例子是在客户端到服务器之间测试应用程序的好方法,但可能不是最好的生产环境设置。首先,Jakarta/Tomcat 服务器不是为静态文件提供服务的最快 Web 服务器。此外,你可能已经为提供静态内容设置了 Web 基础设施,并且希望利用它。下面的例子将展示如何将 Web 引擎部署与静态内容分离。

如果你查看已构建 Web 应用程序的 war 目录,你会注意到两种类型的文件。

  1. 除了 WEB-INF/ 目录(及其子目录)之外的所有内容都是静态内容。
  2. WEB-INF/ 目录的内容包含服务器配置和代码。

构建 Web 应用程序后,将 war 目录中的内容(除了 WEB-INF/ 目录)复制到 apache 配置的文档根目录中。我们将该目录称为 /var/www/doc/MyApp,生成的 URL 为 http://www.example.com/MyApp/MyApp.html。(一个更简单的方法可能是直接复制整个 war 内容,然后从目标位置删除 WEB-INF/。)

要设置 Tomcat 服务器,只需部署整个 war,包括两种类型的內容。包含静态内容的原因是,servlet 可能需要使用 ServletContext.getResource() 以编程方式访问静态内容。对于 GWT RPC servlet 来说,这始终是正确的,因为它们需要加载一个生成的序列化策略文件。

示例

假设 Tomcat 服务器在不同的主机上。在这种情况下,会出现一个问题。浏览器的单源策略 (SOP) 将阻止连接到与原始 URL 不同的端口或机器。我们将使用的策略是配置 Apache 将一个 URL 代理到另一个 URL,以满足 SOP。

有关为这种代理设置配置 Apache 和 Tomcat 的具体说明,请参见 Apache 网站。基本思路是设置 Apache,使其仅重定向对 servlet 的请求。这样,Apache 将提供所有静态内容,而 Tomcat 服务器将仅用于服务调用。对于本例,假设:

  • 你的 Apache 服务器在 www.example.com 上运行。
  • 你的 Tomcat 服务器在 servlet.example.com:8080 上运行。
  • 你的 GWT 模块有一个 <rename-to="myapp">
  • 你有一个 RPC servlet,映射到 /myapp/myService

目的是让 Apache 将对 servlet 的请求代理到另一个服务器,以便

http://www.example.com/MyApp/myapp/myService --> http://servlet.example.com:8080/MyApp/myapp/myService

下面的 Apache 配置使用代理设置了这样的规则。

ProxyPass        /MyApp/myapp/myService  http://servlet.example.com:8080/MyApp/myapp/myService
ProxyPassReverse /MyApp/myapp/myService  http://servlet.example.com:8080/MyApp/myapp/myService

要验证这是否有效,请使用 Web 浏览器访问 http://www.example.com/MyApp/myapp/myServicehttp://servlet.example.com:8080/MyApp/myapp/myService。你应该在这两种情况下得到相同的结果(通常是 405: HTTP method GET is not supported by this URL,这很好)。如果在访问第二个 URL 时得到不同的结果,你可能存在配置问题。

  • 如果你得到 404,那么 URL 映射的左侧很可能存在错误。
  • 如果你得到“Bad Gateway”,那么 URL 映射的右侧很可能存在错误。
  • 如果你得到 403 权限错误,请检查 Apache 配置文件中的 <Proxy> 标记,查看权限是否错误。你可能需要添加类似这样的部分。
<Proxy \*>
     Order deny,allow
     Allow from all
   </Proxy>

其他部署方法

需要注意的是,这些只是两种部署场景的示例。有许多方法可以配置应用程序以进行部署。

  • 一些 Web 引擎在 web.xml 中接受特殊指令,需要考虑这些指令。
  • 一些 Web 服务器有优化方法将 servlet 调用路由到 servlet 引擎(例如 Apache 上的 mod_jk)。
  • 一些 Web 服务器和 Web 引擎具有复制功能,因此你可以在多个节点上负载均衡 servlet。

提示: 你也可以在开发模式下使用真正的生产服务器。如果你将 GWT 添加到现有应用程序中,或者如果你的服务器端需求已经超过了嵌入式 Web 服务器的能力,这将非常有用。请参阅这篇关于如何在开发模式下使用外部服务器的文章。

发出 HTTP 请求

如果你的 GWT 应用程序需要与服务器通信,但你不能在后端使用 Java servlet — 或者你只是不喜欢使用 RPC — 你仍然可以手动执行 HTTP 请求。GWT 包含许多 HTTP 客户端类,简化了对服务器发出自定义 HTTP 请求以及可选地处理 JSONXML 格式的响应。

GWT 包含一组 HTTP 客户端类,允许你的应用程序发出通用的 HTTP 请求。

在 GWT 中使用 HTTP

在 GWT 中发出 HTTP 请求与在任何语言或框架中都非常类似,但有一些重要的区别需要注意。

首先,由于大多数 Web 浏览器的单线程执行模型,长时间的同步操作(如服务器调用)会导致 JavaScript 应用程序的界面(有时还会导致浏览器本身)变得无响应。为了防止网络或服务器通信问题导致浏览器“挂起”,GWT 仅允许进行异步服务器调用。发送 HTTP 请求时,客户端代码必须注册一个回调方法,该方法将处理响应(或错误,如果调用失败)。有关排除同步服务器连接的更多信息,你可能需要查看这篇 常见问题解答文章

其次,由于 GWT 应用程序在 Web 页面中作为 JavaScript 运行,因此它们受浏览器 同源策略 (SOP) 的约束。SOP 阻止客户端 JavaScript 代码与从其他网站加载的不可信(且可能存在危害)资源进行交互。特别是,SOP 使得向除提供加载 GWT 模块的 HTML 宿主页面的服务器之外的服务器发送 HTTP 请求变得困难(虽然并非不可能)。

HTTP 客户端类型

要在你的应用程序中使用 HTTP 类型,你需要先通过在 模块 XML 文件 中添加以下 <inherits> 标记来继承 GWT HTTP 模块。

<inherits name="com.google.gwt.http.HTTP" />

RequestBuilder 是构建和发送 HTTP 请求所需的核心类。它的 构造函数 有用于指定请求的 HTTP 方法(GET、POST 等)和 URL 的参数(URL 实用程序类对于转义无效字符非常有用)。获取 RequestBuilder 对象后,可以使用其方法设置 用户名密码超时间隔。你也可以在 HTTP 请求中设置任意数量的 标头

HTTP 请求准备好后,使用 sendRequest(String, RequestCallback) 方法调用服务器。传递的 RequestCallback 参数将处理响应或发生的错误。当请求正常完成时,将调用你的 onResponseReceived(Request, Response) 方法。可以从 Response 参数中检索响应的详细信息(例如,状态代码HTTP 标头响应文本)。请注意,即使 HTTP 状态代码不是 200(成功),也会调用 onResponseReceived(Request, Response) 方法。如果调用没有正常完成,则会调用 onError(Request, Throwable) 方法,第二个参数描述了发生的错误类型。

如前所述,GWT 中的所有 HTTP 调用都是异步的,因此在对 sendRequest(String, RequestCallback) 的调用之后执行的代码将立即执行,而不是在服务器响应 HTTP 请求之后执行。你可以使用从 sendRequest(String, RequestCallback) 返回的 Request 对象来 监控调用的状态,并在必要时 取消它

以下是对服务器发出 HTTP 请求的简要示例。

import com.google.gwt.http.client.*;
...

String url = "http://www.myserver.com/getData?type=3";
RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, URL.encode(url));

try {
  Request request = builder.sendRequest(null, new RequestCallback() {
    public void onError(Request request, Throwable exception) {
       // Couldn't connect to server (could be timeout, SOP violation, etc.)
    }

    public void onResponseReceived(Request request, Response response) {
      if (200 == response.getStatusCode()) {
          // Process the response in response.getText()
      } else {
        // Handle the error.  Can get the status text from response.getStatusText()
      }
    }
  });
} catch (RequestException e) {
  // Couldn't connect to server
}

处理响应

使用 Response.getText() 从服务器接收响应后,由你来处理它。如果你的响应以 XML 或 JSON 编码,你可以使用 XML 库JSON 库覆盖类型 分别处理它。

习惯异步调用

使用 AsyncCallback 接口对许多开发人员来说是新的。代码不一定是按顺序执行的,它迫使开发人员处理服务器调用正在进行但尚未完成的情况。尽管有这些看似不足的地方,但 GWT 的设计者认为,出于以下几个原因,它是创建 AJAX 中可用界面的重要组成部分。

  • 大多数浏览器中的 JavaScript 引擎是单线程的,这意味着
    • JavaScript 引擎将在等待同步 RPC 调用返回时挂起。
    • 如果任何 JavaScript 入口点花费的时间超过几分钟,大多数浏览器都会弹出对话框。
  • 服务器可能无法访问或无响应。如果没有异步机制,应用程序可能会无限期地等待服务器调用返回。
  • 异步机制允许服务器调用与应用程序内部的逻辑并行运行,缩短了对用户的整体响应时间。
  • 异步机制可以并行服务多个服务器调用。

除了浏览器机制之外,异步 RPC 使您的应用程序能够在应用程序中实现真正的并行性,即使没有多线程。例如,假设您的应用程序显示一个包含许多小部件的大型 表格。构建和布局所有这些小部件可能很耗时。同时,您需要从服务器获取数据以在表格中显示。这是一个使用异步调用的完美理由。在开始构建表格及其小部件之前,立即启动一个异步调用来请求数据。当服务器正在获取所需数据时,浏览器正在执行您的用户界面代码。当客户端最终从服务器接收到数据时,表格已经构建和布局,并且数据已准备好显示。

为了让您了解这种技术的有效性,假设构建表格需要一秒钟,而获取数据需要一秒钟。如果您同步进行服务器调用,整个过程将至少需要两秒钟。

img

但是,如果您异步获取数据,即使您做了两秒钟的工作,整个过程仍然只需要一秒钟。

img

提示:大多数浏览器将出站网络连接数限制为一次两个,限制了您对多个同时 RPC 的并行性期望。

异步调用最难适应的是调用是非阻塞的,但是,Java 内部类在很大程度上使这变得易于管理。考虑以下异步调用的实现,它改编自 Dynamic Table 示例应用程序。它使用略微不同的语法来定义 AsyncCallback 对象所需的接口,该对象是 getPeople RPC 调用的最后一个参数。

// This code is called before the RPC starts
 //
  if (startRow == lastStartRow) {
    ...
  }

  // Invoke the RPC call, implementing the callback methods inline:
  //
  calService.getPeople(startRow, maxRows, new AsyncCallback<Person[]>() {

    // When the RPC returns, this code will be called if the RPC fails
    public void onFailure(Throwable caught) {
       statusLabel.setText("Query failed: " + caught.getMessage());
       acceptor.failed(caught);
    }

    // When the RPC returns, this code is called if the RPC succeeds
    public void onSuccess(Person[] result) {
      lastStartRow = startRow;
      lastMaxRows = maxRows;
      lastPeople = result;
      pushResults(acceptor, startRow, result);
      statusLabel.setText("Query reutrned " + result.length + " rows.");
    }
  });

  // The above method call will not block, but return immediately.
  // The following code will execute while the RPC is in progress,
  // before either of onFailure() or onSuccess() are executed.
  //
  statusLabel.setText("Query in progress...");
  ...

需要理解的重要问题是,RPC 调用调用后的代码将在实际往返服务器的过程中执行。尽管 onSuccess() 方法中的代码是在调用中内联定义的,但它不会在调用代码返回到 JavaScript 主循环并且来自服务器的结果消息返回之前执行。

直接评估 RPC

此功能没有按计划进行,GWT 团队强烈建议不要使用它。当您有非平凡的服务器域对象时,请尝试使用 Request Factory 功能。