“在CapeDwarf内部”是一系列关于CapeDwarf项目内部的博客文章。CapeDwarf是基于多种JBoss技术的Google AppEngine API的开源实现。你可以在项目的页面 https://jboss.com.cn/capedwarf 上找到更多关于CapeDwarf的信息。
AppEngine Datastore API
AppEngine API中最重要的单个API可能是Datastore API,它(正如其名称所示)提供了一个用于存储、检索和查询数据的API。这是我们首先在CapeDwarf中着手实现的API。它基本上是这个项目的证明概念。
熟悉JBoss技术的那些人将知道,JBoss已经有一个现有的项目,该项目将提供实现Datastore API所需的大部分功能 - Infinispan。Infinispan是一个高度可扩展、高可用性的键/值NoSQL数据存储和分布式数据网格平台。所以基本上Infinispan提供了我们所需要的一切 - 我们需要做的只是实现Infinispan和数据存储API之间的适配器。
闯入谷歌的工厂
不,当然我并不是在谈论闯入谷歌的实体设施。我在谈论的是AppEngine API中的所有XYServiceFactory类。它们代表了进入API的入口点,并具有诸如getXYService()等方法,您可以使用这些方法获取API提供的所有各种服务的引用。其中一个工厂是DatastoreServiceFactory,我们需要强制它返回我们自己的自定义实现DatastoreService,这样每当有人调用DatastoreServiceFactory.getDatastoreService()时,他们就会得到对CapeDwarf的服务的引用。
由于工厂本身不可配置,总是返回Google自己的DatastoreService实现,因此我们需要对工厂进行字节码操作。Javassist使得这个过程变得非常简单——我们仅仅替换了getDatastoreService方法的全部实现,使其创建一个新的CapedwarfDatastoreService实例并返回。
我们使用相同的技巧对所有的其他XYFactory.getXYService()方法进行了处理。
确保我们正确地实现了API
大部分编码工作都是采用TDD风格完成的。我们使用了JUnit和Arquillian,这允许你编程创建微部署,自动将其部署到运行中的应用服务器并在部署中运行测试。最初,我们只针对CapeDwarf和JBossAS7.1运行测试,但后来也增加了运行相同测试针对Google自己的开发应用服务器甚至生产系统(appspot)的选项。这种在我们的测试中对真实Google AppEngine运行的能力证明极为宝贵,因为它允许我们验证我们的测试,并查看我们的GAE API实现是否与GAE本身一致。
当然,仅基于API文档编写完美的测试是困难的,因为通常API文档不会深入到每个实现的细节。这意味着当我们最初在真实的GAE上运行测试时,相当多的测试实际上失败了,尽管它们在CapeDwarf上通过了。Google和CapeDwarf的API实现之间存在一些细微的差异,通过测试,我们能够确定这些差异并对CapeDwarf进行调整,使其行为完全像GAE。如果没有Arquillian,这将困难得多。
存储和检索数据
好吧,让我们最终转到datastore的实际实现。Datastore最基本的操作是通过键存储和检索实体对象。由于Infinispan公开了Cache接口,该接口扩展了java.util.Map,因此使用Infinispan实现这一功能非常直接。因此,实现datastore的get和put方法就像在Map上调用put和get一样简单。这实际上已经非常简单了。
一个后来会显现出来的注意事项是,默认情况下,Infinispan在将对象存储在缓存中时不会创建防御性副本。这意味着对存储后的对象进行的任何修改都将被从缓存中检索该对象的客户端看到(这仅在对象未被钝化并且是在集群中的同一节点上访问时才成立)。我们可以使用Infinispan的storeAsBinary选项,但我们认为在存储和返回之前简单地克隆Entity会更快,因为Entity上已经实现了clone()方法。
查询
在实现基本操作(通过键存储、检索和删除实体)后,我们转向更难的部分——查询。Infinispan-Query和Hibernate-Search已经提供了将实体的属性索引到Lucene索引并针对该索引执行查询的能力,然后从Infinispan缓存中检索结果。
为了使Infinispan索引实体,我们需要在Entity类中添加一些注解。我们通过Javassist的字节码操作实现了这一点。由于每个datastore Entity可以有一个完全动态的属性集(属性不必在任意的模式中预先指定),我们需要实现一个Hibernate-Search Field Bridge,它将实体的所有属性映射到一个Lucene文档。
由于实体现在存储在缓存以及Lucene索引中,我们剩下要做的只是实现一个查询转换器,该转换器将Google AppEngine查询转换为Infinispan的缓存查询。实际上,Infinispan的CacheQuery不过是一个围绕Lucene查询的包装器,因此转换器实际上是将GAE查询转换为Lucene查询。它是通过Hibernate-Search提供的DSL来完成的。
AppEngine拆分某些类型的查询,CapeDwarf则不需要。
关于Google对Datastore查询的实现,有趣的一点是某些类型的查询(IN、OR、NOT_EQUAL)被拆分为多个查询,然后将结果合并到一个结果集中。我们选择不这样做,因为Infinispan-Query/Hibernate-Search/Lucene完全能够在单个查询中完成这项工作。然而,当使用包含IN操作符的查询时,这种方法存在问题。在这种情况下,GAE执行多个查询,结果的顺序取决于IN列表中项目的顺序。由于这已在GAE文档中明确说明,并且某些应用程序可能依赖于这一点,我们不得不按照相同的规则对结果进行排序。虽然当查询返回整个实体时这不是问题,但在执行投影查询(这些查询仅返回实体属性的一个子集)时,这相当痛苦。当客户端执行返回属性foo的投影查询,并通过属性bar(使用IN操作符)进行过滤时,我们必须将bar添加到请求的投影属性列表中,以便我们可以根据IN子句中项目的顺序对结果进行排序。事后看来,也许我们应该简单地采取与GAE相同的做法,将这些类型的查询拆分为多个查询。我们可能会在将来这样做。
总结
这是我们对CapeDwarf中实现Datastore API的快速概述。我还没有介绍Datastore统计信息、回调和元数据。这些是Datastore API中的相对较新的功能,我将在未来的《Inside CapeDwarf》博客文章中介绍我们是如何实现它们的。
如果您在Google AppEngine上运行任何应用程序,我们非常希望您能尝试CapeDwarf,并告诉我们您遇到的问题。有关如何在CapeDwarf上运行您的应用程序的详细说明,请参阅Aleš Justin最近关于首次CapeDwarf发布的博客文章。