部署程序集
在部署程序集之前,先看一下程序集的分类。
因为不同的公司可能会开发出有相同名字的程序集来,如果这些程序集都被复制到同一个相同的目录下,最后一个安装的程序集将会代替前面的程序集。而一个程序运行时,刚好需要加载前面的程序集,这个时候很有可能会出错,因为不同公司开发的程序集虽然名字一样,功能可能会差别很大。这就是著名的Windows “DLL Hell”出现的原因。
很明显,简单的用文件名来区分程序集是不够的,CLR需要支持某种机制来唯一的标识一个程序集。这就是所谓的强命名程序集。
一个强命名程序集包含四个唯一标志程序集的特性:文件名(没有扩展名),版本号,语言文化信息(如果有的话),公有秘钥。这些信息存储在程序集的清单(manifest)中。清单包含了程序集的元数据,并嵌入在程序集的某个文件中。
下面的字符串标识了四个不同的程序集文件:
“MyType, Version=1.0.1.0,
Culture=neutral, PublicKeyToken=bf5779af662fc055”
“MyType, Version=1.0.1.0,
Culture=en-us, PublicKeyToken=bf5779af662fc055”
“MyType, Version=1.0.2.0,
Culture=neturl, PublicKeyToken=bf5779af662fc055”
“MyType, Version=1.0.2.0,
Culture=neutral, PublicKeyToken=dbe4120289f9fd8a”
如果一个公司想唯一的标识它的程序集,那么它必须首先获取一个公钥/私钥对,然后将公有秘钥和程序集相关联。不存在两个公司有同样的公钥/私钥对的情况,正是这种区分使得我们可以创建有着相同名称,版本和语言文化信息的程序集,而不引起任何冲突。
与强命名程序集对应的就是所谓的弱命名程序集。(其实就是普通的没有被强命名的程序集)。有时候又叫私有程序集。两种程序集在结构上是相同的。都使用相同的PE文件格式,PE表头,CLR表头,元数据,以及清单(manifest)。二者之间真正的区别在于:强命名程序集有一个发布者的公钥/私钥对签名,其中的公钥/私钥对唯一的标识了程序集的发布者。利用公钥/私钥对,我们可以对程序集进行唯一性识别、实施安全策略和版本控制策略,这种唯一标识程序集的能力使得应用程序在试图绑定一个强命名程序集时,CLR能够实施某些“已确知安全”的策略(比如只信任某个公司的程序集)。
如何创建强命名程序集(Strong Name Assembly)
创建一个强命名程序集首先需要获得一个用强命名实用工具 (Strong Name Utility,即SN.exe,.NET SDK自带)产生的密钥。
下面简要介绍一下SN.exe的一些用法。 要产生一个公钥/私钥对:
a)SN –k MyCompany.Keys
该命名告诉SN.exe创建一个名为MyCompany.keys的文件。MyCompany.keys文件将包含以对以二进制格式存储的公有密钥和私有密钥。
b)查看公有密钥:
首先生成一个只包含公有密钥的文件: SN –p MyCompany.keys MyCompany.PublicKey
然后用-tp参数查看:SN –tp MyCompany.PublicKeys
Public key is
00240000048000009400000006020000002400005253413
10004000001000100bb7214723ffc13901343df4b9c464ebf
7ef4312b0ae4d31db04a99673e8163768cc0a2a7062e731d
beb83b869f0509bf8009e90db5c8728e840e782d2cf928dae
35c2578ec55f0d11665a30b37f8636c08789976d8ee9fe9a5
c4a0435f0821738e51d6bdd6e6711a5acb620018658cce93
df37d7e85f9a0104a5845053995ce8
Public key token is 2dc940d5439468c2
创建好了公钥/私钥对,创建强命名程序集就很容易了。只需要把System.Reflection.AssemblyKeyFileAttribute特性加入到源代码中就可以了。
说明:公钥/私钥对文件的扩展名可以是任意的(也可以没有),因为编译的时候都是以元数据的格式读取的。
根据程序集的不同,相对应就有两种不同的部署方式:私有方式与全局方式
a)私有方式
和应用程序部署在同一目录下的程序集称作私有部署程序集。弱命名程序集只能进行私有部署。
b)全局方式
全局部署方式将程序集部署在一些CLR已确知的地方,当CLR搜索程序集时,它会知道到这些地方去找。强命名程序集既可以进行私有部署,也可以进行全局部署。
表11-2列出了两种程序集在两种不同的部署方式的适用情况。
表11-2
程序集种类 | 是否可以进行私有部署 | 是否可以进行全局部署 |
普通程序集 | 是 | 否 |
强命名程序集 | 是 | 是 |
如何部署强命名程序集(Strong Name Assembly)和GAC
a)GAC的概念
如果一个Assembly要被多个应用程序访问,那么他就必须放在一个CLR已确知的目录下,并且CLR在探测到有对该Assembly的引用时,它必须能自动到该目录下寻找这个程序集。这个已确知的目录称作GAC(Global Assembly Cache),就是全局程序集缓存。它一般位于下面的目录下:<System Drive>:/Windows/Assembly/GAC。 GAC的作用就是提供给CLR一个已知的确定的目录去寻找引用的程序集。
b)GAC的内部结构
GAC是一个特殊的结构化的目录,用Windows Explorer浏览你会以为它只是一个包含很多程序集的普通目录。其实不是这样的,在命令行下查看,你会发现它实际上包含很多子目录,子目录的名字和程序集的名称是相同的,但它们都不是实际的程序集,实际的程序集位于程序集名对应的目录下。比如进入GCFWK子目录,会发现其中又有很多的子目录。机器内每一个安装到GAC的GCFWK.dll在GCFWK中都会有一个子目录。如图11-13所示。
图11-13
这里只有一个目录表明只有一个版本的GCFWK程序集被安装。实际的程序集保存在每一个对应的版本目录下。目录的名称以下划线的形式分割为“(Version)_(Culture)_(PublicKeyToken)”。GCFWK的语言文化信息为netture,就表示为“0.0.0__bf5779af662fc055”。表示的意义是: “GCFWK, Version=1.0.0.0, Culture=neutral,PublicKeyToken=bf5779af662fc055” 如果语言文化信息为“ja”,就表示“1.0.0.0_ja_bf5779af662fc055”,表示的意义是: “GCFWK, Version=1.0.0.0, Culture=ja, PublicKeyToken=bf5779af662fc055”。
c)部署强命名程序集到GAC
GAC包含很多子目录,这些子目录是用一种算法来产生的,我们最好不要手动将程序集拷贝到GAC中,相反,我们应使用工具来完成这样的工作。因为这些工具知道GAC的内部结构。
在开发和测试中,最常用的工具就是GACUtil.exe。 在GAC中注册程序集跟COM注册差不多,但相对更容易:
1.把程序集添加到GAC中: GACUtil /i sample.dll (参数/i是安装的意思)
2.把程序集移出GAC GACUtil /u sample(参数/u就移除的意思,不要带后缀名)
注意:不能将一个弱命名程序集安装到GAC中。
如果你试图把弱命名程序集加入到GAC中,会收到错误信息:”
Failure adding assembly to the cache: Attempt to install an assembly without a strong name”
d)强命名程序集的私有部署
把程序集安装到GAC有几个好处。首先,GAC使得很多程序可以共享程序集,这从整体上减少了使用的物理内存;其次,很容易将一个新版的程序集部署到GAC中,并通过一种发布者策略(差不多就是一种重定向方法,比如将原来引用版本为1.0.0.0程序集的程序,通过更改它的配置文件,转而让程序去引用版本为2.0.0.0的程序集)来使用新版本;最后,GAC还提供了对不同版本程序集的并存(side-by-side)管理方式。但是,GAC的安全策略通常只允许管理员更改,同时,向GAC中安装程序集也破坏了.NET框架的简单拷贝部署的许诺。
除了向GAC或者以私有部署方式部署强命名程序集之外,还可以将强命名程序集部署在仅为一小部分程序知道的某个任意目录下。配置每一个应用程序的XML配置文件,让它们指向一个公有目录,这样,在运行时,CLR将知道到哪里去找这个强命名程序集。但这样又有可能会引发“DLL Hell”的问题,因为没有哪个程序可以控制这个程序集何时被卸载。这在.NET中也是不被鼓励的。
CLR如何解析类型引用?
CLR在解析一个被引用的类型时,它可以在以下三个地方的其中之一找到该类型:
1.同一个文件。
对同一个文件中类型的访问在编译时就已经确定下来了,CRL直接从该文件中加载被引用的类型。完成加载后,程序将继续运行。
2.不同的文件,相同的程序集
CLR首先确保被引用的文件在当前程序集清单中的FileDef表内。CLR然后会在加载程序集清单文件的目录中查找被引用的文件。该文件被加载的同时,CLR会检查它的散列值以确保文件的完整性,之后便会找到相应的类型成员。完成加载后,程序将继续运行
3.不同的文件,不同的程序集
当被引用的程序集在一个不同的程序集文件中时,CLR会首先加载包含被引用程序集的清单所在的文件。如果该文件没有包含所需要的类型,CLR会根据此清单文件加载适当的文件。这样也会找到相应类型的成员。完成加载后,程序将继续运行。
如果在解析类型引用的过程中出现任何错误,比如文件找不到,文件不能被加载,散列值不匹配等等,系统将会抛出相应的异常。
现在就通过几个实际的例子来全程演示程序集部署的理论。
首先先看一下这几个例子中需要的材料。
程序集AssemblySetup,版本1.0,弱命名程序集,由TestApp1来引用。在该程序集中实现ShowMe方法。该方法打印出“我是弱命名程序集版本1.0”。该演示程序存放在目录AssemblySetup1.0中。
程序集AssemlySetup,版本2.0,弱命名程序集,由TestApp2来引用。在该程序集中实现ShowMeMessage方法。该方法打印出“我是弱命名程序集版本2.0”。该演示程序存放在目录AssemblySetup2.0中。
程序集AssemblySetup,版本1.0,强命名程序集,由TestApp3来引用。在该程序集中实现ShowMeMessage方法。该方法打印出“我是强命名程序集版本1.0,公司A出品”。该演示程序存放在目录AssemblySetup1.0A中。
程序集AssemblySetup,版本1.0,强命名程序集,由TestApp4来引用。在该程序集中实现ShowMeMessage方法。该方法打印出“我是强命名程序集版本1.0,公司B出品”。该演示程序存放在目录AssemblySetup1.0B中。
所有的程序集都采用相同的命名空间VS2008.Chapter11.AssemblySetup。类名也采用相同的类名AssemblySetup。两个强命名程序集使用不同的公钥/密钥对来区别。
打开VS2008,在目录AssemblySetup1.0中创建一个类库项目AssemblySetup,删除掉原先生成的Class1.cs文件,添加新类AssemblySetup,填写如下代码:
namespace VS2008.Chapter11.AssemblySetup
{
public class AssemblySetup
{
public void ShowMe()
{
Console.WriteLine("我是弱命名程序集版本1.0");
}
}
}
然后在该解决方案中添加一个控制台应用程序TestApp1,让该项目设置为启动项目,添加对AssemblySetup.dll的引用。然后在Program.cs文件中添加代码。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using VS2008.Chapter11.AssemblySetup;
namespace TestApp1
{
class Program
{
static void Main(string[] args)
{
try
{
AssemblySetup ams = new AssemblySetup();
ams.ShowMe();
}
catch
{
Console.WriteLine("出现异常,可能是程序集引用错误!");
}
Console.ReadKey();
}
}
}
程序运行正常。
在程序集生成的目录中生成了AssemblySetup.dll文件,查看属性中的版本信息,如图11-14所示。
图11-14
同样的道理,在目录AssemblySetup2.0添加类库AssemblySetup,实现的代码如下:
namespace VS2008.Chapter11.AssemblySetup
{
public class AssemblySetup
{
public void ShowMeMessage()
{
Console.WriteLine("我是弱命名程序集版本2.0");
}
}
}
注意,这个AssemblySetup类的实现的方法与第一个方法的名称不同,而且在生成之前,一定将该项目中文件AssemblyInfo.cs的[assembly: AssemblyVersion("2.0.0.0")]中的版本信息改为2.0,点击生成。同样的道理添加测试程序TestApp2,给TestApp2项目添加一个应用程序配置文件App.config。在Program.cs文件中添加代码。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using VS2008.Chapter11.AssemblySetup;
namespace TestApp1
{
class Program
{
static void Main(string[] args)
{
try
{
AssemblySetup ams = new AssemblySetup();
//注意这里调用的方法名与第一个不同
ams.ShowMeMessage();
}
catch
{
Console.WriteLine("出现异常,可能是程序集引用错误!");
}
Console.ReadKey();
}
}
}
程序运行正常,并且在AssemblySetup项目的debug目录中查看程序集AssemblySetup的版本信息如图11-15所示:
图11-15
在TestApp2的debug目录下,看到有程序集AssemblySetup.dll,现在在debug中新建一个目录sub,将AssemblySetup.dll移动到sub目录中,然后在执行文件,将会出现异常:
未处理的异常: System.IO.FileNotFoundException: 未能加载文件或程序集“AssemblySetup, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null”或它的某一个依赖项。系统找不到指定的文件。
现在将实例AssemblySetup1.0中的AssemblySetup.dll复制到该目录中,重新运行程序,会出现另一个异常:未处理的异常: System.MissingMethodException: 找不到方法:“Void VS2008.Chapter11.AssemblySetup.AssemblySetup.ShowMeMessage()”。
在 TestApp1.Program.Main(String[] args)
说明虽然二者的版本不同,但是TestApp2测试程序仍然将该程序集视为自己引用的程序集,从这个操作可以看出弱程序集采用的是使用程序集文件的名称来标志一个程序集,而不是像强程序集使用文件名+版本号+区域性+公钥的办法来标识一个程序集。
现在更改TestApp2的配置文件TestApp2.exe.config,添加搜索程序集的私有目录。下面提供一个指定一个私有目录的样本。配置文件的全部内容如下:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="sub"/>
</assemblyBinding>
</runtime>
</configuration>
重新运行程序,仍然是System.MissingMethodException: 找不到方法异常,将先前拷贝过来的AssemblySetup.dll删除。再重新运行程序。程序运行正常,输出:“我是弱命名程序集版本2.0”这句话。
从上面的操作可以看出,程序在搜索弱程序集的方法:首先,在当前目录(工作目录)寻找,如果找到与自己引用相同文件名的程序集,加载,而不管这个程序集的版本等信息,只认程序集的文件名。如果找不到,才从配置文件中读取配置文件提供的私有目录,在私有目录中寻找。如果没有找到,将会给出异常System.IO.FileNotFoundException: 未能加载文件或程序集。
现在实现两个强程序集AssemblySetup。在AssemblySetup1.0A目录中新建类库项目,项目名为AssemblySetup。在文件AssemblySetup.cs输入代码:
namespace VS2008.Chapter11.AssemblySetup
{
public class AssemblySetup
{
//方法名为ShowMeMessage
public void ShowMeMessage()
{
Console.WriteLine("我是强命名程序集版本1.0,公司A出品");
}
}
}
为了与强命名程序集做比较,现在先生成程序集AssemblySetup.dll,使用ILDASM查看程序集清单,看到图11-16。
图11-16
为了使该程序集成为强命名程序集需要生成一个公钥/密钥对。使用SN.exe工具生成一个包含公钥密钥的文件CompanyA.keys,将密码文件放入E:/CompanyA,图11-17所示。
图11-17
接着生成包含公钥的文件CompanyA.publickeys,如图11-18所示。
图11-18
然后查看公钥密码,如图11-19所示。
图11-19
然后将CompanyA.keys文件拷贝到项目AssemblySetup下,在该项目的文件AssemblyInfo.cs中添加程序集属性AssemblyKeyFile用来指定公钥/密钥文件。即添加[assembly: AssemblyKeyFile("CompanyA.keys")],重新生成程序集AssemblySetup.dll。仍然采用ILDASM工具打开该程序集,从程序集清单中可以看到图11-20。
图11-20
可以看到程序集清单中多了公钥,内容与先前使用工具SN查看的一致。
继续在该项目解决方案AssemblySetup中添加测试控制台程序TestApp3,添加引用,设为启动项,然后添加代码。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using VS2008.Chapter11.AssemblySetup;
namespace TestApp3
{
class Program
{
static void Main(string[] args)
{
try
{
AssemblySetup ams = new AssemblySetup();
ams.ShowMeMessage();
}
catch
{
Console.WriteLine("出现异常,可能是程序集引用错误!");
}
Console.ReadKey();
}
}
}
运行程序输出:我是强命名程序集版本1.0,公司A出品
采用同样的办法在目录AssemblySetup1.0B中生成第四个强命名程序集以及测试程序TestApp4。第四个强命名程序集生成的公钥密码如图11-21所示。注意公钥标记。
图11-21
在TestApp4的项目debug目录中将程序集AssemblySetup.dll文件放入sub子目录中,然后将目录AssemblySetup1.0A的AssemblySetup.dll文件拷贝过来,运行程序。出现异常:
未处理的异常: System.IO.FileLoadException: 未能加载文件或程序集“AssemblySetup,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=c0f2db5b72c553b2”或它的某一个
依赖项。找到的程序集清单定义与程序集引用不匹配。 (异常来自 HRESULT:0x80131040)文件名:“AssemblySetup, Version=1.0.0.0, Culture=neutral, PublicKeyToken=c0f2db5b72c553b2”在 TestApp4.Program.Main(String[] args)
这里的PublicKeyToken=c0f2db5b72c553b2就是图11-21中的公钥标记。而相同的处理,在弱命名程序集中出现的异常是:未处理的异常: System.MissingMethodException: 找不到方法。这个说明了前面介绍的识别强程序集的规则。
现在将两个强命名程序集加入GAC。如图11-22所示。
图11-22
添加到GAC以后,分别将项目TestApp3、TestApp4的debug目录中的程序集AssemblySetup.dll删除掉。分别在debug目录中运行程序,程序运行正常。
下面看一下GAC空间两个强命名程序集的区别。
在目录C:/WINDOWS/assembly/GAC_MSIL中有一个AssemblySetup目录,进入该目录,可以看到图11-23。
图11-23
可以看到两个以1.0.0.0__a65ca2ef1a4b1d5d、1.0.0.0__c0f2db5b72c553b2命名的目录,刚才添加进来的两个强命名程序集就分别位于这两个目录下。