SPSite对象是我们用来获取诸如SPWeb, SPList, SPListItem, SPFile等对象的必经之路. 每一个SPSite对象都维护有一个对叫做SPRequest对象的引用, 这个SPRequest连接着非托管的WSS世界. 这个内部类SPRequest有对一个叫做SP.SPRequest的COM对象的引用, SP.SPRequest的ClassID是BDEADEE2-C265-11D0-BCED-00A0C90AB50F, 它是由OWSSVR.DLL实现并暴露出来的.
SP.SPRequest这个COM对象暴露了近400个基本的operations, 还有基本上你使用Microsoft .NET托管的SharePoint Object Model所能做的一切(从内容数据库中读取数据, 写入数据). 实际上, 这些操作都会通过这个非托管的COM对象来完成. 更进一步的是, OWSSVR.DLL实际上是一个在IIS中注册了的ISAPI extension, 它的方法可以直接通过对/_vti_bin/owssvr.dll的Http访问来调用. 许多Office应用程序(Word, Excel, InfoPath, SharePoint Designer等)就是通过对OWSSRV进行直接的http调用才完成于SharePoint服务器之间的远程整合的. 把OWSSRV.DLL形容为WSS3.0的灵魂和身体一点也不夸张. 它有着久远的历史, 还在SharePoint 1.0版的时候, Microsoft .NET还没有出现, 那时web server都是用诸如ISAPI和DCOM这样的技术实现的. 关于OWSSRV和WSS架构的更多信息, 可以参考MSDN上的文章Overview of the SharePoint Team Services Architecture.
让我们现在来看一看ISPRequest这个公有接口吧, 它是由SP.SPRequest这个COM对象实现的, 对于.NET来说, 通过SPRequest可以访问得到该对象. 下面的列表仅展现由这个COM对象暴露出来的那四百多个中的一小部分, 通过它们你就可以理解这个对象都完成了那些操作. 正如你所看到的, 这些基本上是WSS中的全部:
[ ComImport, Guid("BDEADEBE-C265-11D0-BCED-00A0C90AB50F"), ComConversionLoss, TypeLibType((short)0x100), SuppressUnmanagedCodeSecurity, InterfaceType((short)1) ] public interface ISPRequest { ... void GetFieldsSchemaXml(?); void GetViewsSchemaXml(?); ... string CreateList(?); ... void DeleteList(?); ... void QueryUserInfo(?); ... string AddField(?); void RemoveField(?); void UpdateField(?); ... void GetMetadataForUrl(?); void DeleteView(?); void UpdateView(?); void CreateView(?); ... void AddOrUpdateItem(?); void DeleteItem(?); void GetAttachmentsInfo(?); ... void CrossListQuery(?); void RenderViewAsHtml(?); ... void UpdateUser(?); ... void BackupSite(?); ... void CreateSite(?); void DeleteSite(?); void OpenSite(?); void SetSiteProps(?); void GetAllWebsOfSite(?); ... void UpdateFileOrFolderProperties(?); void PutFile(?); ... void GetFile(?); ... void CheckOutFile(?); void CheckInFile(?); ... void CreateWeb(?); void SaveWebAsTemplate(?); ... void OpenWeb(?); string ApplyTheme(?); ... int AddRoleDef(?); ... void AddWebPart(?); ... void CreateListViewPart(?); ... void GetTimeZoneInfo(?); ... void AddMeeting(?); ... void GetSiteQuota(?); ... void RegisterEventReceiver(?); ... void InvokeTimerJob(?); ... void GetListContentTypes(?); ... void AddWorkflowToListItem(?); ... void SetGhostedFile(?); ... int AddNavigationNode(?); ... }
想要查看由SPRequest暴露出来的方法的完整列表, 你可以在Reflector中打开这个类. 为了这么做, 你首先需要从GAC中拿到Microsoft.SharePoint.Library. 为了拿到一份该文件的拷贝, 你需要Start -> Run -> 键入‘C:\WINDOWS\ASSEMBLY\GAC_MSIL’ -> 按‘Enter’键. 这个小技巧可以打开GAC程序集的物理存储文件夹. 记住你必须使用开始-> 运行才可以进入到这个文件夹. 如果你打开了, 找到Microsoft.SharePoint.Library\12.0.0.0__71e9bce111e9429c 文件夹, 拷贝其中的文件, 之后用reflector打开副本.
ISPRequest实际上是一个COM接口, Microsoft.SharePoint.Library.dll中的公有类型SPRequestInternalClass 是对这个接口的包装. /_vti_bin/owssvr.dll这个ISAPI Extension 可以被用来执行在Windows SharePoint Services URL Protocol中定义的各种操作.
尽管SPRequestInternalClass 是public的, 但实际上按照微软SharePoint专家级技术支持工程师Stefan Gossner的说法, 它是没有文档说明的(可以通过他的blog文章Documented / Undocumented API – Why Should I care?来了解更多信息). Stefan还指出, 通过直接访问undocumentd API而修改过的任何SharePoint 数据库, 微软都不会对其提供支持, 这跟直接使用SQL语句修改SharePoint数据库是一样的. 所以, 要注意!
嗯, 现在你知道了, 当我们调用SharePoint .NET object model的时候, 几乎所有的事情都是通过非托管代码完成的, 下一个问题是这意味着什么? 好吧, 首先这意味着在任何时候都有非托管对象存在着, 而且这些对象不能自动地被.NET垃圾收集器清理. 这也就是为什么我们需要在不再使用SPSite对象的时候需要调用Dispose()或Close()的原因了, 因为我们需要使用这两个方法来释放那些非托管的资源. 如果我们不这么做, 那么这些非托管的资源会引发非托管内存的泄露, 并严重的影响服务器的性能. 我们需要在每一个实现了IDisposable接口的对象上调用Close或Dispose方法, 即SPSite和SPWeb对象.
如果你尝试分配过多的SPRequest对象(创建过多的SPSite对象), 而不释放他们, 你会在ULS log中看到等级为Mdeium的日志记录, 如下:
“Potentially excessive number of SPRequest objects (10) currently unreleased on thread 23. Ensure that this object or its parent (such as an SPWeb or SPSite) is being properly disposed. This object will not be automatically disposed.”
有两个注册表键值可以用来控制这样的与分配和释放SPRequest对象的ULS日志记录. 第一个是HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Shared Tools\Web Server Extensions里的LocalSPRequestWarnCount 键值, 它代表着警告阀值. 你应该尽可能的使用最低数目的SPSite对象, 并且一旦用完立即清理. 第二个是HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Shared Tools\Web Server Extensions\HeapSettings 中的键值SPRequestStackTrace, 它控制着得到非正常析构的SPRequest对象的调用栈, 默认情况下, 这个键值并不是开启的, 你可以设置该值为1来在ULS 日志中得到相应的信息. 关于dispose SharePoint对象的更多信息, 请参考MSDN文章Best Practices: Using Disposable Windows SharePoint Services Objects.
由于创建过多的非托管对象对于服务器应用程序来说有很不好的影响, 微软实现了一些缓存SPRequest对象的方法, 从而在可能的时候对其进行重用. 然而, 这种缓存可能引起额外的问题, 比如说:在你不希望使用缓存版本的时候重用一个缓存版本的SPRequest, 或者是重用在另一个线程中创建出来的SPRequest对象. 在我们深入这些潜在问题的之前, 让我们看看缓存的SPRequest是如何工作的吧.
为了管理SPRequest, SharePoint使用了两个内部的类型- SPRequestManager 和SPRequestContext. 所有在Microsoft .NET托管世界创建出来的SPRequest对象都会通过使用一个SPRequestManager对象进行跟踪并缓存在各自的进程中, 这个SPRequestManager对象是一个Singleton实例, 它被保存在Web Application的HttpContext对象中, 或者, 对于非Web Application(比如说OWS Timer jobs, STSADM命令行, 或者rich client), 它被保存在SPFarm的ThreadContext中. 这也就意味着, 这种缓存是以线程为单位的. 下面的代码展现了SPRequestManager是如何被创建出来并与运行着的线程关联起来的.
private static SPRequestManager Instance { get { SPRequestManager manager = null; if (HttpContext.Current == null) { manager = SPFarm.ThreadContext[typeof(SPRequestManager)] as SPRequestManager; } else { manager = HttpContext.Current.Items[typeof(SPRequestManager)] as SPRequestManager; } if (manager == null) { manager = new SPRequestManager(); if (HttpContext.Current == null) { SPFarm.ThreadContext[typeof(SPRequestManager)] = manager; return manager; } HttpContext.Current.Items[typeof(SPRequestManager)] = manager; } return manager; } }
除了这个SharePoint Object Model之外, SPRequeswt还可以被缓存在打开的SPSite的web context中, 这种缓存是在类型SPRequestContext 的帮助下完成的, 这个SPRequestContext 也是一个singleton实例, 它被存储在每个处理请求的线程的HttpContext对象中.
internal static SPRequestContext GetCurrent(HttpContext context) { if (context != null) { string str = “HttpHandlerSPRequestContext”; SPRequestContextreqContext = (SPRequestContext)context.Items[str]; if(reqContext== null) { reqContext= new SPRequestContext(context); context.Items[str] = reqContext; } returnreqContext; } return null; }
站点实例仅在它不是由farm admin账户创建出来的时候缓存起来(即SPSite.m_bAdministratorOperationMode的值为false). 请注意, 使用SPUtility.RunWithElevatedPrivilegies() 方法的代码并不意味着你的代码会拥有farm administrator权限. 一个用户是否是farm administrator仅由SPFarm.CurrentUserIsAdministrator() 方法判定. 满足下面条件的时候, 一个账户才被认为是farm administrator账户:
When you are using SPUtility.RunWithElevatedPrivilegies() this will simply revert the current user identity to the identity of the current application pool, and if this identity is not a farm administrator then your SPSite _will_ be cached in the SPRequestContext. This is done when the SPSite object is created:
当你使用SPUtility.RunWithElevatedPrivilegies() 的时候, 它只是简单地将当前账号复归为当的web application pool的账号, 如果这个账号不是farm administrator, 那么你的SPSite就会被缓存到SPRequestContext中. 这是在创建SPSite对象创建的时候完成的.
private void SPSiteConstructor( SPFarm farm, Guid applicationId, Guid contentDatabaseId, Guid siteId, SPUrlZone zone, Uri requestUri, string serverRelativeUrl, bool hostHeaderIsSiteName, Uri redirectUri, Pairing pairing, SPUserToken userToken) { //... if (SPSecurity.ImpersonatingSelf && (userToken == null)) { // We will enter here if the site is created inside SPUtility.RunWithElevatedPrivilegies() this.m_bAdministratorOperationMode = SPSecurity.ProcessIdIsGlobalAdmin; } //... if (!this.m_bAdministratorOperationMode) { // The site is cached here and may be reused when activating features SPRequestContext.RegisterSite(this); } //... }
下图展示了与SPRequest协同工作的类型之间的关系:
正如你看到的, 我在上面的图表中添加了SPFeatreManager 类型. 这个类可以从非托管世界调用到, 用来在你生成WSS站点的时候激活feature, 也就是在这里可能会重用到缓存的SPSite对象. 这就是每一次你从基于web的SharePoint应用程序(web part, aspx页面, 或SharePoint本身)中创建一个site或一个web的时候, 如果站点定义中有一些feature需要激活时, 所要调用的:
Now if you are using custom site definitions and depending on how complex are they i.e. how many features do you have and what they are doing, in some cases because the cached SPSite will be reused, you may get some very weird errors particularly when trying to provision two sites at the same time. Some of those errors which I have seen at least a couple of times each include:
———————————————————————————————————————————-
System.InvalidOperationException : You cannot invalidate the SPRequest object while it’s in use.
Source: Microsoft.SharePoint; Help Link:
Stack Trace: at Microsoft.SharePoint.Library.SPRequest.ReleaseResources()
at Microsoft.SharePoint.SPRequestManager.Release(SPRequest request)
at Microsoft.SharePoint.SPWeb.Invalidate()
at Microsoft.SharePoint.SPWeb.Close()
at Microsoft.SharePoint.SPSite.Close()
at Microsoft.SharePoint.SPSite.Dispose()
———————————————————————————————————————————-
System.Runtime.InteropServices.COMException : Attempted to make calls on more than one thread in single threaded mode. (Exception from HRESULT: 0×80010102 (RPC_E_ATTEMPTED_MULTITHREAD))
Stack Trace: at Microsoft.SharePoint.Library.SPRequestInternalClass.GetListsWithCallback(String bstrUrl, Guid foreignWebId, String bstrListInternalName, Int32 dwBaseType, Int32 dwBaseTypeAlt, Int32 dwServerTemplate, UInt32 dwGetListFlags, UInt32 dwListFilterFlags, Boolean bPrefetchMetaData, Boolean bSecurityTrimmed, Boolean bGetSecurityData, ISP2DSafeArrayWriter p2DWriter, Int32& plRecycleBinCount)
at Microsoft.SharePoint.Library.SPRequest.GetListsWithCallback(String bstrUrl, Guid foreignWebId, String bstrListInternalName, Int32 dwBaseType, Int32 dwBaseTypeAlt, Int32 dwServerTemplate, UInt32 dwGetListFlags, UInt32 dwListFilterFlags, Boolean bPrefetchMetaData, Boolean bSecurityTrimmed, Boolean bGetSecurityData, ISP2DSafeArrayWriter p2DWriter, Int32& plRecycleBinCount)
———————————————————————————————————————————-
ExeName: w3wp.exe, Machine: localhost, Severity: Unexpected, Product: ‘Windows SharePoint Services’, Category: ‘General’
ERROR: request not found in the TrackedRequests. We might be creating and closing webs on different threads. ThreadId = 24, Free call stack = at Microsoft.SharePoint.SPRequestManager.Release(SPRequest request)
at Microsoft.SharePoint.SPWeb.Invalidate()
at Microsoft.SharePoint.SPWeb.Close()
at Microsoft.SharePoint.SPContentType.CloseWebAsNecessary()
at Microsoft.SharePoint.SPWeb.Close()
at Microsoft.SharePoint.SPSite.Close()
at Microsoft.SharePoint.SPSite.Dispose()
———————————————————————————————————————————-
All of the 3 errors suggest that a cached SPSite may have been reused from a different thread, when it actually shouldn’t have been. It is very difficult to say why those errors do occur but I believe it has to do with the SPRequestContext caching some SPSites which are being reused when activating features. One of the facts that supports this theory is that you don’t get those errors when you are creating the sites from a non HttpContext application such as a custom STSADM command for example. In that case there is no HttpRequest and no SPRequestContext class i.e. no SPSites are being cached and reused. One of the times when I was getting those errors and was trying to resolve the problem I found a couple of post in Internet from people having the same problem but there were no resolutions. Almost everyone experiencing those errors was saying that they don’t get the errors when their code was called by a console application (custom STSADM command). I wasn’t able to resolve the issue and it left as a known issue in the product. If you have experienced the same problem and have managed to nail it down I would love to hear about your findings/resolution. To mitigate the risk of getting those errors I would suggest that you should:
- Avoid using custom WSS site definitions if they include custom feature receivers that contain complex code
- Consider provisioning your sites from a non HttpContext application whenever possible. OWSTimer jobs count as non HttpContext way of provisioning sites
Also remember that you may be getting those errors for a different reason that is completely unrelated to the SPRequestContext cache. For example if you really are trying to reuse SPWeb or SPSite on a different thread or if you are not disposing properly the SPSite/SPWeb.
And finally lets recap the story about the SPRequest. Almost all read/write content/metadata operations that you do using the .NET SharePoint object model use unmanaged class to do the job. This unmanaged class is called SP.SPRequest and is used from .NET via the SPRequest internal class. The class is implemented and exposed by the OWSSRV.DLL library and has almost 400 methods. Because the SPSite and SPWeb objects have a reference to unmanaged objects you must keep the number of SPSite/SPWeb objects that are active at the same time to a minimum and should dispose them as soon as you have finished using them. You can monitor the creation and disposal of SPRequest objects via the ULS log. In order to minimize the number of SPRequests used in a HttpContext application, SharePoint uses internal cache. It is possible your SPSite objects to be reused when you are creating a site or a web from a web application which site/web is based on a custom WSS site definition that contains custom feature receivers. In rare cases you may be getting errors because of this so if you are using custom WSS site definitions then keep the complexity of your custom feature receivers to a minimum. Consider creating your sites from a non HttpContext application such as an STSADM command, Windows Forms application or a OWSTimer job.
I hope you’ve enjoyed this not very short reading. My next post in the “Understanding SharePoint” series, which will not necessarily be my next post, will be about how asynchronous Event Receivers work in SharePoint and what traps people can get into.
未完, 待续....
参考文章:
Understanding SharePoint: SPRequest
http://hristopavlov.wordpress.com/2009/01/19/understanding-sharepoint-sprequest/