Fork me on GitHub

使用 MEF 轻松实现云部署

Joseph Fultz
Chris Mabry

下载代码示例

过去几个月中,我和一位同事一直在从事一个利用 Microsoft Extensibility Framework (MEF) 的项目。在本文中,我们将看看如何使用 MEF 使云部署更易于管理一点、更灵活一些。MEF(以及 Unity 之类的类似框架)是一种软件结构,可将开发人员从管理依赖关系解析、对象创建和实例化等工作中释放出来。您可能不时会发现自己在撰写工厂方法或在构造函数或所需初始化方法内创建依赖对象,但借助 MEF 之类的框架,大部分此类工作不再是必需的了。

通过在我们的部署中将 MEF 与 StorageClient API 结合在一起使用,我们不必重新利用或重新部署我们的 Web 角色,便可以部署和提供新类。此外,我们可以将类型的更新版本部署到云中而不必全部重新部署,只是改为重新利用应用程序。请注意,尽管我们在此处使用的是 MEF,但使用 Unity、Castle Windsor、StructureMap 或其他任何类似容器并按照相似结构应该会取得相同的结果,主要差异体现在语法和类型注册语义上。

设计和部署

俗话说得好: 一分耕耘,一分收获。在本例中,要求某些构造标准以及围绕部署执行其他一些工作。首先,如果您习惯使用依赖关系注入 (DI) 或复合容器,则可能您很喜欢在代码内使实现和接口分离开来。我们不要在这里偏离这个目标 — 我们的所有具体类实现都具有追溯到某一接口类型的继承。这并不意味着每个类都将直接继承自某个接口,但类通常将具有抽象层,这些抽象层遵从“接口 “ 虚拟 “ 具体”之类的模式。

图 1 显示,不仅我感兴趣的主要类具有此类链,而且实际上,其必需的属性之一也是抽象的。通过所有的抽象,可以很方便地替换部件或以导出所需约定(在本例中是接口)的新库的形式添加附加功能。除了复合之外,在您的类设计的抽象化方面严格要求还有一个附带的好处,就是能够通过模拟接口更好地实现测试。


图 1 类关系图

该要求的较难的部分是部署模型中针对应用程序的更改。因为我们想要在运行时生成我们的导入和导出的目录,并且不必再次部署便可以刷新该目录,所以,我们必须部署在 Web 角色部署之外存放我们的具体类的二进制文件。对于在启动时的应用程序,这也会造成一点儿额外的工作。图 2 描绘了在 Global.asax 调用我们已创建的名为 MEFContext 的帮助程序类时该 Global.asax 中的启动工作。


图 2 在启动时生成目录

运行时复合

因为我们将要从存储中的文件加载目录,所以,我们将需要让这些文件进入我们的云存储容器中。因此,使这些文件进入 Windows Azure 存储位置需要成为部署过程中的一环。这可以通过使用 Windows Azure PowerShell cmdlet (wappowershell.codeplex.com) 和一些生成后步骤非常轻松地实现。对我们来说,我们将使用 Windows Azure 存储浏览器 (azurestorageexplorer.codeplex.com) 手动移动这些二进制文件。

我们创建了一个项目,该项目包含一个常见的诊断类、一个客户实体以及几个规则库。所有规则库都必须继承自 IBusinessRule<t> 类型的接口并且导出此类型的接口,其中,t 表示对其实施这些规则的实体。下面是针对规则的类声明的导入部分:

  1. [Export(typeof(IBusinessRule<ICustomer>))]
  2. public class CustomerNameRule : IBusinessRule<ICustomer>
  3. {
  4. [Import(typeof(IDiagnostics))]
  5. IDiagnostics _diagnostics;
  6. ...
  7. }

您可以看到导出以及在我们请求规则对象时 MEF 将为我们注入的诊断依赖关系。知道要导出的内容十分重要,因为这些内容将会成为用来解析您所需实例的约定。Microsoft .NET Framework 4.5 将给 MEF 带来一些改进,将允许放宽当前围绕容器中的泛型的一些约束。例如,目前您可以注册和检索 IBusinessRule<ICustomer> 之类的内容,但不能注册和检索 IBusiness-Rule<t> 之类的内容。有时候,您希望某一类型的所有实例都超出其实际模板类型。目前,实现此目标的最简单方式是注册一个字符串约定名称,它将是您的项目或解决方案中达成一致的约定。在本例中,如前所述的声明将适用。

我们具有两个规则,一个针对电话号码,一个针对姓名,并且具有一个诊断库,它们都将通过 MEF 容器提供。我们需要做的第一件事是从 Windows Azure 存储中获取库并且将其放入本地资源中(本地目录),以便我们可以使用 DirectoryCatalog 加载它们。为此,我们在 Global.asax 的 Application_Start 中包含了几个函数调用:

  1. // Store the local directory for later use (directory catalog)
  2. MEFContext.CacheFolderPath =
  3. RoleEnvironment.GetLocalResource("ResourceCache").RootPath.ToLower();
  4. MEFContext.InitializeContainer();

我们刚刚获取了所需的资源路径,该资源路径配置为 Web 角色的一部分,然后调用方法以便设置容器。该初始化方法又调用 UpdateFromStorage 以便获取文件,并且调用 BuildContainer 以便创建目录和 MEF 容器。

UpdateFromStorage 方法将在预先确定的容器中查找并且遍历该容器中的文件,并将各文件下载到本地资源文件夹中。该方法的第一部分如图 3 中所示。

图 3:UpdateFromStorage 的前半部分

  1. // Could also pull from config, etc.
  2. string containerName = CONTAINER_NAME;
  3. // Using development storage account
  4. CloudStorageAccount storageAccount =
  5. CloudStorageAccount.DevelopmentStorageAccount;
  6. // Create the blob client and use it to create the container object
  7. CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
  8. // Note that here is where the container name is passed
  9. // in order to get to the files we want
  10. CloudBlobContainer blobContainer = new CloudBlobContainer(
  11. storageAccount.BlobEndpoint.ToString() +
  12. "/" + containerName,
  13. blobClient);
  14. // Create the options needed to get the blob list
  15. BlobRequestOptions options = new BlobRequestOptions();
  16. options.AccessCondition = AccessCondition.None;
  17. options.BlobListingDetails = BlobListingDetails.All;
  18. options.UseFlatBlobListing = true;
  19. options.Timeout = new TimeSpan(0, 1, 0);

在前半部分中,我们对存储客户端进行了设置,以便获取我们需要的内容。在此方案中,我们要求的是那里的任何内容。在您正在将文件从存储下传到本地资源的情况下,可能值得执行完整步骤并且获取所有内容。若要更具针对性地提取文件,您可以向 options.AccessCondition 属性分配某个 IfMatch 条件。这要求在上载时对 blob 设置 etag。此外,您可以通过存储上次更新时间和应用 IfModifiedSince 的 AccessCondition,对重新生成 MEF 容器的更新方面进行优化。

图 4 显示 UpdateFromStorage 的后半部分。

图 4 UpdateFromStorage 的后半部分

  1. // Iterate over the collect
  2. // Grab the files and save them locally
  3. foreach (IListBlobItem item in blobs)
  4. {
  5. string fileAbsPath = item.Uri.AbsolutePath.ToLower();
  6. // Just want the file name ...
  7. fileAbsPath =
  8. fileAbsPath.Substring(fileAbsPath.LastIndexOf('/') + 1);
  9. try
  10. {
  11. Microsoft.WindowsAzure.StorageClient.CloudPageBlob pageblob =
  12. new CloudPageBlob(item.Uri.ToString());
  13. pageblob.DownloadToFile(MEFContext.CacheFolderPath + fileAbsPath,
  14. options);
  15. }
  16. catch (Exception)
  17. {
  18. // Ignore exceptions, if we can't write it's because
  19. // we've already got the file, move on
  20. }
  21. }

在存储客户端就绪后,我们只需遍历 blob 项并且将它们下载到资源中。根据整个下载的条件和目标,我们可以在此操作中在本地复制文件夹结构或者基于约定生成文件夹结构。有时候,文件夹结构是为了避免名称冲突而提出的一项要求。我们继续使用霰弹式方法并且获取所有文件,然后将它们放置于一个位置中,因为我们知道,它只是此示例的两个或三个 DLL。

这样,我们使文件就位并且仅需要生成容器。在 MEF 中,复合容器从一个或多个目录生成。在本例中,我们将使用 DirectoryCatalog,因为这样可以很方便地将编录指向目录并且加载可用的二进制文件。因此,用于注册类型和准备容器的代码十分简短:

  1. // Store the container for later use (resolve type instances)
  2. var catalog = new DirectoryCatalog(CacheFolderPath);
  3. MEFContainer = new CompositionContainer(catalog);
  4. MEFContainer.ComposeParts();

现在,我们将运行该站点并且应该会看到容器中提供的类型的转储,如图 5 中所示。


图 5 初始导出

我们在这里没有转储整个容器,而是专门请求 IDiagnostics 接口,然后全部导出类型 IBusinessRule<ICustomer>。正如您所看到的,我们在将新的业务规则库上载到存储容器中之前具有其中的一个。

我们已将 NewRules.dll 放置于存储位置中,现在需要将其加载到应用程序中。理想情况下,您想要通过对存储容器执行一点文件观察,触发容器重新生成。此外,使用 IfModifiedSince AccessCondition 可以通过快速轮询轻松地实现上述操作。但是,我们选择了手动程度更高的过程,即单击我们的测试应用程序上的“Update Catalog”(更新目录)。图 8 显示了结果。


图 8 更新的规则导出

我们刚刚重复了用于创建目录和初始化容器的步骤,并且现在我们有了一个要实施的新的规则库。请注意,我们没有重新启动该应用程序或重新部署,但我们具有在环境中运行的新代码。在这里唯一的遗留问题是需要一些同步方法,因为我们不能在替换引用的同时让代码尝试使用复合容器:

  1. var catalog = new DirectoryCatalog(CacheFolderPath);
  2. CompositionContainer newContainer =
  3. new CompositionContainer(catalog);
  4. newContainer.ComposeParts();
  5. lock(MEFContainer)
  6. {
  7. MEFContainer = newContainer;
  8. }

生成第二个容器并仅替换引用的主要原因是减少锁定时限并且立即返回要使用的容器。

为了进一步充实发展该代码库,下一步是要实现您的自定义目录类型,例如 AzureStorageCatalog,如图 9 中所示。遗憾的是,当前对象模型没有适当的接口或者可轻松重复使用的定义的代码库,因此,使用一点继承以及一些封装可能是最佳选择。实现与 AzureStorageCatalog 列表相似的类将会实现一个简单的模型,这个模型实例化自定义目录并且直接在复合容器中使用它。

图 9 AzureStorageCatalog

  1. public class AzureStorageCatalog:ComposablePartCatalog
  2. {
  3. private string _localCatalogDirectory = default(string);
  4. private DirectoryCatalog _directoryCatalog =
  5. default(DirectoryCatalog);
  6. AzureStorageCatalog(string StorageSetting, string ContainerName)
  7. :base()
  8. {
  9. // Pull the files to the local directory
  10. _localCatalogDirectory =
  11. GetStorageCatalog(StorageSetting, ContainerName);
  12. // Load the exports using an encapsulated DirectoryCatalog
  13. _directoryCatalog = new DirectoryCatalog(_localCatalogDirectory);
  14. }
  15. // Return encapsulated parts
  16. public override IQueryable<ComposablePartDefinition> Parts
  17. {
  18. get { return _directoryCatalog.Parts; }
  19. }
  20. private string GetStorageCatalog(string StorageSetting,
  21. string ContainerName)
  22. { }
  23. }

更新现有功能

向我们的部署中添加新功能相当容易,但更新现有功能或库则不然了。尽管该过程要优于完全重新部署,但仍涉及相当多的人力,因为我们必须将文件移到存储中,并且相关 Web 角色必须更新其本地资源文件夹。但是,我们还将循环使用这些角色,因为我们需要上载和重新加载 AppDomain,以便刷新在容器中存储的类型定义。即使您将复合容器和类型加载到辅助 AppDomain 中并且尝试从那里加载,您从中请求类型的 AppDomain 仍将从以前加载的元数据中加载它。我们可以看到,对此的唯一方法是将实体发送到辅助 AppDomain 并添加一些自定义封送,而非对主 AppDomain 使用导出的类型。该模式对我们而言似乎有问题;自身中存在双重 AppDomain 似乎有问题。因此,一个更简单的解决方案是在新的二进制文件可用后重复使用这些角色。

有一些与 Windows Azure 更新域有关的好消息。请看一下我的 2012 年 2 月的专栏“Windows Azure 部署域”(msdn.microsoft.com/magazine/hh781019),专栏中介绍了更新域的大致情形以及在各情况下如何重新启动实例。从积极的角度上说,该站点无需完全重新部署即可继续工作。但是,在刷新过程中您可能会遇到两个不同的行为。不过,这是一个可接受的风险,因为如果您进行了完全部署,则在回滚更新过程中存在同样的问题。

您可以将此配置为在部署内发生,但问题之一是如何进行协调。为此,要求协调实例的重新启动,因此,或者需要选择一个首要实例,或者要具有某个投票系统。我们认为通过监视进程以及前面提到的 Windows Azure cmdlet,可以更容易地对该任务进行处理,而不是将一些人工智能写入 Web 角色中。

使用 MEF 之类的框架有许多原因,但这有点超出了我们在本文中重点介绍的功能。我们想要强调的是,通过将 Windows Azure 的固有功能与控制类型框架的复合/DI/反转结合在一起使用,您可以创建一个动态的云应用程序,该应用程序可以轻松地响应似乎总是出现的最新更改。

Joseph Fultz 是 Hewlett-Packard Co. 的软件架构师,参与 HP.com 全球 IT 小组的工作。以前他是 Microsoft 的软件架构师,协助 Microsoft 顶层企业和 ISV 客户定义体系结构和设计解决方案。

Chris Mabry 是 Hewlett-Packard Co. 的开发主管,目前他主要是领导一个团队,基于支持服务的客户端框架提供丰富的 UI 体验。

原文地址:http://msdn.microsoft.com/zh-cn/magazine/jj553511.aspx

posted @ 2012-09-16 09:19  张善友  阅读(1725)  评论(0编辑  收藏  举报