.net 程序集(转)

 

2.1.4  程序集和清单

过去,编程人员在编译Windows C++Visual Basic应用程序时,编译结果是以.exe.dll为扩展名的文件。在.NET中这一点仍然不变,但这些结果文件被赋予了一个新的名称:程序集。当然差别并不仅仅于此。在.NET中,这些文件的内部格式与以前相比有很大区别。.NET之前,DLLEXE文件是包含了有平台特征的代码,而所有的.NET程序集包含了称为通用中间语言(Common Intermediate Language)的跨平台代码。

通常,一个程序集正好由一个DLLEXE文件构成,然而程序集也可由多个DLLEXE组成。用户无需考虑其内部物理构成,就可以使用程序集中定义的类型。因此,从逻辑的角度来看,可以把程序集简单地看着一个类、结构、枚举等的类型集合体。程序集又是通过清单(Manifest)巧妙地实现了这一点。

程序集清单包含了有关程序集的重要信息。以多文件程序集为例,清单列出了构成程序集的全部DLLEXE,还包括版本号、区域信息和类型参考信息等。如果一个程序集依赖于其他程序集,清单中还要列出依赖关系。由于清单信息用于描述程序集,在通用术语中被称为元数据(metadata)。当然,清单并非存放元数据的惟一地方,在程序集的核心,每个实现类型均有与之对应的元数据。其概要是,每个程序集都有完整的自身描述。

程序集有两种形式:私有和共享。私有程序集只能被一个程序使用,而共享程序集能被多个应用程序使用。COM组件与共享程序集存在许多共同点,当然也存在诸多不同之处,而管理多个共享程序集比管理多个COM组件容易许多。但是即使这样,默认的程序集类型仍是私有的,开发人员必须清楚将私有程序集转换成共享程序集的具体步骤。 稍后在本章,读者将了解到更多关于私有和共享程序集的解释。

对于开发分布式程序的编程人员,深刻理解程序集十分重要。程序集是在.NET中可部署的单元,其定义了版本和安全范围。换句话说,程序集也是组件。稍后在本章,读者将看到有关构建程序集的详细细节。

 

2.1.5  中间语言

所有.NET语言编译器生成与平台无关的代码,称为通用中间语言(CIL,简称IL)。从概念上来讲,这一点非常类是Java中的字节码。但与Java字节码所不同的是,Microsoft设计的IL可以由任一语言编译器很容易地生成。

前面提到的,程序集包含IL,而不是源代码。在运行过程中,当第一次调用一个方法,为了更快的执行程序,实时Just-In-Time (JIT)编译器将该方法的IL代码转换成源代码(与平台有关).NET只编译需要在运行库中使用的那部分IL。当然,它将会把转换得到的源代码放入缓存中,以便以后的调用能够直接使用该源代码。

同时考虑到CTSCLRIL.NET提供了除了Windows之外,还能移植到其他操作系统的可能。如果可能的话,用户可以用任何一种语言开发与平台无关的应用程序。当然,这里有一个关键词是“可能”,因为目前.NET只能在Windows操作系统上运行。

 

 

2.2  构建和配置.NET程序集

任何.NET程序均由程序集构成。开发人员在编译自己的应用程序时,编译器将构建程序集;部署一个.NET应用程序伴随着程序集的部署;指定.NET应用程序的版本时,也将同时为应用程序中的每个程序集分配版本号;当使用从其他供应商中得到的组件时,就要调用由该供应商提供的程序集。

这一点很明确:程序集的概念已经渗透到.NET程序开发、发布和其他的方方面面。如果编程人员能够充分地理解程序集这个概念,将能够很快地发现并找出自己开发的或第三方提供的组件中的部署错误、版本管理问题,并且对何时使用共享程序集、何时使用私有程序集等问题做出正确的判断。因此,本节不厌其烦地阐述如何构建和使用程序集。

为了帮助掌握涉及到的细节,下面通过构建两个简单的程序集,逐步深入。一个程序集是名为MathLibraryDLL,它包含了一个SimpleMath类,该类又由一个名为MathClientEXE程序集调用。这两个程序集会用来举例说明许多不同的概念,建议读者能够亲自实践并体会其过程。

 

 

2.2.1  构建私有程序集

下面通过使用C#类库项目类型开始创建一个私有程序集。前面提到的,一个私有程序集只能由一个应用程序调用。首先,我们在Visual Studio中建立一个新项目,并选择一个C#类库项目类型(如图2-1)

2-1  创建一个类库程序集

然后在SimpleMath.cs的文件中输入一下代码:

// InSimpleMath.cs

namespace MathLibrary

{

public class SimpleMath

{

public static int Add(int n1, int n2)

{

return n1 + n2;

}

public static int Subtract(int n1, int n2)

{

return n1 - n2;:

}

}

}

该程序集的惟一目的是提供一个SimpleMath类,以供其他客户端程序集调用,所以没有主函数。

在构建该项目时,会生成MathLibrary.dll文件。但记住,它不同于任何旧的DLL,它是一个.NET程序集。因此,它包含了IL代码,以即前面提到的元数据。

1. 使用程序集类型

现在建立调用SimpleMath类的另一个程序集。这次选择一个C#控制台应用项目,输入以下代码:

using System;

namespace MathClient

{

class MathClient

{

static void Main(string[] args)

{

Console.WriteLine(" 5 + 3 = {0}", MathLibrary.SimpleMath.Add(5,3));

 

Console.WriteLine(" 5 - 3 = {0}",

MathLibrary.SimpleMath.Subtract(5,3));

 

Console.ReadLine();

}

}

}

但是,当对该项目编译时,会出现许多编译错误,这是因为在项目中还没有引用MathLibrary程序集,导致编译器不能识别SimpleMath类类型。

要更正这一错误,打开Add Reference对话框(Project | Add Reference)。单击Browse按钮,查找包含MathLibrary.dll文件的目录,并选择该文件(见图2-2)

2-2  添加一个程序集引用

Visual Basic 6.0编程人员可能对这一过程比较熟悉,因为它与添加COM DLL引用的过程很相似。主要的区别在于,在VS.NETIDE从它当前位置向用户项目中建立的目标路径复制程序集。

在引用MathLibrary程序集之后,就能成功地构建客户项目。图2-3显示了目标生成目录怎样维护构建的项目文件。目前,MathLibrary仍然是私有程序集,这样,只有它与客户端可执行程序同属一个目录,客户程序才可使用它。后面将介绍如何将一个私有程序集驻留在应用程序目录的一个子目录中。

2-3  MathClient目标生成目录

2. 探究程序集清单

现在让我们来探究一下MathClient组件的内容。前面提到程序集清单列出了它需要的其他所有程序集,这里可以通过在ILDasm内查看程序集来测试这一内容。ILDasm.NET框架SDK带的非常有用的工具,专为查看.NET程序集而设计的。图2-4显示了在载入MahtClient程序集后的ILDasm工具。

2-4  ILDasm查看MathClient程序集

双击Manifest节点,ILDasm列出manifest的内容。在manifest的顶端,将看到以下内容:

.assembly extern mscorlib

{

.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )

.ver 1:0:2411:0

}

.assembly extern MathLibrary

{

.ver 0:0:0:0

}

 [.assembly extern]项用来列举当前程序集所依赖的那些程序集。正如所见,MathClient程序集依赖于mscorlibMathLibrary两个程序集。mscorlib程序集是CLR的核心库,包含了许多有用类型。例如它实现了MathClient所使用的System.Console.

当一个客户端程序集需要一个外部程序集定义的类型时,它向CLR程序集加载服务发出程序集请求。程序集请求由程序集名和相应的[.assembly extern]块的所有信息组成。程序集加载器通过一个被称为程序集绑定(assembly binding)的过程,使用该信息来查找和加载请求的程序集。这里,程序集绑定在概念上类似于COM服务控制管理器COM Service Control Manager(简称SCuM)的工作过程,但COM服务控制管理过程从来不检查系统注册,而绑定机制就完全不同。

当前MathLibrary程序集需要记录的信息相当少。下面的摘录详细列出了程序集引用的所有内容。表2-1对每一项做了解释。

.assembly extern SomeLibrary

{

.publickeytoken = (99 CB 5A D9 7D 10 88 C5 )

.ver 1:0:552:41586

.locale = (65 00 6E 00 00 00 )

 

}

2-1  程序集请求详述

程序集请求项

   

.assembly extern SomeLibrary

说明引用外部程序集的友好名称

.publickeytoken = ( …)

惟一表示外部程序集发布者的8位散列。参见“构建共享程序集”一节

.ver=1:0:552:41586

外部程序集的版本号。格式是:major:minor:build:revision

.locale =( … )

表示外部程序集的区域信息。例如:英语、德语。专门用于只提供资源(字符串、位图等)的程序集。默认的区域信息是“neutral

 

如果查看每一个被引用的程序集的记录信息,可能会发现忽视了一个重要的部分:程序集所在的位置。程序集加载器如何找到它的呢?答案是,至少在这种情况下,由于MathLibrary程序集与MathClient程序集同在一个目录,加载器就能找到它。然而应当注意,搜寻当前应用程序所在目录的操作,仅仅是查找外部程序集引用诸多步骤中的一步。事实上,要完成这一系列操作,可以采用被称为“探测(probing)”的过程,它是程序集绑定过程中的一部分。

源代码:

本例的源代码参见Chapter2/BasicAssembly

3. 创建应用程序配置文件

探测过程的第一步是要查找一种特殊格式的文件,被称为应用程序配置文件(application configuration file)。该文件必须存在于应用程序目录中,其文件名与应用程序名相同,外加扩展名.config。例如,MathClient.exe程序集的配置文件命名为MathClient.exe.config。通过编写配置文件,编程人员能够指示加载器在应用程序目录的任一子目录中搜索指定的程序集。

例如,为了清理应用程序的目录结构,可能希望把所有的库,包括MathLibrary,移到名为libs的子目录中。然而,如果执行这样的操作后运行MathClient程序,将会出现如图2-5所示的异常报告。

2-5  从应用程序目录移出被引用的程序集引发一个异常

要解决这一问题,用户可以创建应用配置文件。为创建和维护应用程序配置文件,Visual Studio .NET提供了一种方便实用的机制。首先,到Project | Add New Item,选择Text File图标,输入“app.config”文件名(见图2-6),单击Open

2-6  添加app.config文件

这样就创建与用户源代码和项目文件同属一个目录的app.config文件。但是记住,它必须与应用程序的EXEmathclient.exe.config文件同在一个目录下。事实上,在每次建立项目时Visual Studio .NET自动完成此项操作,甚至能根据目标可执行文件名称的变化,相应改变目标应用配置文件的名称。图2-7显示了在向项目中加入该文件和编译时,该项目和bin/debug目录的情况。

现在在配置文件中输入以下内容:

<configuration>

<runtime>

<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">

<probing privatePath="libs" />

</assemblyBinding>

</runtime>

</configuration>

2-7  在编译时app.config文件自动转换为mathclient.exe.config文件

可以看出,配置文件采用XML格式。根据定义,配置文件必须以根元素〈configuration〉作为开始。这里最终目标是确定privatePath特性,但是之前必须首先确定〈runtime〉和〈assemblyBinding〉元素。如果需要,可以按下面所示指定多个待搜索的子目录名:

<probing privatePath="libs;libs/moreLibs;yetMoreLibs" />

现在再运行MathClient程序,就不会报错。

源代码:

本例的源代码参见Chapter2/Probing

4. 探寻探测过程

探测并不只是简单地搜索当前应用程序目录和指定目录,实际执行的操作还很多。例如,如果程序集请求提供区域信息,那么运行库会为程序集搜索带区域信息的子目录。

在许多复杂的情况下,最好用伪代码对整体探测过程进行描述。设想由一种被称为Db(发音:D-flat)的假想语言写成,一个名为ProbeForAssembly的函数,如下面所示。该函数依据代码逐步执行,一旦搜索成功,立即退出。

function ProbeForAssembly( AsmName, AppBase, Culture, PrivatePath)

// AsmName = The friendly name of the assembly, e.g., MathLibrary

// AppBase = Path where the requesting application resides

// Culture = The assembly reference culture, e.g., "En"

// PrivatePath = The list of  search paths specified  in the app config  file

 

// Search first for DLL extension  then EXE extension.

for each Ext in {"dll", "exe"}

Search( AppBase/AsmName.Ext )

 

if Culture == "neutral" Then

Search( AppBase/AsmName/AsmName.Ext )

else

Search( AppBase/Culture/AsmName.Ext )

Search( AppBase/Culture/AsmName/AsmName.Ext )

end if

 

// Search in all the paths specified  in the app config  file

for each Path in PrivatePath

if Culture == "neutral" Then

Search( AppBase/Path/AsmName.Ext )

Search( AppBase/Path/AsmName/AsmName.Ext )

else

Search( AppBase/Path/Culture/AsmName.Ext )

Search( AppBase/Path/Culture/AsmName/AsmName.Ext )

end if

next Path

next Ext

end function

如果对Db语法不是很清楚地说,这里解释几个要点:

      探测过程用了4条主要信息来作为输入:被请求程序集的友好名称、提出请求程序集的路径、被请求程序集的区域信息以及应用程序配置文件中的privatePath设置。

      探测过程由两个主要工作周期组成。第一个周期搜索一个带DLL扩展名的程序集,第二个周期搜索一个带EXE扩展名的程序集。

      无论是privatePath或者区域信息,探测过程总是先搜索应用程序所在目录。

      探测过程将判断与程序集名称匹配的子目录是否存在。如果存在,就搜索该子目录。

      如果区域信息不是“neutral”,探测过程将判断是否存在与区域信息匹配的子目录名。如果存在,就搜索该子目录。

      如果列举出privatePath中指定的子目录名,探测过程也会搜索目录名与程序集或区域信息匹配的子目录。

5. 运用私有程序集和进行探测的好处

现在花一点时间思考一下针对绑定程序集引用的.NET探测机制到底有什么含义。为了弄清这个问题,现假设MathLibrary是一个由MathClient调用的COM DLL。要部署这个应用程序,安装程序必须复制和注册MathLibrary.dll。一旦部署后,如改变MathLibrary.dll的存放位置,就需重新注册。而且,如果在不希望影响用户使用的情况下部署新的MathLibrary.dll版本,必须对后续的每个步骤进行仔细地检查,以确保新、旧版本间的兼容性。最后,考虑一下如果构建了新的MathClient应用程序,更新了所有支持的COM DLL,但是又想在同一个机器上运行新、旧两个版本可能会出现的什么问题。

好了,现在从假设中回到现实,因为.NET程序集探测机制大大简化了这种情况。如果要部署应用程序,要做的就是复制整个应用程序目录和包含支撑程序集的子目录。只需通过调用一个简单的XCOPY命令即可完成,无需注册。而且,可以改变MathLibrary程序集的位置到应用程序所在目录的任意一个子目录中。在这种情况下,所要做的就是更新配置文件,指示程序集加载器查找相应的目录。因为运行库不会校验私有程序集版本(关于程序集版本的细节,接下来将进行介绍),甚至可以通过复制旧版本,来部署MathLibrary的一个新版本。最后,如果希望完整的MathClient程序新旧两个版本在同一机器上运行,只需将新、旧版本安装在不同的目录中即可。

到现在为止,如果在不需要与其他应用程序共享程序集的情况下,应该认识到私有程序集的好处了。但如需共享程序集,就必须通过额外的步骤将私有程序集转变成共享程序集。

 

 

 

2.2.2  构建共享程序集

无论怎样试图避免与共享程序集打交道,仍然会遇到需要在多个应用程序之间共享程序集的情况。共享程序集与私有程序集主要存在两方面的区别:位置和标识。私有程序集必须为于应用程序所在目录或子目录中,而共享程序集安装在被称为全局程序集缓存(Global Assembly CacheGAC)的一个特定的全局缓存中。在Windows操作系统中,GAC位于Windows系统目录下的Assembly目录中(例如:C:/WinNT/Assembly)

虽然共享程序集友好名称与私有程序集一样,但是.NET运行库通过强名(strong name)(也称共享名:shared name)来标识共享程序集。一个强名称由友好名称(MathLibrary)、区域信息(如:英语)、版本号(1.2.0.0)、公钥(public key)和数字签名等组成。这里,需要强名称提供的严格认证等级,因为:

      希望本公司开发的程序集具有唯一的名称,且不会被其他公司复制。

      希望针对不同的实现方式或不同的区域信息,能够共享多个不同版本的程序集。

      防止黑客的“特洛伊木马”程序集替代合法程序集,利用有效访问权限对系统进行严重破坏。

为了更为清楚地说明这些问题,下面举例说明MathLibrary程序集转换成共享程序集的步骤。

1. 在程序集中使用强名称

MathLibrary转换成共享程序集首先需要生成一个强名称。如前所述,强名称实际上是许多程序集信息的集合体。现在如果我们检查一下MathLibrary程序集清单,会发现以下内容:

.assembly MathLibrary

{

// Note, some lines cut for clarity ...

.hash algorithm 0x00008004

.ver 0:0:0:0

}

注意:

如果按照前面例子中程序集建立步骤练习,并希望查看清单中的详细内容,建议注释掉AssmblyInfo.cs文件中的内容。Visual Studio会在项目创建时自动生成该文件。

这里[.assembly] 标记不带“extern”,表示该部分的信息用于当前程序集。而黑体部分内容表示友好名称(MathLibrary)和版本号(0:0:0:0),而要创建强名称,仍然缺少一些信息。

看起来像缺少区域信息。实际上,如果不专门声明,该程序集的区域信息默认为“neutral”。如果建立的程序集内部包含各种资源,如字符串、位图等,就需要根据不同的语言定制区域信息。这种类型的程序集称为附属程序集(satellite assembly),规定不含任何代码。由于MathLibrary包含了特定的代码,也就无须使用专门的区域信息。

不对称密码术和强名称

强名称让人迷惑不解的地方在于使用不对称(也称公钥)密码技术。注意运用这一密码体系的目的不是对程序集的内容加密,而是为下列操作提供安全保障:

      一旦程序集被建立或安装后,任何人不能篡改程序集的内容。

      任何两个发布者不能生成具有相同强名称的程序集。

      程序集的全部版本均来自同一发布者。

下面解释一下不对称密码术的工作机理。不对称密码术包含两种密钥:公钥和私钥。这些密钥通过数学的方法联系起来,只有相应的公钥才能将由私有密钥加密的数据解密。当用户建立一个共享程序集,也相应提供带这一公钥/私钥对的编译器(在下一节读者可了解到这一工作过程)。编译器使用私钥对程序集的内容进行“数字签名”。签名的过程就是对程序集的内容进行散列编码,编码结果大约几百字节。然后使用私钥对其进行加密,得到程序集的数据签名。该程序集在它的清单中存放公钥,并在CLR能够访问的某个位置嵌入数字签名。

现在程序集就可以在GAC中进行安装了。在安装时,GAC需获取程序集的数字签名,并用存放在程序集清单中的公钥将其解密,从而得到编译器编译时生成的程序集的散列值。然后GAC重新使用在前面编译过程中使用的散列算法对程序集的内容进行散列编码。如果这个散列值与在数字签名中的散列值相同,说明程序集内容没有被改变。

现在解释一下如何从其他程序集引用这个共享程序集。当编译器编译消费程序集(consuming assembly)时,它从共享程序集的清单中获取公钥,并将其散列编码成一个八字节的数值,该数值称为公钥令牌(public key token)。散列编码过程的目的是将公钥压缩成更小的数据块。由于公钥内容相当大,任何一个程序集需要存储多个被引用的共享程序集所提供的公钥。因此,消费程序集仅在它的清单中存放公钥令牌。尽管这是压缩格式,从统计学的角度来看,公钥令牌仍然能够区分出所需程序集的发布者。

注意:

.NET以前的beta版中将公钥令牌称为始发者(originator)。读者在阅读以前的文档时,请注意这一差别。

最后,讨论一下当消费程序集需要载入共享程序集中的类型时,完成了哪些操作。首先,当运行库调用共享程序集时,它读取共享程序集的公钥,并产生一个公钥令牌。然后将该令牌与存放在用户程序集清单中的公钥令牌进行对比。如果结果匹配,运行库校验成功,说明从发布者中得到的共享程序集正是用户需要的;如果不匹配,运行库就发出一个异常警告。

在完成公钥令牌校验后,运行库将共享程序集的内容进行散列编码,并将结果与存放在共享程序集中的数字签名值进行对比。实质上,当用户安装共享程序集时,它模拟了GAC的执行过程。不过,运行库要进行检查,以确保没有人对被安装在GAC的程序集进行过恶意篡改。

除非对不对称密钥密码术很熟悉,本节的内容会使读者头晕脑涨。但是应充满信心。在下一节,将了解到如何将这些知识运用到MathLibrary程序集。在进行后续内容之前,必须明白前面部分介绍的内容是一种简化,其假定程序集只有一个文件。多文件程序集使过程变得复杂,但是最终的结果是一样的。

生成公钥/私钥对

回忆一下,在对共享程序集进行签名时,编译器需要一个公钥/私钥对。这里可以通过使用强名称命令行工具(sn.exe)产生这些密钥。该命令有多个可选项,但大多数情况下仅需要掌握-k选项。该选项命令工具在产生一个新的公钥/私钥对,并将其存入指定的文件(见图2-8)

2-8  使用强名称实用程序(sn.exe)

前面提到,编译器使用这些密钥对程序集进行数字签名,但是之前必须告知密钥存放位置。可以使用名为AssemblyKeyFile的程序集特性指定存放密钥的文件路径,并以程序化的方式实现该操作。示例代码如下所示:

// The AssemblyKeyFile attribute in this namespace

using System.Reflection;

[assembly: AssemblyKeyFile(@"D:/MyKey.snk")]

如果程序集特性出现在任何命名空间或类声明之前,那么可以将这一代码放在项目的任何代码文件中。然而,一般存放的位置是在AssemblyInfo.cs文件中。当构建一个新的C#项目时,Visual Studio自行将该文件加入项目。

现在构建MathLibrary程序集时,编译器使用密钥对对程序集进行签名,并可以通过ILDasm在程序集清单中查看结果并确认,结果如图2-9所示。

请注意身份块的[.publickey]项。正如所料,这就是公钥。MathLibrary程序集现在就可以安装在GAC中了。

2-9  使用ILDasm查看MathLibrary清单中的公钥

对于组件供应商来说,把公钥/私钥对存放在安全可靠的地方非常重要。它就是您的身份,绝对不能丢失,绝对不能泄露给其他人。如果丢失了,需要重新创建,基于以前构建的程序集的客户端如果不重新编译,就不能与后续版本的程序集进行绑定。如果一个黑客得到该密钥对,他(或她)就可能使用您的身份发布一个恶意的程序集版本,仅通过更新机器配置文件就能引导客户转到他(或她)的程序集。

警告:

把公共/私钥对存放在安全可靠的地方非常重要。

重建MathClient程序集

在将MathLibrary程序集安装入GAC之前,可通过建立MathClient程序集来观察MathLibrary发生的变化。首先检查MathClient的清单,查看它现在是如何引用MathLibrary程序集的,如图2-10所示。

2-10  在基于强名称的MathLibrary建立之前的MathClient的清单

2-11显示了在针对MathLibrary程序集新的强名称版本构建MathClient后的引用情况。注意在程序集引用信息中增加了[.publickeytoken]一项。

技巧:

为了使Visual Studio识别程序集有了强名称,应该从MathClient项目中删除对MathLibrary引用,而在后面再添加进来。也可通过在Solution Explorer窗口中突出显示MathLibrary引用,并在Property窗口检查Strong Name属性来断定Visual Studio可以识别强名称。

如果运行MathClient程序,会发现结果与以前一样,说明即使MathLibrary有了强名称,仍然能够作为私有程序集使用。在下一节,将示例如何在GAC中安装MathLibrary,并作为共享程序集使用。

2-11  在基于强名称的MathLibrary建立之后的MathClient的清单

GAC中安装程序集

.NET通过特殊的外壳扩展(shfusion.dll)允许在Windows资源管理器中浏览GAC。在GAC中安装MathLibrary程序集最简单的办法是在Windows资源管理器中打开它,“拖放”MathLibrary.dll即可,与将一个文件复制到其他目录的方法一样。

2-12  GAC中查看MathLibrary程序集

在批处理文件和安装文件中可以使用gacutil.exe命令安装共享程序集。选项/i将指定的程序集安装到GAC中。

gacutil /i MathLibrary.dll

注意:

只有具备计算机管理员权限才能在GAC中安装程序集。

不论什么运行机制,一旦在GAC中安装了MathLibrary,计算机上的任一程序集都可能使用它。为了证明这一点,可以删除MathClient的应用程序配置文件(MathClient.exe.config)MathLibrary.dll的本地拷贝,然后运行MathClient.exe。由于加载器现在能在GAC中找到MathLibrary程序集,因此应用程序可以正常运行。

回想前面从MathClient项目中添加对私有MathLibrary程序集的引用,Visual Studio自动向应用程序所在目录复制程序集。但现在MathLibrary具有了强名称并安装在GAC中,如果再次引用这个程序集,Visual Studio就不再复制了。用户可以通过查看Property窗口中的Copy Local属性,检查Visual Sudio是否每次构建时都进行程序集复制操作(如图2-13所示)

2-13  Visual Studio中的MathLibrary引用的Copy Local属性

最后需要注意的,读者可能会对下面的问题感到疑惑:如果向用户应用程序目录中也复制MathLibrary程序集会出现什么情况,到底哪一个拷贝会被载入呢?是GAC中的呢还是应用程序目录中的呢?答案还要看在MathClient清单中是如何引用程序集的。如果MathClient清单中存在MathLibrary程序集的公钥令牌,那么运行库首先检查GAC;如果程序集没有在GAC中,此时运行库就要进行探测。另一方面,如果在MathClient清单中没有MathLibrary公钥令牌,那么运行库直接进行探测,不再查找GAC。由于程序集引用的公钥令牌是空的,不可能与找到的程序集的公钥令牌8609A7F82BCFCECE匹配,因此后者肯定失败。但是,由于引用带有强名称的程序集时编译器自动生成一个公钥令牌,所以后者绝不会发生。

这里涉及到的部分内容会在本章“绑定过程小结”中阐述。

2. 使用延迟签名(Delayed Signing)

由于用于对强名程序集签名的公钥/私钥的敏感性,有的机构希望尽量避免将密钥对分发给软件开发人员。另外,出于测试的目的,软件开发人员需要建立一个接近实际的部署环境。这就意味着,要共享一个程序集,最好在开发过程中按照希望的方式开发和测试。如果不生成程序集的强名称,就不可能做到这点。

遇到这种难题的解决办法是采用延迟签名(delayed signing)。在这个过程中,只有公钥提供给开发人员。正常情况下,这个公钥嵌入在程序集清单中,利用该程序集建立的客户端程序集,将相应的公钥令牌存放进它自己的程序集清单中。但是,由于没有私钥,程序集没有进行数字签名。那么在部署应用程序时,所有程序集就可以转到负责维护该机构的私钥和进行数字签名的人员手中。

延迟签名的第一步是从密钥对中获取公钥。强名称工具(sn.exe)专门为此提供了-p选项。下面命令演示了如何从Mykey.snk中获取公钥、并将其存入MyPublicKey.snk文件的方法。

sn -p MyKey.snk MyPublicKey.snk

这个公钥文件现在就可以采用延迟签名,免费发布给所有的软件开发人员。有了公钥后,还需告诉程序集应使用延迟签名。这一步可利用设置程序集特性AssemblyDelaySign来实现。

 

例如:

// Specify the use of delayed signing

[assembly: AssemblyDelaySign(true)]

 

// Specify the location of the public key

[assembly: AssemblyKeyFile(@"D:/MyPublicKey.snk")]

需要注意的是,AssemblyKeyFile特性只指定了前面创建的公钥文件存放的位置,而不是指含有整个密钥对的文件本身。

当程序集建立好了以后,编译器将公钥放在清单中,并且为数字签名预留好空间。但是,由于没有进行数字签名,在GAC中安装程序集或客户端在运行时加载它时,须将签名验证功能关闭。可以使用强名称工具的-Vr选项完成此项操作,如下例所示:

sn -Vr mathlibrary.dll

现在,无论何时客户端向该程序集提出请求,验证过程均被忽略。而且,如具备一个完整的强名称,就可以将该程序集安装在GAC中,用以开发客户程序。由于该程序集清单包含合法的公钥,针对该程序集建立的客户端程序集包含了合法的公钥令牌,因而在程序集使用私钥签名时无需重建。记住前面提到的命令不会改变程序集本身,相反,它仅仅是注册程序集,使其只在当前机器中省略验证过程。如果要恢复对程序集的校验,使用-Vu选项即可。

当然,一旦完成应用程序设计,这个延迟签名程序集就必须在部署之前用私钥完成签名。这时读者可能已猜到,强名称工具也提供了一个选项:-R。下面的例子显示了用MyKey.snk文件中的私钥对MathLibrary程序集进行签名。

sn -R mathlibrary.dll d:/mykey.snk

3. 区别术语:强名称与共享

坦率地说,作者本人并不喜欢私有程序集、共享程序集这样的术语,因为这些术语只涉及到程序集如何使用,并没有描述其的内部结构。例如,强名称程序集是私有程序集还是共享程序集,主要是看它是否被安装在GAC中。但是,需要使用强名称时,往往不正确地使用了“共享”这个词。

基于这个原因,作者以后只是在描述安装在GAC中的强名称程序集时才使用“共享(shared)”这个词,用常规程序集(regular assembly)来描述没有强名称的程序集。当程序集的用法比它的内部结构更重要时,使用私有程序集这个词汇。如果需要,会使用带限定词汇的完整术语来描述程序集引用,例如“强名称私有程序集“或者“常规私有程序集”。

 

2.3  理解.NET版本控制

提到“版本控制”一词,许多COM开发人员表现出极度的反感。虽然COM版本规则很简单,但是实现起来并非易事。而且,像Visual BasicVisual C++这样的Windows开发工具,也在版本升级过程中不断变化。Visual Basic 6.0在一个简单的对话框后面隐藏了许多深层次的版本细节,这些细节使得在出现版本错误时不能为开发人员提供更多的帮助。Visual C++ 6.0则把繁琐的版本工作全部交给了开发人员来做。.NET版本的解决方案与之相比,更显卓越。但是需要记住,无论什么版本,其固有的复杂性需要一个开发团队的所有成员付出努力。

 

2.3.1  设置程序集的版本信息

一个程序集的版本由存放在清单中的四部分版本号组成,每部分通常使用句点(.)或冒号(:)作为分隔符。

<Major version>.<Minor version>.<Build number>.<Revision>

开发人员可以利用AssemblyVersion特性来设置一个程序集的版本号,和AssemblyKeyFile特性一样,AssemblyVersion特性通常位于AssemblyInfo.cs文件中。

// Format: <Major version>.<Minor version>.<Build number>.<Revision>

[assembly: AssemblyVersion("1.0.*")]

在前面的例子中,主版本号是1,次版本号是0,星号告诉编译器自动生成内部版本号和修订号。然后编译器将得到的版本号存入清单中。现在可以在MathLibrary程序集上尝试一下。图2-14显示的是编译完项目之后的清单情况。

如果愿意的话,编程人员也可按如下例子的格式明确地设置自己的生成版本号和修订号。

2-14   清单中记录的MathLibrary的版本号

// Format: <Major version>.<Minor version>.<Build number>.<Revision>

[assembly: AssemblyVersion("1.0.5.121")]

无论什么时候,只要在构建项目时引用了其他程序集,那么编译器会将所有被引用的程序集的版本号存入清单中。现在重建MathClient程序集,再查看其清单。图2-15显示了该客户端清单记录的版本信息。

前面提到,CLR使用程序集引用中的信息,在运行中将其与正确的程序集绑定。如没有更多说明,CLR仅仅绑定到带有这些特性的程序集。只有运行库能够找到版本号为1.0.550.39732MathLibrary程序集,MathClient程序才能成功运行。但是注意,这项策略只适合于强名称程序集。如果一个常规程序集具有相应的版本号,运行库会将其忽略掉。

2-15  MathClient清单中记录的MathLibrary的版本号

注意:

早期的beta.NET有一个概念,称为快速修复引擎QFE(Quick Fix Engineering),被作为默认的版本控制策略。如果程序集的修订号和内部版本号与请求的版本号不同,则允许运行库载入一个程序集。实际中,该策略被证明条件太宽松,逐步被称作发布者策略(publisher policy)(后面将讨论)的更为明确的绑定形式所代替。如果在旧文档中遇到该术语请记住这点。

在需要更新被引用的程序集之前,会一直觉得这种严格的版本策略很好。如果,在部署应用程序后发现MathLibrary程序集中存在错误,如何构建和部署一个没有错误的程序集呢?这里有两个选择。

      更新和部署整个应用程序。编程人员可以修复MathLibrary程序集,并利用开发环境重建整个应用程序。这将更新MathClient清单中的引用版本,然后把整个应用程序作为一个单元进行部署。在上面所举的特殊例子中,这种方法较为可行,但是如果是一个带有许多程序集的大型程序,就不太实用。

      更新和部署MathLibrary程序集。很显然,这是一种更为可取的方法。但是如何使以前部署的MathClient应用程序与MathLibrary程序集的新版本正确的绑定呢?如果是为了不改变新程序集的版本而固定版本号,就可能导致整个版本号的失效。要解决这类棘手问题,关键在于应用程序配置文件。

 

 

2.3.2  再论应用程序配置文件

前面提到,使用应用程序配置文件来列出运行库探测被引用程序集的子目录清单。也可以使用该文件,利用<bindingRedirect>元素,将对一个特定的程序集版本的请求重定向到另一版本。这种方案的好处在于无需重新编译应用程序就可以绑定新程序集。

例如,假如已经修复并重建好MathLibrary程序集,它的新版本号为1.0.550.41003,需将MathClient程序集重定向加载它,替代清单中所列的1.0.550.39732版本。以下配置文件完成该功能:

<configuration>

<runtime>

<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">

<probing privatePath="libs" />

<dependentAssembly>

<assemblyIdentity name="MathLibrary"

publicKeyToken="99cb5ad97d1088c5" />

<bindingRedirect oldVersion="1.0.550.39732"

newVersion="1.0.550.41003" />

</dependentAssembly>

</assemblyBinding>

</runtime>

</configuration>

该例中,使用了<bindingRedirect>元素的oldVersion特性来指定需要重定向的版本号,newVersion特性来指定重定向的版本号。如果希望将多个旧版本重定向到同一个新版本,也可用OldVersion特性指定一个范围。例如:

<bindingRedirect oldVersion="1.0.0.0 - 1.0.550.39732"

newVersion="1.0.550.41003" />

<bindingRedirect>元素实际上是<dependentAssembly>的一个子元素,每一个<dependent Assembly>元素为在<assemblyIdentity>元素中指定的程序集封装绑定信息。<assemblyIdentity>提供的namepublicKeyToken特性用来指定需要重定向的程序集。这就意味着配置文件可以有多个<dependentAssembly>元素,每一个都可以将被引用的程序集重定向到一个新的版本。例如:

<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">

<probing privatePath="libs" />

<dependentAssembly>

<assemblyIdentity name="MathLibrary"

publicKeyToken="99cb5ad97d1088c5" />

<bindingRedirect oldVersion="1.0.550.39732"

newVersion="1.0.550.41003" />

</dependentAssembly>

<dependentAssembly>

<assemblyIdentity name="AnotherLibrary"

publicKeyToken="99cb5ad97d1088c5" />

<bindingRedirect oldVersion="1.0.337.67001"

newVersion="2.0.622.12345" />

</dependentAssembly>

</assemblyBinding>

 

 

2.3.3  设置机器范围的版本策略

应用程序配置文件的一个局限在于它只能对单个应用程序起作用,如果希望将所有引用从一个程序集版本重定向到另一个版本,而不考虑应用程序本身,那应该怎么办?要实现这一点,可以使用机器配置文件(machine configuration file)。该文件的路径是<.NET Install Path>/Config/Machine.config.

机器配置文件和应用程序配置文件采用类似的格式。虽然前者为配置诸如ASP.NET等机器范围系统而提供了更多的元素,但是重定向请求的方式却是一样的。而对于冲突设置,应用程序配置文件则优于机器配置文件。

 

 原文章地址

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值