http://www.microsoft.com/china/MSDN/library/data/xml/Usdnxmlnettrblshtxsd.mspx?mfr=true
XmlSerializer 常見問題疑難解答
發(fā)布日期: 6/30/2004 | 更新日期: 6/30/2004
Christoph Schittko
適用于:
Microsoft ?Visual Studio ?.NET
摘要:Christoph Schittko 討論了各種相關(guān)技巧,以便診斷在使用 .NET 框架中的 XML 序列化技術(shù)將 XML 轉(zhuǎn)換為對象(以及反向轉(zhuǎn)換)時發(fā)生的常見問題。
本頁內(nèi)容
簡介
.NET 框架中的 XmlSerializer 是一種很棒的工具,它將高度結(jié)構(gòu)化的 XML 數(shù)據(jù)映射到 .NET 對象。XmlSerializer 在程序中通過單個 API 調(diào)用來執(zhí)行 XML 文檔和對象之間的轉(zhuǎn)換。轉(zhuǎn)換的映射規(guī)則在 .NET 類中通過元數(shù)據(jù)屬性來表示。這一編程模型帶有自己的錯誤類別,開發(fā)人員需要了解如何診斷這些錯誤。例如,元數(shù)據(jù)屬性必須描述序列化程序可以處理的 XML 格式的所有變體。本文研究了在使用 XmlSerializer 構(gòu)建基于 XML 的解決方案時可能發(fā)生的各種錯誤,并且討論了用來診斷這些錯誤的技巧和工具。
XmlSerializer 的內(nèi)部工作方式
為了有效地解決在 XML 序列化過程中出現(xiàn)的問題,需要了解一下在非常簡單的 XmlSerializer 接口的內(nèi)部發(fā)生了什么事情。與傳統(tǒng)的分析范型相反,.NET 框架中 System.Xml.Serialization 命名空間的 XmlSerializer 將 XML 文檔綁定到 .NET 類的實例。程序員不再需要編寫 DOM 或 SAX 分析代碼,而是通過直接在這些類中附加 .NET 元數(shù)據(jù)屬性來聲明性地設(shè)置綁定規(guī)則。因為所有分析規(guī)則都是通過屬性表示的,所以 XmlSerializer 的接口非常簡單。它主要由兩個方法組成:Serialize() 用于從對象實例生成 XML;Deserialize() 用于將 XML 文檔分析成對象圖。
在使用強類型的、能夠完美地映射到編程對象的結(jié)構(gòu)嚴謹?shù)?XML 格式時,這種方法非常有效。如果格式由 W3C架構(gòu)定義,并且該架構(gòu)由不帶混合型內(nèi)容或且不過度使用通配符(xs:any 和 xs;anyAttribute)的 complexType 組成,則 XML 序列化是處理該數(shù)據(jù)的好方法。
面向消息的應(yīng)用程序就是一個很好的例子,這些應(yīng)用程序之間的交換格式已預先定義。因為許多消息驅(qū)動的企業(yè)應(yīng)用程序都具有非常高的吞吐量要求,所以 Serialize() 和 Deserialize() 方法被設(shè)計為具有很高的執(zhí)行速度。實際上,正是 XmlSerializer 為 System.Messaging 命名空間中的具有高度可伸縮性的庫、ASP.NET Web 服務(wù)和 BizTalk Server 2004 提供了動力。
為獲得 XmlSerializer 的高性能,需要付出雙重代價。首先是與給定 XmlSerializer 可以處理的 XML 格式有關(guān)的靈活性,其次是實例的構(gòu)造需要進行大量的處理。
當您實例化 XmlSerializer 時,必須傳遞您試圖通過該序列化程序?qū)嵗M行序列化和反序列化的對象的類型。序列化程序?qū)z查該類型的所有公共字段和屬性,以了解一個實例在運行時引用哪些類型。接下來,它將為一組類創(chuàng)建 C# 代碼,以便使用 System.CodeDOM 命名空間中的類處理序列化和反序列化。在此過程中,XmlSerializer 將檢查 XML 序列化屬性的反射類型,以便根據(jù) XML 格式定義來自定義所創(chuàng)建的類。這些類隨后被編譯為臨時程序集,并由 Serialize() 和 Deserialize() 方法調(diào)用以執(zhí)行 XML 到對象的轉(zhuǎn)換。
這個設(shè)置 XmlSerializer 的精巧過程和聲明性編程模型導致了三類錯誤,其中一些錯誤可能很難解決:
? |
所生成的序列化類期望被序列化的對象完全符合元數(shù)據(jù)屬性所定義的類型結(jié)構(gòu)。如果 XmlSerializer 遇到未聲明(顯式聲明或者是通過 XML 序列化屬性聲明)的類型,則對象將無法序列化。
|
? |
XML 文檔在以下情況下無法反序列化:該文檔的根元素不能映射對象類型;該文檔的格式不正確,例如包含 XML 規(guī)范中定義為非法的字符;該文檔違反基礎(chǔ)架構(gòu)的限制(在某些情形下)。
|
? |
最后,序列化類的創(chuàng)建及其隨后的編譯可能由于多種不同的原因而失敗。當傳遞給構(gòu)造函數(shù)的類型或者由該類型引用的類型實現(xiàn)了不受支持的接口或者不能滿足 XmlSerializer 施加的限制時,類的創(chuàng)建可能會失敗。
當附加的屬性生成無法編譯的 C# 代碼時,編譯步驟可能會失敗。編譯步驟也可能由于與安全有關(guān)的原因而失敗。
|
下面各個部分將更深入地研究這些情況,并提供有關(guān)如何解決這些問題的指導和建議。
序列化錯誤
我們要研究的第一類錯誤發(fā)生在 Serialize() 方法中。當在運行時傳遞給該方法的對象圖中的類型與在設(shè)計時在類中聲明的類型不匹配時,將發(fā)生此類錯誤。您可以通過字段或?qū)傩缘念愋投x來隱式聲明類型,也可以通過附加序列化屬性來顯式聲明類型。
圖
1.
對象圖中的類型聲明
這里需要指出的是,依靠繼承是不夠的。開發(fā)人員必須通過將 XmlInclude 屬性附加到基類,或者通過將 XmlElement 屬性附加到字段(這些字段可以容納從所聲明的類型派生的類型的對象),來聲明 XmlSerializer 的派生類型。
例如,請看一下以下類層次結(jié)構(gòu):
public class Base
{
public string Field;
}
public class Derived
{
public string AnotherField;
}
public class Container
{
public Base MyField;
}
如果您依賴繼承并且編寫了與下面類似的序列化代碼:
Container obj = new Container();
obj.MyField = new Derived(); // legal assignment in the
//.NET type system
// ...
XmlSerializer serializer = new XmlSerializer( typeof( Container ) );
serializer.Serialize( writer, obj ); // Kaboom!
您將得到發(fā)自 Serialize() 方法的異常,這是因為沒有 XmlSerializer 的顯式類型聲明。
發(fā)自 XmlSerializer 的異常
診斷這些問題的根源在開始時可能比較困難,這是因為來自 XmlSerializer 的異??雌饋聿]有提供有關(guān)其產(chǎn)生原因的大量信息;至少,它們沒有在開發(fā)人員通常會查看的地點提供信息。
在大多數(shù)情況下,當發(fā)生錯誤時,Serialize、Deserialize 甚至 XmlSerializer 構(gòu)造函數(shù)都會引發(fā)一個相當普通的 System.InvalidOperationException。該異常類型可以在 .NET 框架中的許多地方出現(xiàn);它根本不是 XmlSerializer 所特有的。更糟糕的是,該異常的 Message 屬性也僅產(chǎn)生非常普通的信息。在上述示例中,Serialize() 方法會引發(fā)帶有以下消息的異常:
There was an error generating the XML document.
該消息最多也就是令人討厭的,因為當您看到 XmlSerializer 引發(fā)異常時,就已經(jīng)猜到了這一點。現(xiàn)在,您只好無奈地發(fā)現(xiàn)該異常的 Message 無法幫助您解決問題。
奇怪的異常消息和非描述性的異常類型反映了本文前面介紹的 XmlSerializer 內(nèi)部工作方式。Serialize() 方法會捕獲序列化類中引發(fā)的所有異常,將它們包裝到 InvalidOperationException 中,然后將該異常包沿著堆棧向上傳遞。
讀取異常消息
得到“實際”的異常信息的竅門是檢查該異常的 InnerException 屬性。InnerException 引用了從序列化類內(nèi)部引發(fā)的實際異常。它包含有關(guān)該問題及其發(fā)生地點的非常詳細的信息。您在運行上述示例時捕獲的異常將包含帶有以下消息的 InnerException:
The type Derived was not expected. Use the XmlInclude or SoapInclude
attribute to specify types that are not known statically.
您可以通過直接檢查 InnerException 或者通過調(diào)用該異常的 ToString() 方法來得到此消息。下面的代碼片段演示了一個異常處理程序,它寫出了在反序列化對象的過程中發(fā)生的所有異常中的信息:
public void SerializeContainer( XmlWriter writer, Container obj )
{
try
{
// Make sure even the construsctor runs inside a
// try-catch block
XmlSerializer ser = new XmlSerializer( typeof(Container));
ser.Serialize( writer, obj );
}
catch( Exception ex )
{
DumpException( ex );
}
}
public static void DumpException( Exception ex )
{
Console.WriteLine( "--------- Outer Exception Data ---------" );
WriteExceptionInfo( ex );
ex = ex.InnerException;
if( null != ex )
{
Console.WriteLine( "--------- Inner Exception Data ---------" );
WriteExceptionInfo( ex.InnerException );
ex = ex.InnerException;
}
}
public static void WriteExceptionInfo( Exception ex )
{
Console.WriteLine( "Message: {0}", ex.Message );
Console.WriteLine( "Exception Type: {0}", ex.GetType().FullName );
Console.WriteLine( "Source: {0}", ex.Source );
Console.WriteLine( "StrackTrace: {0}", ex.StackTrace );
Console.WriteLine( "TargetSite: {0}", ex.TargetSite );
}
聲明序列化類型
要解決上述示例中的問題,您只需讀取 InnerException 的消息并實現(xiàn)建議的解決方案。傳遞給 Serialize 方法的對象圖中的一個字段引用了一個類型為 Derived 的對象,但并未將該字段聲明為序列化 Derived 類型的對象。盡管該對象圖在 .NET 類型系統(tǒng)中完全合法,但 XmlSerializer 的構(gòu)造函數(shù)在遍歷容器類型的字段時,并不知道為 Derived 類型的對象創(chuàng)建了序列化代碼,這是因為它沒有找到對 Derived 類型的引用。
要向 XmlSerializer 聲明其他字段和屬性類型,您擁有多種選擇。您可以通過 XmlInclude 屬性(由異常消息提示)聲明基類上的派生類型,如下所示:
[System.Xml.Serialization.XmlInclude( typeof( Derived ) )]
public class Base
{
// ...
}
通過附加 XmlInclude 屬性,可以讓 XmlSerializer 在字段或?qū)傩员欢x為 Base 類型時序列化引用 Derived 類型對象的字段。
或者,您還可以僅在單個字段或?qū)傩陨下暶饔行ь愋?,而不是在基類上聲明派生類型。您可以?XmlElement、XmlAttribute 或 XmlArrayItem 屬性附加到字段,并且聲明該字段或?qū)傩钥梢砸玫念愋汀H缓?,XmlSerializer 的構(gòu)造函數(shù)會將序列化和反序列化這些類型所需的代碼添加到序列化類中。
讀取 StackTrace
InnerException 的 Message 屬性并不是唯一包含有價值信息的屬性。StackTrace 屬性傳達了更多有關(guān)錯誤根源的詳細信息。在堆棧跟蹤的最頂端,您可以找到首先引發(fā)異常的方法的名稱。臨時程序集中的方法名稱對于序列化類遵循格式 Write_,對于反序列化類則遵循格式 Read_。在具有上述錯誤命名空間的示例中,您可以看到異常源自名為 Read1_MyClass 的方法。稍后,我將向您說明如何使用 Visual Studio 調(diào)試器設(shè)置斷點并單步執(zhí)行此方法。不過,首先讓我們看一下圍繞反序列化 XML 文檔發(fā)生的常見問題。
反序列化 XML 時發(fā)生的問題
將 XML 文檔反序列化為對象圖不像將對象圖序列化為 XML 那樣容易出錯。當對象不十分匹配類型定義時,XmlSerializer 會非常敏感,但如果反序列化的 XML 文檔不十分匹配對象,則它會非常寬容。對于與反序列化對象中的字段或?qū)傩圆粚?yīng)的 XML 元素,XmlSerializer 不再引發(fā)異常,而只是簡單地引發(fā)事件。如果您需要跟蹤反序列化的 XML 文檔與 XML 格式之間的匹配程度,則可以注冊這些事件的處理程序。然而,您不需要向 XmlSerializer 注冊事件處理程序以正確處理未映射的 XML 節(jié)點。
在反序列化過程中,只有幾種錯誤條件會導致異常。最常見的條件有:
? |
根元素的名稱或其命名空間不匹配期望的名稱。
|
? |
枚舉數(shù)據(jù)類型呈現(xiàn)未定義的值。
|
? |
文檔包含非法 XML。
|
就像序列化的情形一樣,每當發(fā)生問題時,Deserialize() 方法都會引發(fā)帶有以下消息的 InvalidOperation 異常
There is an error in XML document (, ).
該異常通常在 InnerException 屬性中包含真正的異常。InnerException 的類型隨讀取 XML 文檔時發(fā)生的實際錯誤而有所不同。如果序列化程序無法用傳遞給構(gòu)造函數(shù)的類型、通過 XmlInclude 屬性指定的類型或者在傳遞給 XmlSerializer 構(gòu)造函數(shù)的某個更為復雜的重載的 Type[] 中指定的類型來匹配文檔的根元素,則 InnerException 為 InvalidCastException。請記住,XmlSerializer 將查看 Qname(即元素的名稱)和命名空間,以確定要將文檔反序列化為哪個類。它們都必須匹配 .NET 類中的聲明,以便 XmlSerializer 正確標識與文檔的根元素相對應(yīng)的類型。
讓我們看一個示例:
[XmlRoot( Namespace="urn:my-namespace" )]
public class MyClass
{
public string MyField;
}
反序列化以下 XML 文檔將導致異常,因為 MyClass 元素的 XML 命名空間并不像通過 .NET 類上的 XmlRoot 屬性所聲明的那樣是 urn:my-namespace。
<MyClass> <MyField>Hello, World</MyField> </MyClass>
讓我們更進一步地觀察一下該異常。異常 Message 比您從 Serialize() 方法中捕獲的消息更具描述性;至少它引用了文檔中導致 Deserialize() 失敗的位置。盡管如此,當您處理大型 XML 文檔時,查看文檔并確定錯誤可能不會如此簡單。InnerException 又一次提供了更好的信息。這一次,它顯示:
<MyClass xmlns=''> was not expected.
該消息仍然有一些模糊,但它的確向您指明了導致問題的元素。您可以回頭仔細檢查一下 MyClass 類,并將元素名稱和 XML 命名空間與 .NET 類中的 XML 序列化屬性進行比較。
反序列化無效的 XML
另一個經(jīng)常報告的問題是無法反序列化無效的 XML 文檔。XML 規(guī)范禁止在 XML 文檔中使用某些控制字符。然而,有時您仍然會收到包含這些字符的 XML 文檔。正如您猜想的那樣,問題暴露在 InvalidOperationException 中。盡管如此,在這種特殊情況下,InnerException 的類型是 XmlException。InnerException 的消息正中要害:
hexadecimal value <value>, is an invalid character
如果您通過將其 Normalization 屬性設(shè)置為 true 的 XmlTextReader 進行反序列化,則可以避免此問題。遺憾的是,ASP.NET Web 服務(wù)在內(nèi)部使用的 XmlTextReader 將其 Normalization 屬性設(shè)置為 false;也就是說,它將不會反序列化包含這些無效字符的 SOAP 消息。
來自構(gòu)造函數(shù)的異常
本文討論的最后一類問題發(fā)生在 XmlSerializer 的構(gòu)造函數(shù)對傳入的類型進行分析的時候。請記住,構(gòu)造函數(shù)將遞歸檢查類型層次結(jié)構(gòu)中的每個公共字段和屬性,以便創(chuàng)建用來處理序列化和反序列化的類。然后,它將即時編譯這些類,并加載得到的程序集。
在這一復雜的過程中,可能會發(fā)生許多不同的問題:
? |
根元素的聲明類型或者由屬性或字段引用的類型不提供默認的構(gòu)造函數(shù)。
|
? |
層次結(jié)構(gòu)中的某個類型實現(xiàn)了集合接口 Idictionary。
|
? |
執(zhí)行對象圖中某個類型的構(gòu)造函數(shù)或?qū)傩栽L問器時,需要提升安全權(quán)限。
|
? |
生成的序列化類的代碼無法編譯。
|
試圖向 XmlSerializer 構(gòu)造函數(shù)傳遞不可序列化的類型也會導致 InvalidOperationException,但這一次該異常不會包裝其他異常。Message 屬性包含對構(gòu)造函數(shù)拒絕傳入“類型”的原因的充分解釋。試圖序列化未實現(xiàn)不帶參數(shù)的構(gòu)造函數(shù)(默認構(gòu)造函數(shù))的類的實例時,將產(chǎn)生帶有以下 Message 的異常:
Test.NonSerializable cannot be serialized because it does not have a default public constructor.
另一方面,解決編譯錯誤是非常復雜的。這些問題暴露在帶有以下消息的 FileNotFoundException 中:
File or assembly name abcdef.dll, or one of its dependencies, was not found. File name: "abcdef.dll"
at System.Reflection.Assembly.nLoad( ... )
at System.Reflection.Assembly.InternalLoad( ... )
at System.Reflection.Assembly.Load(...)
at System.CodeDom.Compiler.CompilerResults.get_CompiledAssembly()
....
您可能不知道“找不到文件”異常與實例化序列化程序?qū)ο笾g有什么關(guān)系,但請記?。簶?gòu)造函數(shù)寫入 C# 文件并試圖編譯這些文件。該異常的調(diào)用堆棧提供了一些有用的信息,為這種懷疑提供了依據(jù)。當 XmlSerializer 試圖加載由調(diào)用 System.Reflection.Assembly.Load 方法的 CodeDOM 生成的程序集時,發(fā)生了該異常。該異常沒有提供有關(guān) XmlSerializer 根據(jù)推測要創(chuàng)建的程序集不存在的原因的解釋。通常,該程序集不存在的原因是編譯失敗,這是由于序列化屬性生成了 C# 編譯器無法編譯的代碼,但這種情況很少出現(xiàn)。
注 當 XmlSerializer 運行時所屬的帳戶或安全環(huán)境無法訪問 temp 目錄時,也會發(fā)生該錯誤。
XmlSerializer 所引發(fā)的任何異常錯誤消息都不包含實際的編譯錯誤,甚至連 InnerException 也不包含實際的編譯錯誤。這使得解決這些異常變得非常困難,直到 Chris Sells 發(fā)布了他的 XmlSerializerPrecompiler 工具。
XmlSerializerPreCompiler
XmlSerializer PreCompiler 是一個命令行程序,它執(zhí)行與 XmlSerializer 的構(gòu)造函數(shù)相同的步驟。它可分析類型,生成序列化類,并編譯這些類 — 因為它被純粹設(shè)計為故障排除工具,所以它可以安全地向控制臺寫入任何編譯錯誤。
該工具使用起來非常方便。您只需使該工具指向包含導致異常的類型的程序集,并指定要預編譯的類型。讓我們看一個示例。當您將 XmlElement 或 XmlArrayItem 屬性附加到定義為交錯數(shù)組的字段時,會發(fā)生一個經(jīng)常報告的問題,如下面的示例所示:
namespace Test
{
public class StringArray
{
[XmlElement( "arrayElement", typeof( string ) )]
public string [][] strings;
}
}
在為類型 Test.StringArray 實例化 XmlSerializer 對象時,XmlSerializer 構(gòu)造函數(shù)會引發(fā) FileNotFoundException。如果您編譯該類并試圖序列化該類的實例,將得到 FileNotFoundException,但不會得到有關(guān)該問題實質(zhì)的線索。XmlSerializerPreCompiler 可以為您提供缺少的信息。在我的示例中,StringArray 類被編譯為名為 XmlSer.exe 的程序集,并且我必須用下面的命令行運行該工具:
XmlSerializerPreCompiler.exe XmlSer.exe Test.StringArray
第一個命令行參數(shù)指定了程序集,第二個參數(shù)定義了該程序集中要預編譯的類。該工具會向命令窗口寫入大量信息。
圖
2. XmlSerializerPreCompiler
命令窗口輸出
需要查看的重要代碼行是具有編譯錯誤的代碼行以及兩個與以下內(nèi)容類似的代碼行:
XmlSerializer-produced source:
C:\DOCUME~1\\LOCALS~1\Temp\.cs
現(xiàn)在,XmlSerializerPreCompiler 為我們提供了編譯錯誤以及含有無法編譯的代碼的源文件的位置。
調(diào)試序列化代碼
通常情況下,XmlSerializer 會在不再需要序列化類的 C# 源文件時將其刪除。然而,有一個未經(jīng)證實的診斷開關(guān),可用來指示 XmlSerializer 將這些文件保留在硬盤上。您可以在應(yīng)用程序的 .config 文件中設(shè)置此開關(guān):
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.diagnostics> <switches> <add name="XmlSerialization.Compilation" value="4" /> </switches> </system.diagnostics> </configuration>
若此開關(guān)出現(xiàn)在 .config 文件中,C# 源文件將保留在 temp 目錄中。如果您使用的計算機運行 Windows 2000 或更高版本,則 temp 目錄的默認位置是 \Documents and Settings\\LocalSettings\Temp 或 \Temp(對于在 ASP.NET 帳戶下運行的 Web 應(yīng)用程序)。這些 C# 文件很容易丟失,因為它們的文件名看起來非常奇怪并且是隨機生成的,例如:bdz6lq-t.0.cs。XmlSerializerPreCompiler 可設(shè)置該診斷開關(guān),因此您可以在記事本或 Visual Studio 中打開這些文件,以檢查 XmlSerializerPreCompiler 對其報告編譯錯誤的代碼行。
您甚至可以逐句執(zhí)行這些臨時序列化類,因為診斷開關(guān)還可以將含有調(diào)試符號的 .pdb 文件保留在硬盤上。如果您需要在序列化類中設(shè)置斷點,則可以在 Visual Studio 調(diào)試器下運行應(yīng)用程序。一旦您在輸出窗口中看到相關(guān)消息,表明應(yīng)用程序已經(jīng)從 temp 目錄中加載了具有這些奇特名稱的程序集,就可以打開具有相應(yīng)名稱的 C# 文件,然后像在您自己的代碼中一樣設(shè)置斷點。
圖
3.
來自診斷開關(guān)的編譯錯誤輸出
在序列化類中設(shè)置斷點之后,您需要執(zhí)行代碼以調(diào)用 XmlSerializer 對象上的 Serialize() 或 Deserialize() 方法。
注 您只能調(diào)試序列化和反序列化,而不能調(diào)試在構(gòu)造函數(shù)中運行的代碼生成過程。
通過在序列化類中單步執(zhí)行,您能夠查明每個序列化問題。如果您要單步執(zhí)行 SOAP 消息的反序列化,則可以使用上述技巧,這是因為 ASP.NET Web 服務(wù)和 Web 服務(wù)代理是在 XmlSerializer 之上構(gòu)建的。您需要做的只是將診斷開關(guān)添加到您的 config 文件中,然后在反序列化消息的類中設(shè)置斷點。如果 WSDL 在生成代理類時沒有準確地反映消息格式,則我偶爾會使用上述技巧來判斷正確的序列化屬性集。
小結(jié)
這些提示應(yīng)該可以幫助您診斷 XmlSerializer 中的序列化問題。您遇到的大多數(shù)問題都源自 XML 序列化屬性的錯誤組合,或源自與要反序列化的類型不匹配的 XML。序列化屬性控制序列化類的代碼生成,并且可能導致編譯錯誤或運行時異常。通過仔細地檢查由 XmlSerializer 引發(fā)的異常,可幫助您識別運行時異常的根源。如果您需要進一步診斷問題,則可以使用 XmlSerializerPreCompiler 工具來幫助您查找編譯錯誤。如果任一種方法都不能幫助您找到問題的根源,則可以檢查自動創(chuàng)建的序列化類的代碼,并在調(diào)試器中逐句執(zhí)行這些代碼。