RequestFactory

RequestFactory 是 GWT-RPC 的替代方案,用于创建面向数据的服务。RequestFactory 及其相关接口(RequestContext 和 EntityProxy)使得使用类似 ORM 的接口在客户端构建面向数据的(CRUD)应用变得很容易。它旨在与服务器上的 ORM 层(如 JDO 或 JPA)一起使用,但这不是必需的。

概览

优势

RequestFactory 使得在客户端和服务器上实现数据访问层变得容易。它允许您以面向数据的方式构建服务器端代码,并提供比 GWT-RPC 更高层次的抽象,GWT-RPC 面向服务而不是面向数据。在客户端,RequestFactory 会跟踪已修改的对象,只将更改发送到服务器,这会导致网络负载非常轻量级。此外,RequestFactory 为将来的自动批处理和请求缓存奠定了坚实的基础。

它与 GWT-RPC 有什么关系?

RequestFactory 使用它自己的 servlet,RequestFactoryServlet,并实现它自己的协议用于客户端和服务器之间的数据交换。RequestFactory 对客户端-服务器编程模型采取了比 GWT-RPC 更具规范性的方法。GWT-RPC 使用远程方法作为其基本构建块(类似于 Java RMI),而 RequestFactory 使用实体(具有持久标识的数据)和服务。请参阅此外部网站以获得 有关 RequestFactory 与 GWT-RPC 的讨论

使用 RequestFactory 编码

让我们看一下使用 RequestFactory 的应用程序中的活动部分。

  1. 实体
  2. 实体代理
  3. 值代理
  4. RequestFactory 接口
  5. 可传输类型
  6. 服务器实现
  7. 在实体类中实现服务
  8. 使用 Locator 和 ServiceLocator

然后我们将看看如何将它们组合在一起。

  1. 连接
  2. 使用 RequestFactory
  3. 实体关系
  4. 验证实体

实体

实体是应用程序中的域类,具有持久标识的概念。一般来说,实体可以持久化到数据存储,例如关系数据库或 Google App Engine 数据存储。在像 JDO 和 JPA 这样的持久化框架中,实体使用 @Entity 进行注释。RequestFactory 不需要在您的域类上使用任何特定框架或注释。以下摘自 GWT 分发版中的 Expenses 示例应用程序 中的实体定义。

package com.google.gwt.sample.expenses.server.domain;

/**
 * The Employee domain object.
 */
@Entity
public class Employee {

  @Size(min = 3, max = 30)
  private String userName;

  private String department;

  @NotNull
  private String displayName;

  private String password;

  @JoinColumn
  private Long supervisorKey;

  @Id
  @Column(name = "id")
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Version
  @Column(name = "version")
  private Integer version;

  @Transient
  private Employee supervisor;

  public String getDepartment() {
    return department;
  }

  public String getDisplayName() {
    return this.displayName;
  }
  ...
}

实体代理

实体代理是服务器端实体的客户端表示。代理接口由 RequestFactory 实现,它们在 GWT-RPC 中充当 DTO(数据传输对象)的角色。RequestFactory 会自动在服务器上的实体之间以及客户端上相应的 EntityProxy 之间传播 bean 风格的属性。此外,EntityProxy 接口使 RequestFactory 能够计算并仅将更改(“增量”)发送到服务器。以下是与上面显示的 Employee 相对应的 EntityProxy。

@ProxyFor(Employee.class)
public interface EmployeeProxy extends EntityProxy {

  String getDepartment();

  String getDisplayName();

  Long getId();

  String getPassword();

  EmployeeProxy getSupervisor();

  String getUserName();

  void setDepartment(String department);

  void setDisplayName(String displayName);

  void setPassword(String password);

  void setSupervisor(EmployeeProxy supervisor);

  void setUserName(String userName);
}

实体代理只需扩展 EntityProxy 接口,并使用 @ProxyFor 或 @ProxyForName 注释引用要表示的服务器端实体。无需在 EntityProxy 中表示服务器端实体中的每个属性和方法,只需要要公开给客户端的属性的 getter 和 setter。请注意,虽然在本例中显示了 getId(),但大多数客户端代码将希望改为引用 EntityProxy.stableId(),因为此方法返回的 EntityProxyId 在所有与 RequestFactory 相关的类中使用。

另请注意,getSupervisor() 方法返回另一个代理类(EmployeeProxy)。所有客户端代码都必须引用 EntityProxy 子类。RequestFactory 会自动将代理类型转换为服务器上的相应实体类型。

值代理

值代理可用于表示任何类型。与 EntityProxy 不同,ValueProxy 不需要公开 ID 和版本。ValueProxy 通常用于表示实体内的嵌入对象类型。例如,联系管理应用程序中的 Person 实体可能将 Address 表示为嵌入类型,因此它将作为 person 实体内的序列化对象进行持久化。

@Entity
public class Person {
  @Id
  private Long id;
  private Integer version = 0;
  private String firstName, lastName;
  @Embedded
  private Address address;
  ...
}

Address 类型只是一个没有持久化注释的 POJO

public class Address {
  private String street1;
  private String street2;
  private String city;
  private String st;
  private String zip;
  ...
}

在客户端,Address 表示为 ValueProxy 并由包含的 EntityProxy 引用

public interface AddressProxy extends ValueProxy {
  public String getStreet1();
  public String getStreet2();
  public String getCity();
  public String getSt();
  public String getZip();
  ...
}

public interface PersonProxy extends EntityProxy {
  Long getId();
  Integer getVersion();
  String getFirstName();
  String getLastName();
  AddressProxy getAddress();
   ...
}

ValueProxy 可用于使用 RequestFactory 将任何类似 bean 的类型传递到服务器或从服务器传递到服务器。

RequestFactory 接口

与 GWT-RPC 一样,您可以通过扩展接口来定义客户端和服务器代码之间的接口。您可以为应用程序定义一个 RequestFactory 接口,它包含返回服务存根的方法。以下摘自 Expenses 示例应用程序

public interface ExpensesRequestFactory extends RequestFactory {

  EmployeeRequest employeeRequest();

  ExpenseRequest expenseRequest();

  ReportRequest reportRequest();

}

EmployeeRequest 服务存根如下所示

@Service(Employee.class)
public interface EmployeeRequest extends RequestContext {

  Request<Long> countEmployees();

  Request<Long> countEmployeesByDepartment(
      String department);

  Request<List<EmployeeProxy>> findAllEmployees();

  Request<EmployeeProxy> findEmployee(Long id);

  Request<List<EmployeeProxy>> findEmployeeEntries(int firstResult,
      int maxResults);

  Request<List<EmployeeProxy>> findEmployeeEntriesByDepartment(
      String department, int firstResult, int maxResults);

  InstanceRequest<EmployeeProxy, Void> persist();

  InstanceRequest<EmployeeProxy, Void> remove();

}

RequestFactory 服务存根必须扩展 RequestContext 并使用 @Service 或 @ServiceName 注释来命名服务器上关联的服务实现类。服务存根中的方法不会直接返回实体,而是返回 com.google.web.bindery.requestfactory.shared.Request 的子类。这允许以与向 GWT-RPC 中的每个服务方法传递 AsyncCallback 对象类似的方式,使用 Request.fire() 异步调用接口上的方法。

requestFactory.employeeRequest().findEmployee(employeeId).fire(
    new Receiver<EmployeeProxy>() {
      @Override
      public void onSuccess(EmployeeProxy employee) {
      ...
      }
    });

就像 GWT-RPC 调用者传递实现 onSuccess() 和 onFailure() 的 AsyncCallback 一样,Request.fire() 接受一个 Receiver,它必须实现 onSuccess()。请注意,Receiver 是一个抽象类,具有 onFailure() 的默认实现,因此您不必每次调用 Request 时都处理失败情况。要更改默认实现(它只是抛出 RuntimeException),您可以扩展 Receiver 并使用适用于应用程序的默认实现覆盖 onFailure()。您可能还想覆盖 onConstraintViolation() 的默认实现,该实现返回服务器上的任何约束冲突(请参阅下面的 验证实体)。

从每个方法返回的 Request 类型使用服务方法的返回值进行参数化。类型参数成为 Receiver 的 onSuccess() 方法期望的类型,如上面的示例所示。没有返回值的方法应返回类型 Request<Void>请求可以使用以下类型进行参数化

  • 内置值类型:BigDecimal、BigInteger、Boolean、Byte、Enum、Character、Date、Double、Float、Integer、Long、Short、String、Void
  • 自定义值类型:ValueProxy 的任何子类
  • 实体类型:EntityProxy 的任何子类
  • 集合:List<T>Set<T>,其中 T 是上述值或实体类型之一,或 Map<K,V>,其中 K 和 V 是上述值或实体类型之一

对实体本身进行操作的实例方法(如 persist() 和 remove())返回 InstanceRequest 类型的对象,而不是 Request。这将在下一节中进一步解释。

可传输类型

RequestFactory 限制了可以用作代理属性和服务方法参数的类型。总的来说,这些类型被称为可传输类型。每个客户端可传输类型都映射到服务器端域类型。映射规则如下

可传输类型到域类型的映射
客户端类型 域类型
基本类型(例如 `int`) 基本类型
包装基本类型(例如 `Integer`) 包装基本类型
其他值类型:Enums、BigInteger、BigDecimal、Date 其他值类型
`@ProxyFor(Foo.class) FooProxy extends EntityProxy` 一个 `Foo` 实体
`@ProxyFor(Bar.class) BarProxy extends ValueProxy` 一个 `Bar` 值对象
`Set` 或 `List` 其中 `T` 是可传输类型 `Set` 或 `List`
`Map` 其中 `K` 和 `V` 是可传输类型 `Map`

在确定代理或上下文类型中定义的客户端方法是否是域方法的有效映射时,客户端类型将转换为其域等效类型,并考虑常规的 Java 类型可分配性规则。

如果代理类型满足以下条件,则它将在客户端可用

  • 作为 Request 参数或返回值在 RequestContext 中引用。
  • 在引用的代理中引用。
  • 引用的代理的超类型,该超类型也是代理(即可分配给 EntityProxy 或 ValueProxy,并且具有 @ProxyFor(Name) 注释)。
  • 通过放置在 RequestFactory、RequestContext 或引用代理上的 @ExtraTypes 注解来引用。在 RequestFactory 或 RequestContext 上添加 @ExtraTypes 注解允许您将子类型添加到“其他人的”代理类型。

多态类型映射规则

  • 代理类型中定义或从超接口继承的所有属性都必须在域类型上可用。这允许代理接口扩展“mixin”接口。
  • 所有代理必须通过 @ProxyFor(Name) 注解映射到单个域类型。
  • 代理实例的 @ProxyFor 用于确定在服务器上实例化的具体类型。
  • 代理接口的任何超类型,只要可以分配给 EntityProxy 或 ValueProxy 并具有 @ProxyFor(Name) 注解,都必须是有效的代理。假设 BProxy 扩展了 AProxy:如果仅引用 BProxy(例如,通过 @ExtraTypes),则仍然允许创建 AProxy。
  • 代理接口之间的类型关系不需要映射的域类型之间有任何特定的类型关系。假设 BProxy 扩展了 AProxy:BEntity 不一定是 AEntity 的子类是可以接受的。这允许域对象与代理接口进行鸭子类型映射。
  • 要通过代理接口返回域对象,声明的代理返回类型必须映射到可分配给返回的域对象的域类型。
  • 特定的返回代理类型将是最派生的类型,可分配给声明的代理类型,并且也映射到返回的域类型或其超类型之一。

服务器实现

可以在服务器上以两种方式之一实现服务:作为类型中的静态方法,或作为服务类中的实例方法,并伴随一个 ServiceLocator。

在这两种情况下,服务接口中定义的方法都在 @Service 或 @ServiceName 注解中命名的类中实现。与 GWT-RPC 不同,服务实现不直接实现 RequestContext 接口。服务器端服务必须实现服务 RequestContext 接口中定义的每个方法,即使实现没有正式实现 RequestContext 接口。每个方法的名称和参数列表在客户端和服务器端相同,并遵循以下映射规则

  • 返回 Reques<T> 的客户端方法在服务器端只返回 T。例如,在客户端接口中返回 Request<String> 的方法在服务器端只返回 String。
  • EntityProxy 类型在服务器端变为域实体类型,因此返回 Request<List<EmployeeProxy>> 的方法在服务器端只返回 List<Employee>
  • 在客户端接口中返回 Request 对象的方法在服务类中实现为静态方法。或者,它们可以实现为 ServiceLocator 返回的服务对象中的实例方法。
  • 对实体实例进行操作的方法(如 persist() 和 remove())在客户端接口中返回一个 InstanceRequest 对象。实例方法不直接传递实例,而是通过 InstanceRequest 上的 using() 方法传递。在服务器端,实例方法必须实现为实体类型中的非静态方法。

RequestFactory servlet 为所有实体需要四个特殊方法。它们可以实现为实体本身,也可以实现为实现 Locator 接口的默认可实例化类型。所需方法在下面的表格中总结。

实体定位器方法可以在实体或 Locator 类中实现
方法直接在实体中`Locatorimpl`描述
构造函数 无参数构造函数 `T create(Classclazz)` 返回一个新的实体实例
getId id_type getId() I getId(T domainObject) 返回实体的持久化 ID,它可以是任何可传输的类型。ID 通常由持久化引擎(JDO、JPA、Objectify 等)自动生成。
按 ID 查找 static findEntity(id_type id) `T find(Classclazz, I id)` 返回 ID 的持久化实体。当直接在实体中实现时,此方法具有特殊的命名约定。在客户端,RequestFactory 接口(服务接口扩展的接口)中定义了一个 find() 方法。在服务器端,该方法命名为“find”加上类型的简单名称,例如 findEmployee,并接受实体 ID 类型的参数。
getVersion Integer getVersion() Object getVersion(T domainObject) 用于 RequestFactory 推断实体是否已更改。后备存储(JDO、JPA 等)负责在每次对象持久化时更新版本,RequestFactory 调用 getVersion() 来了解更改。此信息在两个地方使用。首先,如果实体由于服务器上的方法调用而发生更改,例如,当调用持久化可编辑实体导致服务器上的版本更新时,RequestFactoryServlet 会向客户端发送 UPDATE 事件。其次,客户端维护最近看到的实体的版本缓存。每当它看到版本已更改的实体时,它就会在事件总线上触发 UPDATE 事件,以便侦听器可以更新视图。

在实体类中实现服务

在映射服务时,在客户端接口中返回 Request 对象的方法实现为服务类中的静态方法,例如以下示例中的 Employee.findAllEmployees()。以下是 Expenses 示例项目中 Employee 实体的更多内容

// The Employee domain object
@Entity public class Employee {

// properties, getters, and setters omitted
  public static List<Employee> findAllEmployees() {
    EntityManager em = entityManager();
    try {
      List<Employee> list = em.createQuery("select o from Employee o").getResultList();
      // force to get all the employees
      list.size();
      return list;
    } finally {
      em.close();
    }
  }

  public static Employee findEmployee(Long id) {
    if (id == null) {
      return null;
    }
    EntityManager em = entityManager();
    try {
      Employee employee = em.find(Employee.class, id);
      return employee;
    } finally {
      em.close();
    }
  }

  public static final EntityManager entityManager() {
    return EMF.get().createEntityManager();
  }

  public void persist() {
    EntityManager em = entityManager();
    try {
      em.persist(this);
    } finally {
      em.close();
    }
  }

  public void remove() {
    EntityManager em = entityManager();
    try {
      Employee attached = em.find(Employee.class, this.id);
      em.remove(attached);
    } finally {
      em.close();
    }
  }

  ...

}

使用 Locator 和 ServiceLocator

如果您不想在实体本身中实现持久化代码怎么办?要实现所需的实体定位器方法,请创建一个扩展 Locator<T,I> 的实体定位器类

public class EmployeeLocator extends Locator<Employee, Long> {
  @Override
  public Employee create(Class<? extends Employee> clazz)
  {
    return new Employee();
  }
    ...
}

然后在 @ProxyFor 注解中将其与实体关联

@ProxyFor(value = Employee.class, locator = EmployeeLocator.class)
  public interface EmployeeProxy extends EntityProxy {
    ...
  }

由于许多持久化框架提供通用的 find/get/query 方法,因此也可以创建一个通用 Locator 类,并在每个实体类型的 @ProxyFor 注解中指定它。为此,您的所有实体都可以扩展一个基类,该基类提供 getId() 和 getVersion()。或者,通用 Locator 可以使用反射在需要时调用 getId() 和 getVersion()。

许多持久化框架还使得利用通用 DAO 类成为可能,其中实体 DAO 扩展通用 DAO 并根据需要重写方法。RequestFactory 的优势之一是可以将 DAO 类直接公开为服务。但是,如果服务实现为实体类中的静态方法,则从基类继承服务方法不起作用。幸运的是,您可以使用 ServiceLocator 来告诉 RequestFactory 如何获取服务实例。使用 ServiceLocator 时,RequestFactory 将调用返回 Request 类型的实例方法,而不是静态方法。

要使用 ServiceLocator,只需实现 ServiceLocator 接口。它可能像这样简单

public class MyServiceLocator implements ServiceLocator {
  @Override
  public Object getInstance(Class<?> clazz) {
    try {
      return clazz.newInstance();
    } catch (InstantiationException e) {
      throw new RuntimeException(e);
    } catch (IllegalAccessException e) {
      throw new RuntimeException(e);
    }
  }
}

然后用服务名称和定位器类注释服务接口

@Service(value = EmployeeDao.class, locator = MyServiceLocator.class)
interface EmployeeRequestContext extends RequestContext

注意:RequestFactory 缓存 ServiceLocator 和服务实例,因此请确保两者都是线程安全的。

将所有内容整合在一起

连接

为了使用 RequestFactory,请将以下行添加到您的 .gwt.xml 中

<inherits name='com.google.web.bindery.requestfactory.RequestFactory' />

将以下 jar 文件添加到您的 WEB-INF/lib 目录中

  • requestfactory-server.jar
  • javax/validation/validator-api-1.0.0.GA.jar
  • 您选择的 JSR 303 验证器,例如 hibernate-validator

在 web.xml 中映射 RequestFactoryServlet

<servlet>
    <servlet-name>requestFactoryServlet</servlet-name>
    <servlet-class>com.google.web.bindery.requestfactory.server.RequestFactoryServlet</servlet-class>
    <init-param>
        <param-name>symbolMapsDirectory</param-name>
        <!-- You'll need to compile with -extras and move the symbolMaps directory
            to this location if you want stack trace deobfuscation to work -->
        <param-value>WEB-INF/classes/symbolMaps/</param-value>
    </init-param>
</servlet>

<servlet-mapping>
    <servlet-name>requestFactoryServlet</servlet-name>
    <url-pattern>/gwtRequest</url-pattern>
</servlet-mapping>

创建实体、EntityProxy 类型和 RequestFactory 及其服务接口后,可以使用 GWT.create() 将其变为现实,并使用应用程序的 EventBus 初始化它

final EventBus eventBus = new SimpleEventBus();
requestFactory = GWT.create(ExpensesRequestFactory.class);
requestFactory.initialize(eventBus);

使用 RequestFactory

现在我们准备让 RequestFactory 投入工作。要在客户端创建新实体,请在 EntityProxy 类型上调用 RequestContext.create(),然后使用实体服务接口中定义的方法将其持久化。使用上面的示例代码来创建一个新的 Employee 对象并将其持久化到数据库,我们将编写

EmployeeRequest request = requestFactory.employeeRequest();
EmployeeProxy newEmployee = request.create(EmployeeProxy.class);
newEmployee.setDisplayName(...);
newEmployee.setDepartment(...);
...
Request<Void> createReq = request.persist().using(newEmployee);

所有客户端代码都应该使用 EmployeeProxy,而不是 Employee 实体本身。这样,域对象就不必与 GWT 兼容,与 GWT-RPC 不同,GWT-RPC 在客户端和服务器端使用相同的具体类型。EmployeeProxy 没有构造函数,因为它是一个接口,而不是一个类,因此您必须使用 requestContext.create(EmployeeProxy.class) 对其进行实例化。这种方法的好处是它允许 RequestFactory 跟踪对象创建和修改,因此它只能将更改发送到服务器。

请注意,persist() 方法没有参数,这与 Employee 实体中方法的实现一致。在 EmployeeRequest 接口中,它返回类型 InstanceRequest,而 InstanceRequest 又从 using() 方法中获取方法将调用的实例,如上所示。或者,如果使用 ServiceLocator,则可以将 persist() 方法声明为 Requestpersist(Employee emp),在这种情况下,newEmployee 将作为参数传递给 persist() 方法,以代替 using() 方法。

现在让我们添加代码将新创建的员工保存到服务器

createReq.fire(new Receiver<Void>()
{
  @Override
    public void onSuccess(Void arg0)
    {
        // Update display
    }
});

我们使用 Receiver 触发请求,请求完成后会回调我们。

从 RequestContext.create() 中未返回的任何对象(例如从服务器接收的那些对象)必须通过调用 RequestFactory 的 edit() 方法启用更改。从可编辑代理的 getter 返回的任何 EntityProxy 也是可编辑的。

EmployeeProxy editableEmployee = request.edit(returnedEmployee);
editableEmployee.setDepartment(newDepartment);
...
Request<Void> updateReq = request.persist().using(editableEmployee);

edit() 方法返回不可变对象的副本,原始对象可以丢弃。要将更改发送到服务器,您需要使用服务接口方法中定义的方法创建一个新的服务器请求,并像上面那样触发它。所有编辑都在特定上下文中发生,当上下文触发时,所有这些编辑都会被发送,因此任何方法调用都会导致更改被发送到服务器。

实体关系

对相关实体的更改可以在单个请求中持久化。例如,来自 GWT 主干中的 DynatableRF 示例应用程序 的此代码同时创建了一个新的 Person 和 Address

PersonRequest context = requestFactory.personRequest();
AddressProxy address = context.create(AddressProxy.class);
PersonProxy person = context.create(PersonProxy.class);
person.setAddress(address);
context.persist().using(person).fire(...);

RequestFactory 自动在单个请求中发送整个对象图。在这种情况下,服务器上 Person.persist() 的实现负责持久化相关的 Address,这可能自动发生,也可能不会自动发生,具体取决于 ORM 框架以及关系的定义方式。请注意,RequestFactory 目前不支持嵌入对象(各种 ORM 框架中的 @Embedded),因为它期望每个实体都独立存在,并具有自己的 ID。

查询服务器时,RequestFactory 不会自动填充对象图中的关系。要做到这一点,请在请求上使用 with() 方法,并指定相关属性名称作为字符串

Request<Person> findReq = requestFactory.personRequest().find(personId).with("address");

还需要使用 with() 方法来检索类型扩展 ValueProxy 的任何属性。with() 方法接受多个 String 参数,因此您可以一次指定多个属性名称。要指定嵌套属性,请使用点符号。将所有内容整合在一起,您可能拥有

Request<Person> findReq = find(personId).with("phone","address.city","address.zip")

验证实体

RequestFactory 支持 JSR 303 Bean 验证。这使得在服务器上保留验证规则并通知客户端实体因验证失败而无法持久化成为可能。要使用它,请确保服务器类路径中存在 JSR 303 实现,并使用 java.validation 注解(如 @Size 和 @NotNull)对您的实体进行注释,如上面的 Employee 实体所示。在调用服务器上的服务方法之前,RequestFactory 将调用验证框架并将来自服务器的任何 ConstraintViolations 发送到客户端,然后客户端将调用请求的 Receiver 上的 onViolation() 方法。

结论

RequestFactory 是 GWT 2.1 中新“Bindery”功能的核心。在以后的文章中,我们将探讨与单元小部件、编辑器、事件总线以及活动和位置的集成。