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