你真的需要数据库吗?了解 XPrevail。






2.15/5 (9投票s)
2004年11月21日
25分钟阅读

41692

244
讨论了“普适性”概念,以及它在某些情况下如何成为使用数据库的理想替代方案。我们将了解 XPrevail,一个开源的普适性层,以及它的功能如何实现简单而现代的编程。
什么是普适性?
普适性概念最初由巴西人 Klaus Wuestefeld 提出,并在 Java 项目 Prevayler 中实现。它是将旧的、已在其他场景中单独应用的概念结合起来。
正如我在摘要中提到的,使用普适性是使用数据库的一种替代方案。通过它,所有业务对象都持久化在内存中,并保证在断电或应用程序失败的情况下,它们将忠实地恢复。普适性使用的主要优点包括:
- 查询性能的巨大提升;
- 解决方案成本的急剧降低(消除了数据库服务器(硬件)成本、
数据库服务器软件许可证、维护运营成本等);
- 促进真正的面向对象开发(在我看来是最有趣的)。
不要忘记访问 XPrevail 项目网站中的“什么是?”部分。
关键概念
为了理解普适性的总体工作原理,重要的是我们首先了解此概念中常用的一些术语,因此,我们接下来将看到其中主要的几个。
PrevalenceEngine :这是普适性框架中的一个类,负责所有执行过程,它记录和备份在 PrevalentSystem 中进行的更改。我们通常简称为“引擎”。稍后我们将看到 XPrevail 提供了四种不同类型的引擎,每种都有其特殊性。
PrevalentSystem :用户定义的类,必须引用应用程序的所有业务对象,即将被持久化的对象。 当向 PrevalenceEngine 请求快照时,引用的对象将被序列化。这样,该类的任何对象都不会丢失其状态。
Snapshot :PrevalenceEngine 的过程,在此过程中,它序列化所有的 PrevalentSystem(实际上,稍后我们将看到 XPrevail 支持使用多个 PrevalentSystem),使其与请求时的状态一致。这会生成一个新的快照存档,并且不再需要读取以前操作的日志存档。
Operation :根据普适性最初的概念,所有在 PrevalentSystem 中保存的对象上进行的修改都必须通过实现框架定义的 IOperation 接口的类的对象进行。这些实例被传递给 PrevalenceEngine 进行注册和执行。在 prevayler 项目中,等效接口称为 Transaction 。(更多详情请参见清单)。
普适性执行流程
为了更好地理解这个概念,我现在将描述一个普适性应用程序的执行流程。假设我们在一个给定的应用程序中只有一个业务类,即 Client 类。我们将逐步使这个应用程序具有普适性。
1. 我们需要定义一个类来保存对我们 Client 对象的引用,它将是我们的 PrevalentSystem,可以命名为 Clients 。
2. 我们将声明一个 PrevalenceEngine 引用并对其进行初始化,此时将需要提供 PrevalentSystem 的类信息,当然必须告知 Clients 。
3. 必须通过 PrevalentSystem 属性向 PrevalenceEngine 请求 PrevalentSystem 的初始化。它将准确返回一个 Clients 类的实例,其状态与上次执行时相同(如果发生过)。
4. 如前所述,PrevalentSystem 中的任何更改都必须通过实现 IOperation 接口的类的对象进行。因此,我们可以声明两个类,一个名为 AddClient ,另一个名为 RemoveClient ,它们都实现 IOperation 接口。它们将分别用于添加和删除 PrevalentSystem 中的 Client 对象。(在本文的后续部分,我们将看到 XPrevail 考虑了更简单直观的编程,无需创建这些类)
5. 要添加一个客户端,我们现在需要创建一个 AddClient 类的实例,在其构造函数中传递执行任务所需的信息。然后我们必须请求 PrevalenceEngine 执行此操作。这将涉及先将 AddClient 对象序列化到一个用于操作日志的存档中,然后执行它,实际添加客户端将在此处发生。
6. 假设在执行几次第5项后,除了也进行了一些客户端移除,那么我们将有一个包含确定数量 Client 对象的 PrevalentSystem。操作日志存档将包含所有(实现 IOperation )对 PrevalentSystem 进行更改的对象的序列化版本,顺序与发生时完全相同。
7. 如果此时发生断电。我们的应用程序必须在环境恢复后立即恢复运行。在 PrevalenceEngine 初始化时,它将读取所有生成的日志操作存档。它将反序列化存档中包含的所有对象并再次执行它们。请注意,此存档中保存的对象是 AddClient 类的,而不是 Client 类的。PrevalenceEngine 持续记录业务对象中进行的更改,而不是业务对象本身。
8. 应用程序恢复运行并恢复其先前的状态后,我们可以在 PrevalentSystem 中生成更多更改并请求一个快照。此时,PrevalenceEngine 将生成一个新存档,不是操作日志,而是专门用于快照的存档。此存档将包含整个 PrevalentSystem 的序列化版本,即所有业务对象的实际序列化版本。假设在快照之后,我们在 PrevalentSystem 的状态中又进行了一些更改,并且又发生了断电。
9. 当我们的应用程序请求初始化 PrevalenceEngine 时,它将搜索最近生成的快照。利用它,它将反序列化其内容并获取 PrevalentSystem 的一个版本。完成此操作后,它将获取所有在上次快照之后的操作日志存档,并在获取的 PrevalentSystem 上重新执行所有先前的操作。这将导致一个 PrevalentSystem,其状态与断电之前存在的状态相同,并尽可能优化。
普适性的要求
普适性对将被持久化的业务对象有两个必要要求:它们必须是可序列化的和确定性的。确定性对象是指在相同的一系列更改以相同顺序提交后,总是产生相同最终状态的对象。代表 PrevalentSystem 的类显然也需要是可序列化的。在 .NET 中,使类可序列化的最有效和最简单的方法是通过 SerializableAttribute 属性。
普适性中涉及的概念以优化的方式执行任务,这反映在以下问题中:
- 序列化业务对象中的更改,而不是对象本身,这显著减少了保证 PrevalentSystem 状态所需的磁盘空间量。
- 不对生成的存档进行任何类型的索引,毕竟目标不是模拟数据库,而是消除它。因此,只专注于对象的序列化和恢复过程。
- 通过快照恢复 PrevalentSystem 比通过操作日志备份要快得多。原因很容易理解,通过快照直接反序列化整个 PrevalentSystem,而通过操作日志,它需要反序列化所有操作对象并再次执行它们。因此,建议定期进行快照,但请注意,在对 PrevalentSystem 进行快照期间,不能对其进行修改。
另一个重要细节是,由于所有对象都保存在内存中,因此要高效地使用普适性,需要足够的 RAM 来容纳系统中的所有业务对象。
我希望您已经注意到,在前面描述的步骤中,我们没有涉及到任何 SQL 指令。普适性概念的一个积极的附带效应是,我们被迫更认真地面对面向对象概念,因为我们将只使用它。我们使用自己的编程语言来执行查询,可以以任何形式获取和操作数据。
XPrevail 是什么?
XPrevail 是一个 .NET 普适性对象层,友好且可扩展,用 Delphi for .NET 实现。该框架兼容任何 .NET 语言,并且是免费和开源的。XPrevail 的清单主题阐述了 XPrevail 存在的原因,以及它相对于其他普适性实现的不同之处。
我在其他普适性框架面前所寻求的大部分差异化,都是为了实现更自然的编程,提供一个尽可能透明且不具侵入性的框架。我试图让 XPrevail 的编程模型以一种应用程序对其依赖性更小的方式呈现,从而在无需付出巨大努力的情况下,方便地更换持久化机制。
我认为持久化是软件的一个独立方面,不应该显著影响其模型、架构和实现。这种理念在 XPrevail 的建模和实现过程中始终如一。这些目标促使我以不同的方式做一些事情,并添加一些小功能。
XPrevail 带来的主要差异化功能如下:
多 PrevalentSystem
XPrevail 带来了 MultiSystemsPrevalenceEngine,它能够通过 .NET 的透明代理来处理多个 PrevalentSystem。这种类型的引擎带来了巨大的优势,即无需定义对象来对 PrevalentSystem 进行所有修改,这些修改可以通过 PrevalentSystem 中定义的方法进行。对这些方法的调用会被拦截并自动转换为对象,然后以传统方式进行序列化和执行。
它实现了良好的职责划分,使得每个 PrevalentSystem 只保留对逻辑相关对象的操作。此外,我们可以使用系统本身的控制类作为 PrevalentSystem,而无需创建包含它们的全局类。
总而言之,使用多个 PrevalentSystem 有助于我们获得友好优雅的代码,更符合良好的面向对象编程实践。
覆盖所有业务对象
XPrevail 通过 ExtremePrevalenceEngine 引入了截获所有对业务对象进行的请求的可能性,并自动将其转换为适当的对象,进行序列化和执行。通过这种方式,我们可以以完全透明的方式工作,直接访问和修改业务对象,无需外部类甚至 PrevalentSystem 的干预。
这是使 XPrevail 的使用对客户端代码更友好的一个重要步骤,只需少量要求,就能够直接实现业务对象的普适性,即使是对其进行的更改。这也促进了更自然的编程形式,并且对 XPrevail 或普适性概念的依赖性更小(请参阅 路线图,了解 XPrevail 也支持关系型数据库持久化的意图)。
支持面向方面编程 - AOP
XPrevail 带来的另一个非常有趣的功能是可以使用基于面向方面编程(通常简称为 AOP)概念的开发模型。框架的大部分基于代理,而代理又基于代码拦截,这是 AOP 的基本原则。因此,正如我已经在代理和框架内部类之间使用类似方法进行通信一样,一旦我意识到这种潜力,就只是发布一种方法供客户端代码获取这种方法的特性。
实现 IAspectsSupport 接口的框架引擎,目前是 MultiSystemsPrevalenceEngine 和 ExtremePrevalenceEngine,提供了注册和注销方面(任何实现 IAspect 接口的对象)的方法。
XPrevail 提供了两个已实现的方面,在初步付费版本中,客户端代码可以免费使用,它们是 TraceAspect 和 ProfileAspect 。它们分别能够记录对业务对象调用的方法以及记录执行这些方法所需的时间。这些功能极其有用和强大,XPrevail 的未来版本肯定会得到很好的开发(参见 路线图)。
使用 XPrevail
现在我们将看到 ExtremePrevalenceEngine 的完整使用示例,它是 XPrevail 的主要 PrevalenceEngine,也是框架真正精髓所在。在继续之前,建议阅读 XPrevail 网站上的 Hello Prevalence 和 Hello Prevalence 2 教程,这将有助于我们理解这里将开发的示例。
定义对象模型
在考虑持久化之前,我们首先需要定义业务对象的模型。为了简化问题,我们只使用一个业务对象,即 People。我们将用它来创建一个小型的人员登记应用程序。
People = class
strict private
FName: System.String;
FBirthDate: System.DateTime;
FEMail: System.String;
public
function ToString : String; override;
procedure set_Name(const Value: System.String);
procedure set_EMail(const Value: System.String);
procedure set_NiverDate(const Value: System.DateTime);
function get_Age: Integer;
property Name : System.String read FName write set_Name;
property Age : Integer read get_Age;
property EMail : System.String read FEMail write set_EMail;
property BirthDate : System.DateTime read FBirthDate write set_NiverDate;
constructor Create(aName : String; aEMail : String; aBirthDate : DateTime);
end;
很快,这将是我们类的标准定义,但是我们需要进行一些更改,以使其满足普适性和 ExtremePrevalenceEngine 的要求。为此,我们需要进行以下修改:
-
如前所述,业务对象需要是可序列化的,因此我们必须在类中包含 SerializableAttribute 属性。
-
业务对象以及 PrevalentSystem 需要继承自 MarshalByRefObject。
-
为了使框架能够正确识别给定的业务对象,它需要实现 IObjectID 接口,并用 BusinessObjectClassID 属性进行标记。
完成必要的修改后,我们的 People 类接口如下:
[Serializable]
[BusinessObjectClassID(PeopleClassID)]
People = class (MarshalByRefObject, IObjectID)
strict private
FID : System.String;
FName: System.String;
FBirthDate: System.DateTime;
FEMail: System.String;
public
function ObjectID : string;
function ToString : String; override;
procedure set_Name(const Value: System.String);
procedure set_EMail(const Value: System.String);
procedure set_NiverDate(const Value: System.DateTime);
function get_Age: Integer;
property Name : System.String read FName write set_Name;
property Age : Integer read get_Age;
property EMail : System.String read FEMail write set_EMail;
property BirthDate : System.DateTime read FBirthDate write set_NiverDate;
constructor Create(aID, aName : String; aEMail : String; aBirthDate : DateTime);
end;
下一步是创建一个将成为我们 PrevalentSystem 的类,因此请参阅为此目的定义的 PeopleManager 类。
[Serializable]
[PrevalentSystemClassID('{319D4955-4CE8-4443-9010-9BAE12046C24}')]
PeopleManager = class (MarshalByRefObject, IBusinessObjectFromID)
strict private
FPeoples : ArrayList;
strict protected
function BusinessObjectFromID(BOClassID, ObjectID: string): TObject;
public
function AllPeoples : ArrayList;
[BusinessObjectExport]
function get_Peoples(Index: Integer): People;
property Peoples[Index : Integer] : People read get_Peoples;
function get_PeopleCount : Integer;
property PeopleCount : Integer read get_PeopleCount;
[Operation]
[BusinessObjectExport]
function AddPeople(aID, aName : String; aEMail : String; aBirthDate : DateTime) : People;
[Operation]
[RealBusinessObjectRecovery]
procedure RemovePeople(aPeople : People);
constructor Create;
end;
与业务对象类一样,PrevalentSystem 也需要继承自 MarshalByRefObject 并接收 Serializable 属性(实际上是可序列化的)。由于 ExtremePrevalenceEngine 是一个支持使用多个 PrevalentSystem 的 PrevalenceEngine,因此需要用 PrevalentSystemClassID 属性标记它们,以便框架能够正确识别它们。还请注意, IBusinessObjectFromID 接口由 PeopleManager 类实现,这仅在 PrevalentSystem 引用业务对象时才需要。
请注意 PeopleManager 类方法上使用的一些修饰属性,现在让我们看看它们各自的作用。
1. Operation:它向 XPrevail 指示该方法对 PrevalentSystem 的状态进行修改。如果修改 PrevalentSystem 状态的方法没有用此属性标记,则修改将不会持久化。
2. BusinessObjectExport:它向框架指示此方法返回一个业务对象,该业务对象在 PrevalentSystem 之外,通过这种方式,XPrevail 拦截此调用并返回一个代理对象,以便直接在对象中进行的修改能够持久化。请注意,与所有持久化机制一样,程序员需要决定哪些内容将被持久化。通过没有此属性的方法获取的业务对象中的更改将不会持久化。
3. RealBusinessObjectRecovery:它向 XPrevail 指示必须从其副本版本中恢复对象(更多详细信息请参见下面的框)。
RealBusinessObjectRecovery
普适性通常禁止实现 IOperation 接口(或 Prevayler 中的 Transaction)的类,即修改 PrevalentSystem 状态的类直接引用业务对象。因为以这种方式,业务对象将被复制,并且相关的更改将在该副本版本中进行,这会引起足够多的问题。这个错误非常常见,被认为是普适性中的一个“命名错误”。
因此,XPrevail 不推荐这样做,但也不禁止。它为您提供了自行塑造代码的选项,但客户将承担一定的性能成本。如果确定选择此选项,只需用 RealBusinessObjectRecovery 属性标记方法,XPrevail 将确保这不会产生意外问题。这将在示例中演示。
很快,现在我们已经有了用于开发普适性示例的合理类。更好的是,所有的持久化代码实际上都已经完成了,也就是说,几乎没有。请注意,XPrevail 的要求并不具有很强的侵入性,因为属性和/或接口的实现是通过修饰来决定的。将来如果决定将 XPrevail 作为持久化机制移除,或者将软件定制到另一种机制,并不会导致代码的显著损失。
开发控制台应用程序
我们现在将创建一个控制台应用程序来演示我们定义的类的使用。为了避免文本过于冗长,我将只列出真正优秀的实现片段。要访问完整代码,只需下载本文的示例应用程序。要编译它,必须下载 XPrevail,您可以通过链接 http://xprevail.sourceforge.net/en-us/download.htm 进行下载。
第一个要评论的点是如何获取我们 PrevalentSystem 的初始实例,正如我们将看到的,它必须向 PrevalenceEngine 请求。但在此之前,我们首先需要获取引擎本身的实例。以下代码演示了整个过程:
001. var
002. Engine : ExtremePrevalenceEngine;
003. GenKeys : GenerateKeys;
004. PManager : PeopleManager;
005.
006. begin
007. Engine := XPrevailFactory.CreateExtremePrevalenceEngine([typeOf(GenerateKeys),
typeOf(PeopleManager)], Path.Combine(Environment.CurrentDirectory, 'data'));
008. GenKeys := Engine.PrevalentSystems[typeOf(GenerateKeys)] as GenerateKeys;
009. PManager := Engine.PrevalentSystems[typeOf(PeopleManager)] as PeopleManager;
010. {…}
011. end.
第 002 到 004 行仅包含引用的声明。在第 007 行,请求创建 ExtremePrevalenceEngine 类的一个实例,正如我们所看到的,这是通过 XPrevailFactory 类完成的。CreateExtremePrevalenceEngine 方法要求第一个参数是一个 System.Type 数组,代表将由引擎管理的 PrevalentSystem。第二个参数指定了操作日志存档和快照存档将存放的文件夹。
您肯定已经注意到,我们正在使用一个名为 GenerateKeys 的类作为 PrevalentSystem。正如其名称所示,它将用作我们对象的 ID 生成器。这个类的定义非常简单,可以在下面看到:
[Serializable]
[PrevalentSystemClassID('{5CFE36DA-9C6A-495F-911B-10CD99374195}')]
GenerateKeys = class (MarshalByRefObject)
strict private
HashKeys : HashTable;
public
[Operation]
function NewKey(ObjectType : System.Type) : Integer;
constructor Create;
end;
确保为指定类型的对象生成唯一的序列号,可能出现业务对象 ID 相同的情况,只要它们是不同类型的。这是 ExtremePrevalenceEngine 的一个要求。请注意,由于它不保存业务对象,因此不需要实现 IBusinessObjectFromID 接口。下面我们可以看到其主要方法 NewKey 的实现。
function GenerateKeys.NewKey(ObjectType: System.Type): Integer;
var
KeyID : Integer;
begin
Monitor.Enter(ObjectType);
try
if HashKeys.Contains(ObjectType) then
begin
KeyID := Integer(HashKeys.Item[ObjectType]);
Inc(KeyID);
HashKeys.Item[ObjectType] := &Object(KeyID);
end
else
begin
KeyID := 1;
HashKeys.Add(ObjectType, &Object(KeyID));
end;
Result := KeyID;
finally
Monitor.Exit(ObjectType);
end;
end;
第 008 行和第 009 行获取了我们 PrevalentSystem 的实例。由于 PeopleManager 的操作将使用 GenerateKeys,因此重要的是先请求后者。这是通过 PrevalentSystems 属性完成的,该属性已告知预期的 PrevalentSystem 的 System.Type。现在我们将继续分析我们应用程序实现的一些关键部分。
添加人员
var
{...}
ID : String;
begin
{...}
ID := Convert.ToString(GenKeys.NewKey(typeOf(People)));
PManager.AddPeople(ID, Name, Email, BirthDate);
{...}
end;
删除人员
{...}
PManager.RemovePeople(PManager.Peoples[
{...}
编辑人员
var
Ind : Integer;
Pp : People;
Value : String;
begin
{...}
Pp := PManager.Peoples[
{...}
WriteLine('');
WriteLine('输入新值:');
Value := Console.ReadLine;
case
1 : Pp.Name := Value;
2 : Pp.EMail := Value;
3 : Pp.BirthDate := Convert.ToDateTime(Console.ReadLine);
else begin
WriteLine('无效选项,退出。');
Exit;
end;
end;
{...}
end;
列出人员
var
Arr : ArrayList;
I : Integer;
begin
Console.WriteLine('');
Arr := PManager.AllPeoples;
for I := 0 to Arr.Count - 1 do
begin
with (Arr[I] as People) do
begin
Console.WriteLine('[{0}] {1}', &Object(I), &Object(ToString));
end;
end;
Console.WriteLine('');
end;
显示结果
正如我们所见,所有代码都惊人地简单。现在让我们看看应用程序运行中的一些情况:
图 1. ObjectApp 示例启动
图 2. 添加人员
图 3. 列出注册人员
我们的应用程序已准备就绪,可以随意登记、修改和删除人员,最重要的是,无论发生什么,您的数据都将是安全的。多次执行这些操作,请求快照,并评估结果。您可以进行测试,终止进程或甚至断开机器,您会发现数据将在下次执行时保留并恢复。所有这些都无需使用数据库!
在进行一些操作、请求一个快照以及后续的更多操作之后,我们可以看到 XPrevail 生成的文件如图 4 所示。
图 4. 操作日志存档和快照存档
支持面向方面编程 - AOP
在本文中,我将只介绍这个主题,但它肯定会在后续的教程或文章中进行更详细的探讨。
XPrevail 带来的一个出色功能是支持基于面向方面编程(AOP)原则的编程。这种支持是动态且可扩展的,创建方面的过程简单而强大。通过它,我们可以为日志生成、并发控制、性能分析器、审计等提供优雅的解决方案。
AOP 承诺填补 POO 未填补的空白,其中最经典的例子是日志生成。在传统方法中,当我们需要生成应用程序执行流程的日志时,我们最终不得不将代码分散到一些类中。这些代码并非类的意图。
AOP 认为日志生成以及其他各种问题(包括持久化)都是软件的“方面”,不应与业务规则混淆。AOP 的实现目前尚未像 POO 那样标准化,因此可以找到一些处理和实现此概念的方法和理念。
我现在将只展示这个奇妙之处的一些潜力,以及如何通过 XPrevail 来使用它。
使用 ProfilerAspect
假设我们想要对应用程序进行性能分析,以了解某些方法的执行时间。你会怎么做呢?显然,你会打开应用程序源代码并开始分发测量代码,对吧?但是,让我们看看如何使用 XPrevail 来实现这一点。为此,我们只需修改测试应用程序的两行代码,如下所示:
{...}
Engine.RegisterAspect(ProfilerAspect.Create(nil, falsifies));
{...}
这一行代码在我们的引擎中注册了一个新的方面(XPrevail 已提供)。它的构造函数需要两个参数,第一个是用于记录测量的流——如果传递 nil,它将输出到控制台。第二个参数指示是否必须不加区别地作用于 Engine 所持有的 PrevalentSystem 和业务对象的所有方法。
由于我们传递 false,它将仅作用于用 MethodProfiler 属性标记的方法。因此,为了测试,我们将在 PeopleManager.AllPeoples 方法中添加此属性。
{...}
[ MethodProfiler ]
function AllPeoples: ArrayList;
{...}
很快,完成这些后,只需编译应用程序并再次执行即可。图 5 显示了结果。
图 5. ProfilerAspect 正在运行
可怕?不,我会说有趣。这无疑是一个非常强大的功能。正如之前所说,现在我只想留下这个演示,将来会有另一个教程或文章更详细地探讨使用 XPrevail 开发方面。
XPrevail 项目的当前状态
目前 XPrevail 处于 0.9.4 alpha 版本,正在不断进行改进和重构。虽然是 alpha 版本,但它已经完全可用。请访问项目网站的“路线图”部分,了解其发展方向。
总的来说,XPrevail 有可能以透明的方式支持关系型数据库的持久化。复制、高效的事务支持和对象锁定等功能也已预见。最大的目标是,通过 XPrevail,我们能够编程具有方面、容错和负载平衡的普适性应用程序。它还支持开发双重应用程序,这是一种即使对于有状态应用程序也能实现故障容忍度的方法。
我仍在逐步开发类的教程文档、文章和单元测试。任何在这方面的帮助,以及其他语言的翻译都将不胜感激。通过文章、创建示例和案例研究来推广框架将非常受欢迎。
结论
简单,不是吗?这就是对象模型,您看到我们的应用程序中大量引用持久化代码了吗?确实没有。您能计算出在编写 SQL 代码时花费了多少时间吗? L 但对我来说,最酷的感觉是持久化不存在,我们可以自由地使用对象,而无需担心它们的状态,因为它们会持久存在。
不要忘记访问项目网站 http://xprevail.sourceforge.net,立即开始编程普适性应用程序。
版本 1.1 - 2004年11月21日
FernandoVM 是 Borland Delphi 认证专家,参加了所有届 BorCon 巴西大会的演讲。他曾是 ClubeDelphi 杂志的撰稿人、作者和技术编辑。他目前在 Softium Informática Ltda 担任技术经理、架构师和 Tactium 产品开发团队协调员。Tactium 是一款用于 CTI/CRM 的实时分布式解决方案,被巴西 14 个州的 90 多家公司使用。他是 XPrevail 项目的创建者和维护者,可以通过 fernandovm@users.sourceforge.net 或 http://fernandovm.blogspot.com 与他联系。