GWT & Hibernate

Sumit Chandel,Google 开发者关系

2009 年 7 月

(感谢 Bruno Marchesson 对本文的贡献)

许多开发者询问如何将 GWTHibernate 结合使用。尽管您可以在 GWT 开发者论坛 上找到有关此主题的大量讨论,但我们认为总结一些最受欢迎的策略,并在 GWT 应用程序开发的背景下突出其优点和缺点将会有所帮助。

在深入研究集成策略之前,让我们先熟悉一下基础知识。

基础知识

为了本文的目的,我们将假设读者已经熟悉 Hibernate 在服务器端的配置和使用。如果您想了解有关 Hibernate 及其工作原理的更多信息,强烈建议您阅读 Hibernate 主页上发布的入门教程。

此外,为了让我们的 Hibernate 对象持久化,我们将使用 HSQLDB,它提供了一个内存中的 Hibernate SQL 数据库。您可以在 此处 下载它。同样,我们不会过多地讨论 HSQLDB 的工作原理,但我们会介绍足够的内容,使其能够在我们将看到的示例中运行。

让我们从一个简单的示例开始。假设我们正在开发一个在线音乐唱片商店。此类应用程序中一个明显的域对象将是 Record 对象,我们希望将其持久化到服务器端并在客户端显示。我们还希望用户能够创建帐户并将音乐唱片添加到他们的在线资料中。为我们创建一个 Account 对象也是有道理的,我们希望将其持久化。因此,让我们创建这两个类。

Record.java

public class Record implements Serializable {
  private Long id;
  private String title;
  private int year;
  private double price;

  public Record() {
  }

  public Record(Long id) {
    this.id = id;
  }

  // Along with corresponding getters + setters.
}

Account.java

public class Account implements Serializable {
  Long id;
  String name;
  String password;
  Set<Record> records;

  public Account() {
  }

  public Account(Long id) {
    this.id = id;
  }

  public void addRecord(Record record) {
    if (records == null) {
      records = new HashSet<Record>();
    }
    records.add(record);
  }

  public void removeRecord(Record record) {
    if (records == null) {
      return;
    }
    records.remove(record);
  }

  // Along with corresponding getters + setters.
}

然后,我们需要为每个持久化类型创建相应的 Hibernate 映射文件,如下所示

Record.hbm.xml

<hibernate-mapping>
  <class name="com.google.musicstore.domain.Record" table="RECORD">
    <id name="id" column="RECORD_ID">
      <generator class="native"/>
    </id>
    <property name="title"/>
    <property name="year"/>
    <property name="price"/>

  </class>
</hibernate-mapping>

Account.hbm.xml

<hibernate-mapping>
  <class name="com.google.musicstore.domain.Account" table="ACCOUNT">
    <id name="id" column="ACCOUNT_ID">
      <generator class="native"/>
    </id>
    <property name="name"/>
    <property name="password"/>

    <set name="records" table="ACCOUNT_RECORD" lazy="true">
      <key column="ACCOUNT_ID"/>
      <many-to-many column="RECORD_ID" class="com.google.musicstore.domain.Record"/>
    </set>
  </class>
</hibernate-mapping>

现在我们已经创建了持久化类,让我们创建一个简单的 UI,它将允许我们输入新的帐户和唱片,以及将它们持久化到服务器端的 GWT RPC 服务。让我们从 RPC 服务开始。

我们不会详细介绍每个 RPC 组件在此处所起的作用,但如果您不熟悉 GWT RPC 子系统,请查看 GWT RPC 文档 以了解详细信息。

首先,我们创建客户端服务接口。如果您想避免下面列出的大量接口方法,请考虑使用命令模式,如 此处 所述。

MusicStoreService.java

@RemoteServiceRelativePath("musicservice")
public interface MusicStoreService extends RemoteService {
  public List<Account> getAccounts();

  public List<Record> getRecords();

  public Long saveAccount(Account account);

  public Long saveRecord(Record record);

  public void saveRecordToAccount(Account account, Record record);
}

MusicStoreServiceAsync.java

public interface MusicStoreServiceAsync {
  public void getAccounts(AsyncCallback<List<Account>> callback);

  public void getRecords(AsyncCallback<List<Record>> callback);

  public void saveAccount(Account accountDTO, AsyncCallback<Long> callback);

  public void saveRecord(Record record, AsyncCallback<Long> callback);

  public void saveRecordToAccount(Account accountDTO, Record recordDTO,
AsyncCallback<Void> callback);
}

最后,我们在服务器端创建服务实现类。

MusicStoreServiceImpl.java

public class MusicStoreServiceImpl extends RemoteServiceServlet implements
MusicStoreService {

  @Override
  public List<Account> getAccounts() {
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    List<Account> accounts = new ArrayList<Account>(session.createQuery("from Account").list());
    session.getTransaction().commit();
    return accounts;
  }

  @Override
  public List<Record> getRecords() {
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    List<Record> records = new ArrayList<Record>(session.createQuery("from Record").list());
    session.getTransaction().commit();
    return records;
  }

  @Override
  public Long saveAccount(Account account) {
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    session.save(account);
    session.getTransaction().commit();
    return account.getId();
  }

  @Override
  public Long saveRecord(Record record) {
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    session.save(record);
    session.getTransaction().commit();
    return record.getId();
  }

  @Override
  public void saveRecordToAccount(Account account, Record record) {
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    account = (Account) session.load(Account.class, account.getId());
    record = (Record) session.load(Record.class, record.getId());
    account.addRecord(record);
    session.save(account);
    session.getTransaction().commit();
  }
}

您可能已经注意到上面代码片段中 MusicStoreServiceImpl 方法实现中有一些 HibernateUtil 调用。这实际上是一个自定义类,作为帮助程序实用程序创建,用于检索和使用 Hibernate 会话工厂,就像之前提到的 Hibernate 教程中所做的那样。为了方便起见,以下是 HibernateUtil 代码,粘贴在下面以便您能够理解。如果您想了解有关 HibernateUtil 类正在做什么的更多详细信息,我强烈建议您查看教程以获得完整的解释。

public class HibernateUtil {

  private static final SessionFactory sessionFactory;

  static {
    try {
      // Create the SessionFactory from hibernate.cfg.xml
      sessionFactory = new Configuration().configure().buildSessionFactory();
    } catch (Throwable ex) {
      // Make sure you log the exception, as it might be swallowed
      System.err.println("Initial SessionFactory creation failed." + ex);
      throw new ExceptionInInitializerError(ex);
    }
  }

  public static SessionFactory getSessionFactory() {
    return sessionFactory;
  }
}

最后,我们的服务器端 GWT RPC 服务已准备好对我们的 Hibernate 对象执行 CRUD 操作(实际上,我们跳过了删除功能)。现在我们只需要一个接口来实际进行 RPC 调用。我已经创建了一个示例应用程序,其中包含一个 UI,它将允许我们添加唱片,添加帐户,将唱片添加到帐户,当然还有查看所有现有帐户及其关联的唱片。示例代码并不代表最佳实践,只是一个快速简单的实现,让我们能够快速运行。该示例还包含我们迄今为止已完成的服务器端 RPC 和 Hibernate 代码。您可以在 此处 下载该示例。

在示例源代码中,您将在根目录中找到一个 build.xml 文件和一个 build.properties 文件。在为您的机器正确配置 gwt.homegwt.dev.jar 属性后,您可以使用 Ant 来构建项目,以及启动托管模式以查看 UI 和我们嵌入式 Jetty 服务器中设置的 Hibernate 实例。只需从命令行运行以下命令即可

ant build hosted

但是,在此之前,我们需要启动内存中的 HSQLDB,以便我们可以持久化我们的 Hibernate 对象。在您下载的示例项目中,您应该在项目根目录下找到一个名为“data”的文件夹。您还将在 lib 文件夹中找到 hsqldb.jar。我们要做的就是调用 hsqldb.jar 文件中包含的 org.hsqldb.Server 类,同时位于“data”目录中以托管 HSQLDB 属性和日志输出。您可以通过从命令行(位于“data”目录中)运行以下命令来完成此操作。

java -cp ../lib/hsqldb.jar org.hsqldb.Server

现在我们已经准备好了持久化层,让我们使用上面的 ant 命令编译并运行我们的应用程序,使其处于托管模式。一旦 buildhosted ant 任务都完成,您应该会看到托管模式浏览器启动,并显示“添加帐户/唱片”选项卡。最后,我们可以开始持久化我们的唱片(从 GWT 客户端到 Hibernate 数据库,使用我们的 Hibernate 对象!)。继续尝试添加一个帐户和一个唱片到我们内存中的 Hibernate,以开始我们的数据集。

接下来,尝试选择“将唱片添加到帐户”面板,将我们新创建的唱片添加到也新创建的帐户中。很有可能,您会收到类似于以下屏幕截图的错误消息。

img

为什么 Hibernate 对象在到达浏览器世界时无法理解

那么哪里出错了呢?查看托管模式控制台,您会注意到控制台中记录了警告消息“在调度传入 RPC 调用时出现异常”。选择警告消息,下部窗格将显示一个相当长的堆栈跟踪。

这是需要注意的部分

Caused by: com.google.gwt.user.client.rpc.SerializationException: Type 'org.hibernate.collection.PersistentSet' was not included in the set of types which can be serialized by this SerializationPolicy or its Class object could not be loaded. For security purposes, this type will not be serialized.
at com.google.gwt.user.server.rpc.impl.StandardSerializationPolicy.validateSerialize(StandardSerializationPolicy.java:83)
at com.google.gwt.user.server.rpc.impl.ServerSerializationStreamWriter.serialize(ServerSerializationStreamWriter.java:591)

这里关键在于当我们尝试加载并检索帐户时抛出的 SerializationException

那么到底哪里出错了呢?正如您可能在 GWT RPC 文档 中阅读到的那样,每当通过 RPC 传输的类型不可“序列化”时,就会抛出 SerializationException。这里的可序列化定义意味着 GWT RPC 机制知道如何将类型从字节码序列化和反序列化为 JSON,反之亦然。要将类型声明为可序列化到 GWT 编译器,您既可以使要通过 RPC 传输的类型实现专门为此目的创建的 IsSerializable 接口,也可以实现标准的 java.io.Serializable 接口,前提是其成员和方法包含也是可序列化的类型。

AccountRecord Hibernate 对象的情况下,我们正在实现 Serializable 接口,因此它们应该可以工作,对吧?事实证明,魔鬼藏在细节中。

当您获取一个对象并将其转换为 Hibernate 对象时,该对象现在已增强为可持久化。这种持久化并非没有对对象的某种检测。在 Hibernate 的情况下,Javassist 库实际上通过持久化实体替换并重写了这些对象的字节码,以使 Hibernate 的魔力发挥作用。这对 GWT RPC 意味着,当对象准备好通过网络传输时,它实际上并不是编译器认为将要传输的相同对象,因此在尝试反序列化时,GWT RPC 机制不再知道类型是什么,并拒绝反序列化它。

事实上,如果您要更深入地了解之前对 loadAccounts() 的调用,并深入到 RPC.invokeAndEncodeResponse() 方法,您会发现我们尝试反序列化的对象现在已成为一个 Account 类型的 ArrayList,其 java.util.Set 记录已被 org.hibernate.collection.PersistentSet 类型替换。

在其他持久层框架(如 JDO 或 JPA)上使用 Google App Engine 时,也会出现类似的问题。

一种可能的解决方案是在通过服务器端 RPC 调用返回之前,再次以相反的方向替换类型。这是可行的,并且可以解决我们在此遇到的问题,但我们还没有完全摆脱困境。使用 Hibernate 的另一个巨大优势是,我们可以在需要时延迟加载关联对象。例如,在服务器端,我可以加载一个帐户,对其进行更改,并且只有在调用 account.getRecords() 并对这些记录执行某些操作时才加载其关联记录。特殊的 Hibernate 监控将负责在我进行调用时实际获取记录,使其仅在真正需要时可用。

正如您可能想象的那样,这将转化为 GWT RPC 世界中的奇怪行为,在这种世界中,这些 Hibernate 对象从 Java 服务器端传输到浏览器端。如果 GWT RPC 服务尝试延迟访问关联,您可能会看到类似于 LazyInitializationException 的异常被抛出。

集成策略

幸运的是,有一些解决方法可以规避这些问题,并提供这些方法固有的其他优势。

使用数据传输对象

处理此问题的最简单方法之一是在繁重的 Hibernate 对象与其在客户端关心的数据表示之间引入一个轻量级对象。这个中间对象通常被称为 数据传输对象 (DTO)

DTO 是一个简单的 POJO,它只包含我们可以访问的简单数据字段,以便在客户端应用程序页面上显示。然后可以使用我们数据传输对象中的数据构造 Hibernate 对象。DTO 本身将只包含我们想要持久化的数据,但不包含由 Hibernate Javassist 添加到其 Hibernate 对应对象的任何延迟加载或持久化逻辑。

将此应用于我们的示例,我们得到以下两个 DTO

AccountDTO.java

package com.google.musicstore.client.dto;

import java.io.Serializable;
import java.util.Set;

public class AccountDTO implements Serializable {
  private Long id;
  private String name;
  private String password;
  private Set<RecordDTO> records;

  public AccountDTO() {
  }

  public AccountDTO(Long id) {
    this.id = id;
  }

  public AccountDTO(Long id, String name, String password,
      Set<RecordDTO> records) {
    this.id = id;
    this.name = name;
    this.password = password;
    this.records = records;
  }

  // Along with corresponding getters + setters.
}

RecordDTO.java

package com.google.musicstore.client.dto;

import java.io.Serializable;

public class RecordDTO implements Serializable {
  private Long id;
  private String title;
  private int year;
  private double price;

  public RecordDTO() {
  }

  public RecordDTO(Long id) {
  this.id = id;
  }

  public RecordDTO(Long id, String title, int year, double price) {
    this.id = id;
    this.title = title;
    this.year = year;
    this.price = price;
  }

  // Along with corresponding getters + setters.
}

接下来,让我们添加以这些新的 DTO 为参数的构造函数,以对应于它们的 Hibernate 对象。

Account.java

public Account(AccountDTO accountDTO) {
  id = accountDTO.getId();
  name = accountDTO.getName();
  password = accountDTO.getPassword();
  Set<RecordDTO> recordDTOs = accountDTO.getRecords();
  if (recordDTOs != null) {
    Set<Record> records = new HashSet<Record>(recordDTOs.size());
    for (RecordDTO recordDTO : recordDTOs) {
      records.add(new Record(recordDTO));
    }
    this.records = records;
  }
}

Record.java

public Record(RecordDTO record) {
  id = record.getId();
  title = record.getTitle();
  year = record.getYear();
  price = record.getPrice();
}

最后,我们需要修改现有的 GWT RPC 组件,以将 DTO 对应对象作为参数。

MusicStoreService.java

@RemoteServiceRelativePath("musicservice")
public interface MusicStoreService extends RemoteService {
  public List<AccountDTO> getAccounts();

  public List<RecordDTO> getRecords();

  public Long saveAccount(AccountDTO accountDTO);

  public Long saveRecord(RecordDTO recordDTO);

  public void saveRecordToAccount(AccountDTO accountDTO, RecordDTO recordDTO);

  public List<AccountDTO> getAllAccountRecords();
}

MusicStoreServiceAsync.java

public interface MusicStoreServiceAsync {
  public void getAccounts(AsyncCallback<List<AccountDTO>> callback);

  public void getRecords(AsyncCallback<List<RecordDTO>> callback);

  public void saveAccount(AccountDTO accountDTO, AsyncCallback<Long> callback);

  public void saveRecord(RecordDTO record, AsyncCallback<Long> callback);

  public void saveRecordToAccount(AccountDTO accountDTO, RecordDTO recordDTO, AsyncCallback<Void> callback);

  public void getAllAccountRecords(AsyncCallback<List<AccountDTO>> callback);
}

现在我们修改 MusicStoreServiceImpl.java 类。

MusicStoreServiceImpl.java

public class MusicStoreServiceImpl extends RemoteServiceServlet implements
MusicStoreService {

  @Override
  public List<AccountDTO> getAccounts() {
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    List<Account> accounts = new ArrayList<Account>(session.createQuery("from Account").list());
    List<AccountDTO> accountDTOs = new ArrayList<AccountDTO>(
    accounts != null ? accounts.size() : 0);
    if (accounts != null) {
      for (Account account : accounts) {
        accountDTOs.add(createAccountDTO(account));
      }
    }
    session.getTransaction().commit();
    return accountDTOs;
  }

  @Override
  public List<RecordDTO> getRecords() {
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    List<Record> records = new ArrayList<Record>(session.createQuery("from Record").list());
    List<RecordDTO> recordDTOs = new ArrayList<RecordDTO>(records != null ? records.size() : 0);
    if (records != null) {
      for (Record record : records) {
        recordDTOs.add(createRecordDTO(record));
      }
    }
    session.getTransaction().commit();
    return recordDTOs;
  }

  @Override
  public Long saveAccount(AccountDTO accountDTO) {
    Account account = new Account(accountDTO);
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    session.save(account);
    session.getTransaction().commit();
    return account.getId();
  }

  @Override
  public Long saveRecord(RecordDTO recordDTO) {
    Record record = new Record(recordDTO);
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    session.save(record);
    session.getTransaction().commit();
    return record.getId();
  }

  @Override
  public void saveRecordToAccount(AccountDTO accountDTO, RecordDTO recordDTO) {
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    Account account = (Account) session.load(Account.class, accountDTO.getId());
    Record record = (Record) session.load(Record.class, recordDTO.getId());
    account.addRecord(record);
    session.save(account);
    session.getTransaction().commit();
  }

  @Override
  public List<AccountDTO> getAllAccountRecords() {
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    List<Account> accounts = new ArrayList<Account>(session.createQuery("from Account").list());
    List<AccountDTO> accountDTOs = new ArrayList<AccountDTO>(accounts != null ? accounts.size() : 0);
    if (accounts != null) {
      for (Account account : accounts) {
        accountDTOs.add(createAccountDTO(account));
      }
    }
    session.getTransaction().commit();
    return accountDTOs;
  }

  private AccountDTO createAccountDTO(Account account) {
    Set<Record> records = account.getRecords();
    Set<RecordDTO> recordDTOs = new HashSet<RecordDTO>(records != null ? records.size() : 0);
    if (records != null) {
      for (Record record : records) {
        recordDTOs.add(createRecordDTO(record));
      }
    }
    return new AccountDTO(account.getId(), account.getName(), account.getPassword(), recordDTOs);
  }

  private RecordDTO createRecordDTO(Record record) {
    return new RecordDTO(record.getId(), record.getTitle(), record.getYear(), record.getPrice());
  }
}

最后,我们需要更新来自 MusicStore 入口点类的 RPC 服务接口调用,以使用新的 DTO 参数化方法签名。

现在,我们有了包含 AccountRecord 类的域包,它位于正确的位置,并且隔离在服务器端。现在,我们可以从基本应用程序模块 XML 文件中删除引用域包的 <source> 标签

MusicStore.gwt.xml

<!-- Remove the line below -->
<source path="domain"/>

请注意,这里没有太多变化。在从数据库中检索 Hibernate 对象后,我们唯一需要做的就是将它们复制到它们对应的 DTO 中,将这些 DTO 添加到列表中,并在客户端回调中将它们返回。但是,有一件事我们需要注意,那就是我们将这些对象复制到 DTO 的过程。

我们创建了 createAccountDTO(Account account) 方法,该方法包含我们要将 Account Hibernate 对象转换为我们即将返回的仅数据 DTO 的逻辑。

createAccountDTO(Account account)

private AccountDTO createAccountDTO(Account account) {
  Set<Record> records = account.getRecords();
  Set<RecordDTO> recordDTOs = new HashSet<RecordDTO>(records != null ? records.size() : 0);
  if (records != null) {
    for (Record record : records) {
      recordDTOs.add(createRecordDTO(record));
    }
  }
  return new AccountDTO(account.getId(), account.getName(), account.getPassword(), recordDTOs);
}

您还会注意到,我们正在调用另一个名为 createRecordDTO(Record record) 的复制方法。正如您可能想象的那样,就像我们需要将 Account 对象转换为它们的 DTO 等价物一样,我们也需要对 Record 对象进行相同的定向转换。

createRecordDTO(Record record)

private RecordDTO createRecordDTO(Record record) {
  return new RecordDTO(record.getId(), record.getTitle(), record.getYear(), record.getPrice());
}

实现 DTO 解决方案后,尝试从命令行再次运行:ant clean build hosted 以查看解决方案的实际效果(同时确保 HSQL 内存数据库仍在运行)。

您可以 此处下载第一个示例应用程序的版本,该应用程序完全实现了 DTO 解决方案。

何时使用 DTO 方法

您可以看到这一点。我们需要转换的 Hibernate 对象越多,我们需要创建的特殊复制方法就越多,才能将它们通过网络传输。更重要的是,因为我们通过网络传输的 DTO 不会像 Hibernate 对象那样加载完整的对象图,所以在某些情况下,我们需要仔细考虑如何将 Hibernate 对象复制到 DTO,以及我们希望在通过网络发送时它和其关联对象有多完整,以及我们希望将此操作留给另一个 RPC 调用。使用 DTO 方法,所有这些都必须在代码中处理。

虽然这种方法也有一些优点。首先,我们现在有了可以发送到客户端的轻量级数据传输对象,这导致更轻的有效载荷。此外,通过强迫我们考虑复制策略,因为我们正在从服务器获取完整的 Hibernate 对象图,并针对客户端在给定时间需要看到的内容对其进行优化,我们降低了浏览器在具有五千条记录的帐户中崩溃的风险,并且还使用户体验更快。

我们创建的 DTO 可能会根据应用程序的服务器端架构加倍甚至三倍使用。例如,像 Java 消息服务这样的东西可能不会知道如何处理作为消息传递的 Hibernate 对象。现在可以使用 DTO 来代替传递,这在相关的 JMS 组件中更容易处理。

也就是说,如果您有很多需要转换的 Hibernate 对象,DTO/复制方法创建过程可能很麻烦。值得庆幸的是,还有其他策略可以帮助解决这种情况。

使用 Dozer 进行 Hibernate 集成

Dozer 是一个开源库,它可以通过读取和解析 XML 文件来自动为我们生成 DTO,从而减轻开发人员的手动创建这些文件的负担。我们可以使用 Dozer 来克隆我们的 Hibernate 实体。

首先,简单介绍一下 Dozer 的工作原理。Dozer 基于 Java Bean 规范,并使用它将数据从持久实体复制到新的 POJO 实例。

如前所述,Dozer 允许我们使用 XML 映射来告诉它要将哪些属性复制到新的 DTO 实例,以及要排除哪些属性。当 Dozer 读取这些映射文件并将对象复制到 DTO 时,它是隐式执行的,这意味着您可以预期映射文件中未明确排除的任何属性都将被包含。这有助于保持 Dozer 映射文件简洁明了

<mappings>
  <mapping>
    <class-a>com.google.musicstore.domain.Account</class-a>
    <class-b>com.google.musicstore.dto.AccountDTO</class-a>
  </mapping>
</mappings>

现在我们已经了解了 Dozer 映射的工作原理,让我们看看它们如何应用于 Hibernate 对象。想法是简单地将所有属性复制到我们的 DTO,同时删除任何标记为 lazy="true" 的属性。Dozer 将负责将这些最初延迟加载的持久集合替换为普通集合,这些集合可以通过 RPC 传输和序列化。

<mappings>
  <mapping>
    <class-a>com.google.musicstore.domain.Account</class-a>
    <class-b>com.google.musicstore.dto.AccountDTO</class-b>
    <field-exclude>
      <a>records</a>
      <b>records</b>
    </field-exclude>
  </mapping>

  <mapping>
    <class-a>com.google.musicstore.domain.Record</class-a>
    <class-b>com.google.musicstore.dto.RecordDTO</class-b>
  </mapping>
</mappings>

Dozer 的一个优点是,它会自动处理在两个类之间复制数据的操作,这两个类都具有相同字段类型和名称的属性。由于 Account/Record 对象及其 DTO 等价物都使用相同的属性名称,因此我们已经完成了上面配置的 Dozer 映射。将此映射文件保存到 dozerBeanMapping.xml 中,并将其放置在项目的类路径上。现在,我们需要让之前的 DTO 解决方案使用 Dozer,只需删除我们添加的复制逻辑(因为它不再需要),并使用 Dozer 映射将我们的 Hibernate 数据复制到 DTO,然后通过网络发送。

所有三个 GWT RPC MusicStore 服务组件的方法签名保持不变。改变的只是 MusicStoreServiceImpl 方法实现中从 Hibernate 对象到 DTO 的复制逻辑。在以前有 createAccountDTO()createRecordDTO() 调用的任何地方,我们现在将有

DozerBeanMapperSingletonWrapper.getInstance().map(account, AccountDTO.class));
// or
DozerBeanMapperSingletonWrapper.getInstance().map(record, RecordDTO.class));

同样地,当我们需要从传入的 DTO 创建 Hibernate 对象时,反之亦然。

您会注意到,这种方法迫使我们进行单独的调用以加载 Account 对象的记录,因为我们不再编写和使用自己的复制逻辑。这种传递逻辑并不一定很糟糕,因为我们想要在服务器上延迟加载属性的原因可能在客户端仍然成立。当我们确实想要复制具有 lazy="true" 的属性而不遇到 LazyInitializationExceptions 时,Dozer 确实允许我们创建自己的自定义转换器 - 这些类决定如何将一种类型复制到另一种类型。这与 DTO 方法类似,只是现在我们所有的复制逻辑都整齐地重构到专门的转换器类中。

尝试使用 Dozer 解决方案的示例应用程序,再次从命令行运行:ant clean build hosted。您可以 此处下载第一个示例应用程序的版本,该应用程序完全实现了 Dozer 解决方案。

何时使用 Dozer 方法

这种方法也有一些与 DTO 方法相同的缺点。您将需要创建一个 DTO 或其他类型的 RPC 可传输类,Dozer 映射器可以将 Hibernate 对象数据复制到该类。您还需要为每个需要在通过 RPC 传输数据时复制到 DTO 的 Hibernate 对象创建 <mapping> 条目。

使用 Dozer 的另一个缺点是,对于数据在其属性中深度嵌套的类型,DTO 的查找和生成可能需要一段时间,尤其是在我们正在映射许多这些实体的情况下。自定义转换器可以在这里提供帮助,但随着对象数量的不断增加,它们可能变得很麻烦。

但是,Dozer 方法为 DTO 的生成添加了一些不错的自动化功能,并且与我们之前手动生成它们相比节省了一些时间。我们节省了很多复制逻辑,这些逻辑会使我们的代码变得更臃肿,并且缺乏凝聚力。它还使我们能够配置控制哪些属性被复制,这有助于确保我们不会将包含多余数据的对象发送回客户端。

因此,如果您的项目包含许多需要通过 RPC 传输的 Hibernate 对象,并且还包含可以被 Dozer XML 映射捕获的简单复制逻辑,那么这可能是一种适合您的方法。另一方面,当延迟加载的属性导致您的加载策略一直变化到客户端层时,这破坏了分层的优势,并且当对象数量开始急剧增加时,也许下面描述的 Gilead 方法将是最佳选择。

使用 Gilead 进行 Hibernate 集成

Gilead(以前称为 Hibernate4Gwt)是 一个开源库,它提出了另一种更透明的解决方案,用于在 Hibernate 和 GWT 之间交换对象。

工作原理

Gilead 的第一个原则是自动将未初始化的代理替换为 null,并将持久集合替换为模拟 JRE 中的基本集合。这些替换都在不需要定义任何特定映射的情况下完成。Gilead 还会将重新创建 Hibernate 代理或持久集合所需的信息存储在服务器端对象或克隆对象中,使其能够重新创建这些对象,而无需调用数据库。

img

Gilead 还提供了一个专门的适配器,用于 Hibernate 和 GWT,使其集成变得轻松。

在最简单的情况下,GWT 和 Hibernate 之间的集成可以通过以下步骤实现

  1. 让您的持久类继承 LightEntity 类(用于 Gilead 库中无状态模式集成的类)。
  2. 让你的远程 RPC 服务扩展 PersistentRemoteService 而不是 RemoteServiceServlet
  3. 按照下面的代码片段配置你的 GWT RPC 服务的 beanManager(有关配置 bean 管理器的更多信息,请参阅 Gilead 文档)。
public class UserRemoteImpl extends PersistentRemoteService implements UserRemote {
  ...

      /**
       * Constructor
   */
  public UserRemoteImpl() {
    HibernateUtil hibernateUtil = new HibernateUtil();    hibernateUtil.setSessionFactory(HibernateUtil.getSessionFactory());

        PersistentBeanManager persistentBeanManager = new PersistentBeanManager();    persistentBeanManager.setPersistenceUtil(hibernateUtil);    persistentBeanManager.setProxyStore(new StatelessProxyStore());

        setBeanManager(persistentBeanManager);
  }
}

配置完成后,我们的 Hibernate 实体将自动转换为可通过 RPC 传输并在客户端 GWT 代码中使用的类型,无需我们进行任何其他编码或映射。

将这三个更改应用于基本 MusicStore 应用程序将导致以下结果

1) 使你的持久类继承 LightEntity

Account.java

import net.sf.gilead.pojo.java5.LightEntity;

public class Account extends LightEntity implements Serializable {
  // ...
}

Record.java

import net.sf.gilead.pojo.java5.LightEntity;

public class Account extends LightEntity implements Serializable {
  // ...
}

2) 让你的远程 RPC 服务扩展 PersistentRemoteService

MusicStoreServiceImpl.java

import net.sf.gilead.gwt.PersistentRemoteService;

public class MusicStoreServiceImpl extends PersistentRemoteService implements MusicStoreService {
  // ...
}

3) 按照上面的代码片段配置你的 GWT RPC 服务的 beanManager

MusicStoreServiceImpl.java

import net.sf.gilead.core.PersistentBeanManager;
import net.sf.gilead.core.hibernate.HibernateUtil;
import net.sf.gilead.core.store.stateless.StatelessProxyStore;
import net.sf.gilead.gwt.PersistentRemoteService;

public class MusicStoreServiceImpl extends PersistentRemoteService implements MusicStoreService {

  /**
   * Constructor
   */
  public MusicStoreServiceImpl() {
    HibernateUtil gileadHibernateUtil = new HibernateUtil();
    gileadHibernateUtil.setSessionFactory(com.google.musicstore.util.HibernateUtil.getSessionFactory());

    PersistentBeanManager persistentBeanManager = new PersistentBeanManager();
    persistentBeanManager.setPersistenceUtil(gileadHibernateUtil);
    persistentBeanManager.setProxyStore(new StatelessProxyStore());

    setBeanManager(persistentBeanManager);
  }
}

除了设置 bean 管理器的新的构造函数之外,这里还需要注意一个变化。我们现在除了在 util 包中定义的 HibernateUtil 类之外,还使用 net.sf.gilead.core.hibernate.HibernateUtil。这是正确设置 Gilead 所必需的。

就这么简单。我们现在可以使用客户端 GWT RPC 服务接口中对 AccountRecord 对象的原始调用。尝试执行以下命令,使用 Gilead 方法编译应用程序并在托管模式下运行

ant clean build hosted

再次,你可以从 这里下载我们已经构建的包含完全实现的 Gilead 解决方案的示例应用程序版本。

传输注解

为了避免在网络上发送敏感或大量数据,Gilead 还提供了一个 @ServerOnly 注解,它将排除在克隆对象中带注解的属性。另外,如果你不希望在 GWT 客户端中对克隆进行的更改反映到持久实体并持久化,你也可以将 @ReadOnly 注解添加到属性中。

何时使用 Gilead

Gilead 的主要优势是透明性和开发人员效率:配置完成后,所有事情都将自动完成,以便传输和使用你的 Hibernate 实体,就像你在客户端期望的那样(我们尝试在最初尝试将 AccountRecord 传输到 RPC 线程时所做的那样)。

Gilead 论坛 上也有很多支持,主要来自该库的实际作者 Bruno Marchesson。Gilead 在两年前首次宣布,它已经成熟,并因其在 GWT 和 Hibernate 集成方面的有效性而闻名。

但是,Gilead 也有一些缺点

  • 初始项目配置是一个至关重要的初始步骤,对于该库的新手来说,有时很难做好。事实上,Gilead 项目的大多数论坛帖子都与配置和设置问题有关。
  • Gilead 库的工作方式就像一个黑盒,就像 Hibernate 和 GWT 一样。
  • 动态代理”功能仍在测试阶段。

结论

如果你在服务器端使用 Hibernate,希望上面讨论的集成策略将有助于你的 GWT 客户端与你的 Hibernate 后端进行通信。这些方法各有优劣,特别是对于实现互操作的开发人员来说,其负担会有所不同。但是,在所有这些策略以及 web 应用程序开发的其他方面,首要关注的始终应该是用户的性能。其中一些方法有时会产生相当大的运行时开销。例如,Dozer 和 Gilead 方法在序列化大量数据时可能会给用户体验带来负担,而 DTO 解决方案的设计可以尽可能简洁有效,以提高性能。

本文可能没有涵盖 Hibernate 和 GWT 集成的其他方面。对于任何进一步的讨论,我强烈建议您访问 GWT 开发者论坛