ASP.NET MVC 框架中的 ControllerTypeCache 和 TypeCacheUtil





5.00/5 (2投票s)
本文介绍了 ASP.NET MVC 框架在检索控制器类型以实例化控制器时,如何使用 ControllerTypeCache、TypeCacheUtil 和 TypeCacheSerializer 类来进行缓存。
为了创建控制器的实例,DefaultControllerFactory
类会使用 .NET 反射。它会在不同的程序集中搜索匹配的控制器类型。然后使用此控制器类型来实例化控制器类。您可以在我 之前的 文章中了解更多关于 DefaultControllerFactory
类工作原理的信息。
现在,反射本身是一项非常耗费资源的操作,您可以参考这个 SO 讨论,以大致了解它带来的开销。因此,为了避免这种开销,MVC 框架使用了一种高效的缓存机制。为了避免每次都搜索控制器类型,它会记住每次发现的控制器类型。为了记住发现的控制器,它会在应用程序启动时创建一个 XML 文件,并在后续请求中将每个新发现的控制器类型添加到其中。了解这一点后,您一定想看看这个动态生成的缓存文件。您可以在 C:\Windows\Microsoft.NET\Framework\v4.0.30319\Temporary ASP.NET Files\NAMEOFYOURAPP\xxxxx\xxxxxxxx\UserCache\
文件夹中找到它。您会发现那里有两个 XML 文件,我们感兴趣的是名为 MVC-ControllerTypeCache.xml
的那个。另一个名为 MVC-AreaRegistrationTypeCache.xml
的文件用于缓存区域。MVC-ControllerTypeCache.xml 看起来是这样的:
<?xml version="1.0" encoding="UTF-8"?> <!--This file is automatically generated. Please do not modify the contents of this file.--> <typeCache mvcVersionId="23916767-7e3a-4fab-ad85-0bb94082b2dd" lastModified="3/31/2013 12:03:09 PM"> <assembly name="MyMVCApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"> <module versionId="6c6e3d2b-46c9-4a49-842d-c1cee44511ca"> <type>MyMVCApp.Controllers.HomeController</type> <type>MyMVCApp.Controllers.AccountController</type> </module> </assembly> </typeCache>
缓存文件的创建和维护的整个过程由三个类处理:
- ControllerTypeCache
- TypeCacheUtil
- TypeCacheSerializer
让我们逐一了解这些类,以理解它们的作用。
ControllerTypeCache 和 TypeCacheUtil 类
ControllerTypeCache
类是一个地方,控制器类型通过 TypeCacheUtil
类从缓存中检索,它按名称对控制器类型进行分组,以便从给定的命名空间中检索请求的控制器。
第一次调用 ControllerTypeCache
类是从 DefaultControllerFactory
类的 GetControllerTypeWithinNamespaces()
方法内部进行的。调用会指向 ControllerTypeCache
类的 EnsureInitialized()
方法,并传递 BuildManagerWrapper
类的一个引用。该方法如下所示:
注意:该类的所有代码都可以在 Codeplex 中找到。
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// See License.txt in the project root for license information.
private const string _typeCacheName = "MVC-ControllerTypeCache.xml";
private Dictionary<string, ILookup<string, Type>> _cache;
private object _lockObj = new object();
public void EnsureInitialized(IBuildManager buildManager) {
if (_cache == null) {
lock (_lockObj) {
if (_cache == null) {
List<Type> controllerTypes = TypeCacheUtil.GetFilteredTypesFromAssemblies(_typeCacheName, IsControllerType, buildManager);
var groupedByName = controllerTypes.GroupBy(
t => t.Name.Substring(0, t.Name.Length - "Controller".Length),
StringComparer.OrdinalIgnoreCase);
_cache = groupedByName.ToDictionary(
g => g.Key,
g => g.ToLookup(t => t.Namespace ?? String.Empty, StringComparer.OrdinalIgnoreCase),
StringComparer.OrdinalIgnoreCase);
}
}
}
}
EnsureInitialized()
方法使用双重检查锁定来确保 _cache
字典的初始化只在应用程序的整个生命周期中执行一次,并且在第一个请求命中 IIS 时发生。_cache
字典以控制器名称作为键,存储控制器类型以及它所在的命名空间。为了从 XML 文件中提取控制器类型,会调用 TypeCacheUtil
类的 GetFilteredTypesFromAssemblies()
方法。这是该类的唯一公共方法。该方法如下所示:
注意:该类的所有代码都可以在 Codeplex 中找到。
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // See License.txt in the project root for license information. public static List<Type> GetFilteredTypesFromAssemblies(string cacheName, Predicate<Type> predicate, IBuildManager buildManager) { TypeCacheSerializer serializer = new TypeCacheSerializer(); // first, try reading from the cache on disk List<Type> matchingTypes = ReadTypesFromCache(cacheName, predicate, buildManager, serializer); if (matchingTypes != null) { return matchingTypes; } // if reading from the cache failed, enumerate over every assembly looking for a matching type matchingTypes = FilterTypesInAssemblies(buildManager, predicate).ToList(); // finally, save the cache back to disk SaveTypesToCache(cacheName, matchingTypes, buildManager, serializer); return matchingTypes; }
TypeCacheUtil
类是实际操作 XML 缓存文件的类。该方法做的第一件事是:GetFilteredTypesFromAssemblies()
方法做的第一件事是创建一个 TypeCacheSerializer
类的实例,正如其名称所示,该类用于将发现的控制器类型写入 XML 缓存,并在需要时将其从 XML 文件中读回。稍后我们将更详细地介绍这一点。之后,会尝试通过调用内部方法 ReadTypesFromCache()
来读取缓存文件,并将缓存文件名、BuildManagerWrapper
类的引用、TypeCacheSerializer
引用以及指向 ControllerTypeCache
类的 IsControllerType()
方法的委托传递给它。ReadTypesFromCache()
方法如下所示:
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // See License.txt in the project root for license information. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Cache failures are not fatal, and the code should continue executing normally.")] internal static List<Type> ReadTypesFromCache(string cacheName, Predicate<Type> predicate, IBuildManager buildManager, TypeCacheSerializer serializer) { try { Stream stream = buildManager.ReadCachedFile(cacheName); if (stream != null) { using (StreamReader reader = new StreamReader(stream)) { List<Type> deserializedTypes = serializer.DeserializeTypes(reader); if (deserializedTypes != null && deserializedTypes.All(type => TypeIsPublicClass(type) && predicate(type))) { // If all read types still match the predicate, success! return deserializedTypes; } } } } catch { } return null; }
ReadTypesFromCache()
方法做的第一件事是,使用 BuildManagerWrapper
类的引用,调用 ReadCachedFile()
方法。BuildMangerWrapper
类实现了 IBuildManager
接口。除了该类完成的各种工作之外,它当前的工作是使用 ReadCachedFile()
方法读取缓存文件。它返回一个文件流,然后该文件流使用 TypeCacheSerializer
类进行反序列化。反序列化后,它会检查每个类型是否为公共类型、类类型且不是抽象类型。除了这些检查之外,还会调用由谓词引用的 IsControllerType()
方法。IsControllerType()
方法看起来是这样的:
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // See License.txt in the project root for license information. internal static bool IsControllerType(Type t) { return t != null && t.IsPublic && t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) && !t.IsAbstract && typeof(IController).IsAssignableFrom(t); }
IsControllerType()
方法执行一个关键检查,它确认控制器名称是否以 "Controller"
后缀结尾。因为如果我们使用 DefaultControllerfactory
类,MVC 中的每个控制器都需要以 "Controller"
字符串结尾。如果通过所有检查,则所有控制器类型都将返回给 GetFilteredTypesFromAssemblies()
方法。否则,如果由于某种原因从缓存读取失败,它将开始搜索所有程序集。为此,它会调用 FilterTypesInAssemblies()
方法。
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // See License.txt in the project root for license information. private static IEnumerable<Type> FilterTypesInAssemblies(IBuildManager buildManager, Predicate<Type> predicate) { // Go through all assemblies referenced by the application and search for types matching a predicate IEnumerable<Type> typesSoFar = Type.EmptyTypes; ICollection assemblies = buildManager.GetReferencedAssemblies(); foreach (Assembly assembly in assemblies) { Type[] typesInAsm; try { typesInAsm = assembly.GetTypes(); } catch (ReflectionTypeLoadException ex) { typesInAsm = ex.Types; } typesSoFar = typesSoFar.Concat(typesInAsm); } return typesSoFar.Where(type => TypeIsPublicClass(type) && predicate(type)); }
FilterTypesInAssemblies()
使用 BuildManagerWrapper
类和 GetReferencedAssemblies()
方法来获取所有页面编译所需的已引用程序集。然后,它遍历所有提取的程序集,获取它们的类型,最后根据调用 TypeIsPublicClass()
和 IsControllerType()
方法来过滤类型。
现在我们拥有了程序集中存在的所有匹配类型。为了利用这些找到的类型,框架会将所有这些类型再次保存到 XML 文件中。因此,会调用 SaveTypesToCache()
方法,并将缓存文件名、所有匹配的类型、构建管理器类和序列化器类的引用传递给它。
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // See License.txt in the project root for license information. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Cache failures are not fatal, and the code should continue executing normally.")] internal static void SaveTypesToCache(string cacheName, IList<Type> matchingTypes, IBuildManager buildManager, TypeCacheSerializer serializer) { try { Stream stream = buildManager.CreateCachedFile(cacheName); if (stream != null) { using (StreamWriter writer = new StreamWriter(stream)) { serializer.SerializeTypes(matchingTypes, writer); } } } catch { } }
SaveTypesToCache()
使用 BuilManagerWrapper
的 CreateCachedFile()
方法创建缓存文件。然后使用 TypeCacheSerializer
类的 SerializeTypes()
方法将类型写入 XML 缓存文件。至此,GetFilteredTypesFromAssemblies()
方法和 TypeCacheUtil
类的任务完成,找到的匹配类型将返回给 EnsureInitialized()
方法。EnsureInitialized()
会对类型进行分组并将其保存在 _cache
字典中。
这标志着由 DefaultControllerFactory
类的 GetControllerTypeWithinNamespaces(
调用 )
EnsureInitialized()
方法的执行结束。现在我们有了所有的控制器类型;我们需要找出哪个控制器类型属于哪个命名空间。为了做到这一点,会调用 ControllerTypeCache
类的 GetControllerTypes()
方法,并将控制器名称和命名空间传递给它。
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // See License.txt in the project root for license information. public ICollection<Type> GetControllerTypes(string controllerName, HashSet<string> namespaces) { HashSet<Type> matchingTypes = new HashSet<Type>(); ILookup<string, Type> nsLookup; if (_cache.TryGetValue(controllerName, out nsLookup)) { // this friendly name was located in the cache, now cycle through namespaces if (namespaces != null) { foreach (string requestedNamespace in namespaces) { foreach (var targetNamespaceGrouping in nsLookup) { if (IsNamespaceMatch(requestedNamespace, targetNamespaceGrouping.Key)) { matchingTypes.UnionWith(targetNamespaceGrouping); } } } } else { // if the namespaces parameter is null, search *every* namespace foreach (var nsGroup in nsLookup) { matchingTypes.UnionWith(nsGroup); } } } return matchingTypes; } internal static bool IsNamespaceMatch(string requestedNamespace, string targetNamespace) { // degenerate cases if (requestedNamespace == null) { return false; } else if (requestedNamespace.Length == 0) { return true; } if (!requestedNamespace.EndsWith(".*", StringComparison.OrdinalIgnoreCase)) { // looking for exact namespace match return String.Equals(requestedNamespace, targetNamespace, StringComparison.OrdinalIgnoreCase); } else { // looking for exact or sub-namespace match requestedNamespace = requestedNamespace.Substring(0, requestedNamespace.Length - ".*".Length); if (!targetNamespace.StartsWith(requestedNamespace, StringComparison.OrdinalIgnoreCase)) { return false; } if (requestedNamespace.Length == targetNamespace.Length) { // exact match return true; } else if (targetNamespace[requestedNamespace.Length] == '.') { // good prefix match, e.g. requestedNamespace = "Foo.Bar" and targetNamespace = "Foo.Bar.Baz" return true; } else { // bad prefix match, e.g. requestedNamespace = "Foo.Bar" and targetNamespace = "Foo.Bar2" return false; } } }
GetControllerTypes()
在 _cache
字典中搜索控制器名称。找到匹配的字典项后,它会遍历控制器类型集合,以查找存在于提供的命名空间中的控制器类型。为了进行命名空间匹配,它会将提供的命名空间和控制器类型的命名空间发送到 IsNamespaceMatch()
匹配方法。对所有符合条件的控制器类型会形成一个哈希集,并返回。如果 GetControllerTypes()
方法没有提供任何命名空间,则返回所有找到的控制器类型。
所有匹配的命名空间随后将返回给调用方法,即 DefaultControllerFactory
类的 GetControllerTypeWithinNamespaces()
方法。DefaultControllerFactory
类使用此控制器类型来实例化控制器。至此,我们完成了对 ControllerTypeCache
类的探索。
BuildManagerWrapper 类
BuildManagerWrapper
类实现了 IBuildManager
接口,它包含使用 System.web.compilation
命名空间中的 BuildManager
类来在运行时编译代码文件并返回其类型、获取所有已引用程序集、创建和读取缓存文件以及通过从指定虚拟路径获取对象工厂来检查文件是否存在的方法。该类如下所示:
注意:该类的所有代码都可以在 Codeplex 中找到。
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // See License.txt in the project root for license information. namespace System.Web.Mvc { using System.Collections; using System.IO; using System.Web.Compilation; internal sealed class BuildManagerWrapper : IBuildManager { bool IBuildManager.FileExists(string virtualPath) { return BuildManager.GetObjectFactory(virtualPath, false) != null; } Type IBuildManager.GetCompiledType(string virtualPath) { return BuildManager.GetCompiledType(virtualPath); } ICollection IBuildManager.GetReferencedAssemblies() { return BuildManager.GetReferencedAssemblies(); } Stream IBuildManager.ReadCachedFile(string fileName) { return BuildManager.ReadCachedFile(fileName); } Stream IBuildManager.CreateCachedFile(string fileName) { return BuildManager.CreateCachedFile(fileName); } } }
这是 TypeCacheUtil
类用于执行各种工作的类。它使用 CreateCachedFile()
方法来创建缓存文件,以便缓存数据,我们将缓存文件的内容写入此方法返回的 Stream
对象。ReadCachedFile()
方法用于读取缓存文件的内容,我们通过读取此方法返回的 Stream
对象来实现。GetReferencedAssemblies()
方法用于获取成功编译控制器类型所需的所有已引用程序集。GetCompiledType()
方法编译虚拟路径上的代码文件并返回其编译后的类型。
TypeCacheSerializer 类
为了写入缓存文件的内容,TypeCacheUtil
类使用 TypeCacheSerializer
类的这两个方法:SerializeTypes()
和 DeserializeTypes()
,这也是该类中包含的唯一方法。方法体如下所示:
注意:该类的所有代码都可以在 Codeplex 中找到。
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // See License.txt in the project root for license information. [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This is an instance method for consistency with the SerializeTypes() method.")] public List<Type> DeserializeTypes(TextReader input) { XmlDocument doc = new XmlDocument(); doc.Load(input); XmlElement rootElement = doc.DocumentElement; Guid readMvcVersionId = new Guid(rootElement.Attributes["mvcVersionId"].Value); if (readMvcVersionId != _mvcVersionId) { // The cache is outdated because the cache file was produced by a different version // of MVC. return null; } List<Type> deserializedTypes = new List<Type>(); foreach (XmlNode assemblyNode in rootElement.ChildNodes) { string assemblyName = assemblyNode.Attributes["name"].Value; Assembly assembly = Assembly.Load(assemblyName); foreach (XmlNode moduleNode in assemblyNode.ChildNodes) { Guid moduleVersionId = new Guid(moduleNode.Attributes["versionId"].Value); foreach (XmlNode typeNode in moduleNode.ChildNodes) { string typeName = typeNode.InnerText; Type type = assembly.GetType(typeName); if (type == null || type.Module.ModuleVersionId != moduleVersionId) { // The cache is outdated because we couldn't find a previously recorded // type or the type's containing module was modified. return null; } else { deserializedTypes.Add(type); } } } } return deserializedTypes; } public void SerializeTypes(IEnumerable<Type> types, TextWriter output) { var groupedByAssembly = from type in types group type by type.Module into groupedByModule group groupedByModule by groupedByModule.Key.Assembly; XmlDocument doc = new XmlDocument(); doc.AppendChild(doc.CreateComment(MvcResources.TypeCache_DoNotModify)); XmlElement typeCacheElement = doc.CreateElement("typeCache"); doc.AppendChild(typeCacheElement); typeCacheElement.SetAttribute("lastModified", CurrentDate.ToString()); typeCacheElement.SetAttribute("mvcVersionId", _mvcVersionId.ToString()); foreach (var assemblyGroup in groupedByAssembly) { XmlElement assemblyElement = doc.CreateElement("assembly"); typeCacheElement.AppendChild(assemblyElement); assemblyElement.SetAttribute("name", assemblyGroup.Key.FullName); foreach (var moduleGroup in assemblyGroup) { XmlElement moduleElement = doc.CreateElement("module"); assemblyElement.AppendChild(moduleElement); moduleElement.SetAttribute("versionId", moduleGroup.Key.ModuleVersionId.ToString()); foreach (Type type in moduleGroup) { XmlElement typeElement = doc.CreateElement("type"); moduleElement.AppendChild(typeElement); typeElement.AppendChild(doc.CreateTextNode(type.FullName)); } } } doc.Save(output); }
SerializeTypes()
方法的实现非常简单,它按 Module
和它们所在的 Assembly
对接收到的控制器类型进行分组,并将其写入 XML 文件。
同样,DeserializeTypes()
方法首先检查 mvcVersionId
值,以确保缓存文件是由相同版本的 MVC 创建的,然后它读取 XML 文件的内容,从中创建一个列表并返回。