.NET企業(yè)級項目中遇到的國際化問題和解決方法
企業(yè)級的系統(tǒng)和我們平常桌面、手機上運行的軟件有著很重要的區(qū)別,其中比較重要的一點就是環(huán)境(包括第三方的系統(tǒng)的不同接口以及各系統(tǒng)的不同版本、安全性、數據等)比較復雜,所以不論在產品維護還是部署過程中要考慮的因素都有很多。
偶們的系統(tǒng)的最新版本最近在南非上線,遇到了不少問題。昨天晚上本屌和同事解決了一個比較“嚴重”的問題(因為影響到系統(tǒng)的核心功能),發(fā)現(xiàn)是由“國際化”的問題所引起。雖然找到問題的原因并不困難,但是要在客戶的環(huán)境解決還是費了不少精力(因為不能從產品中去修復這個問題,即使給一個補丁按照公司的流程至少也需要半個月)。之前我也幫運營團隊解決過一部分這類問題,在工作中也看到過不少,那么這里一并記錄一下吧。希望大家在以后的開發(fā)過程中多加注意。
一、數據庫的日期時間格式問題
這應該是幾乎所有的系統(tǒng)在部署到不同國家的客戶環(huán)境中都會遇到的問題,卡死不少程序,耗費無數人力。因此這是在數據存儲時一定要格外注意的地方。這里以SQL Server為例。
(1)數據表中的時間格式
對于日期和時間,SQL Server中提供了datetime、datetime2、datetimeoffset、date、time這幾種類型。我個人推薦只使用datetime和datetime2,因為這兩個類型與.NET的程序兼容性最好(都可以直接對應成DateTime)。在.NET中也提供了DateTimeOffset類型,但是在一些比較老的語言中并不支持,為了兼容使用datetime就足夠了。使用其他類型會牽扯到轉型的問題,應盡量避免以免產生額外的問題。
需要注意的是,datetime支持的最小時間是1753年,因此要避免在程序中直接使用DateTime.MinValue傳到數據庫中,否則會產生out-of-range的錯誤。
在一些古老的系統(tǒng)中,會使用int類型來存儲日期,因為int會占4個字節(jié),所以兩者互相轉型是支持的,使用convert進行轉型即可,一般不會有什么問題。
(2)時區(qū)的問題
建議在數據庫中所有數據都以UTC時間保存,在顯示的時候客戶端根據本地的時區(qū)進行處理,因此在存儲過程中一定要注意把時間轉換成UTC時間。如果不保存UTC時間,以后在顯示和遷移的過程中難免會發(fā)生混亂,所以盡量不要使用當地的時間來保存,即使系統(tǒng)只在一個地區(qū)使用。
在設計SSRS報表時,也要注意時區(qū)的問題。由于數據庫中存儲的是UTC時間,所以在調用Reporting Service服務的時候應把用戶輸入的時間轉換成UTC時間后傳給報表服務。在顯示的使用,使用如下代碼格式化單元格:
不要在報表的存儲過程中對時區(qū)進行處理。雖然很方便且直觀,但是當你的系統(tǒng)有幾十張報表并且由不同的人開發(fā)的時候就會發(fā)現(xiàn)混亂的局面。
(3)報表的時間格式
這是微軟考慮不周導致的問題。由于報表服務是一個Web Service,所以在實際的運行環(huán)境中無論是日期控件還是報表中輸出日期的單元格都是使用IE的語言設置,而非Windows的地區(qū)。但是在設計報表的時候它又是使用的Windows的日期格式。如果客戶有這類需求,那么我們至少修改IE的設置。
但是最坑人的地方是要修改的是數據庫所在的服務器的IE設置,這樣做會影響到所有的客戶端,那么這也沒辦法。有時候這樣做了也不見得好使,似乎又與數據庫的連接用戶的默認語言有關,所以這類問題我們一般不會花太多時間去修正。
好在這只是一個顯示的問題,并不影響系統(tǒng)的正常功能,也不會出錯,所以很少會聽到客戶抱怨這類問題。
(4)應用程序與數據庫之間的日期時間傳遞
我們首先來看下面一段代碼:
string sql = string.Format("SELECT * FROM [User] WHERE [CreationTime] < '{0}'", creation_time);
在數據庫中CreationTime是一個datetime類型,而我們單引號之間的內容是一個字符串,此時SQL Server會自動對字符串進行轉型,因此相當于隱式實現(xiàn)了一個convert語句,把{0}處的內容轉型成datetime類型。{0}處實際上是一個字符串,在string.Format方法里會隱式調用creation_time的ToString方法,這個方法所返回的日期時間的格式會依據當前線程所處的語言環(huán)境來確定。
在調試的過程中似乎不會發(fā)現(xiàn)問題,但是當產品給一個國外的客戶時,就很有可能發(fā)現(xiàn)SQL Server拋出out-of-range的錯誤,大意是將varchar轉型成datetime類型時越界。導致這個問題的原因簡單來說就是因為客戶端的日期格式與數據庫的設置不一致(更詳細的下面會說明),此時系統(tǒng)理解的年、月、日這幾個字段所處的位置不在正確的地方上,因此有時會越出月或者日的邊界。
避免這個問題的根本解決辦法是在訪問數據庫的時候只使用參數形式,根本不應該出現(xiàn)非參數的拼接形式(同時可以避免SQL注入以及其他bug),如果你在做代碼審查,看到上面這種代碼應該果斷reject回去。使用參數形式的代碼大概是這樣:
SqlCommand command = new SqlCommand("SELECT * FROM [User] WHERE [CreationTime] < @creationTime", connection);
command.Parameters.AddWithValue("creationTime", creation_time);
這樣,就會避免將DateTime類型轉型成字符串,直接使用SQL Server兼容的類型訪問,就避免了格式不一致導致的轉型失?。ɑ蛘咿D換成錯誤的結果)。
我們在系統(tǒng)日志中發(fā)現(xiàn)某個模塊頻繁拋出越界的錯誤,就是因為沒有使用參數的形式進行訪問。與此對應的系統(tǒng)其他模塊都沒有這個類型的錯誤。
(5)SQL語句中的日期格式轉型問題
這就是昨天做支持過程中遇到的一個非常詭異的問題。在我們的產品的SQL語句中有一段類似下面這樣的代碼:
DECLARE @startTime VARCHAR(100)
SELECT @startTime = CONVERT(VARCHAR, timeIn, 23), @endTime = CONVERT(VARCHAR, timeIn + 1, 23) FROM...
我們不談論這段代碼寫得怎樣,我們只關心為什么會出錯。在客戶現(xiàn)場中,客戶的描述是:“在前一天系統(tǒng)還是好的,但是在換班后(我們的系統(tǒng)執(zhí)行換班操作后日期會往后加一天),系統(tǒng)就無法操作。”
分析日志發(fā)現(xiàn)拋出的錯是日期轉型越界,出錯的代碼片段應該就是在這個地方,那么為什么會出錯呢?
經過一番折騰,發(fā)現(xiàn)了SQL Server的一個隱藏炸彈(還好有測試的同事在其他系統(tǒng)見過這個問題,否則也不會往這個方向去想):對于每一個有權限訪問數據庫的用戶(包括Windows登錄方式和SQL Server登錄方式),在用戶的屬性中都有一個稱之為“默認語言”的屬性。在我們公司的產品中,基本只會在British English和English這兩者之間進行選擇。
這個屬性會影響到SQL語句中牽涉到日期與字符串轉型時的處理。因此,在SQL語句中轉型是與這個“默認語言”有關系的。同時,還會與客戶端(訪問數據庫的服務所在的機器)的本地時間格式有關。
客戶的數據庫登錄用戶的默認語言選擇是British English,而數據庫服務器和客戶端的時間格式配置都是南非的時間格式。這種不一致可能會導致一些不可預知的問題。我們要用戶把默認語言給成English(注意:1. 改了之后要重啟SQL Server的服務使之生效; 2. 特別注意修改的是訪問數據庫的用戶,每個用戶都有這個屬性),然后問題莫名其妙地就解決了。
但是后來我們在實驗環(huán)境中按照這種配置都無法重現(xiàn)這個問題,我覺得有可能是客戶修改了一些日期格式的原因所導致。
今天早上我做了一個實驗,把默認語言設置成British English,而客戶端的時間格式是中國的格式,此時上面的timeIn + 1得到的結果竟然是月份加1!這樣就使我們恍然大悟:難怪客戶在前一天沒有問題,而過了一天問題才出現(xiàn)。因為出現(xiàn)問題的時間是6月12日,由于某種特殊的配置(我們至今還是沒嘗試出是何種格式),系統(tǒng)把12當成是月份+1,得到13就越界了,而使用6月11日就不會出越界的問題,但結果是錯誤的。
這顯而易見是微軟的一個bug,無論何種配置,都不應該轉型出錯,即使是月份+1,應該會進到年的字段去。
上面這段SQL語句的正確寫法應該是使用DATEDIFF進行日期運算,而且更不應該出現(xiàn)第三個參數“23”這種直接指定格式的輸出。
(6)結論
通過上面的分析,有一點可以肯定的是:在編碼過程中,除非僅用于顯示,否則必須要避免日期和字符串的相互轉換!
日期轉型成字符串應該只在客戶端顯示的最后一步進行轉型,而字符串轉換成時間應該只在應用程序中使用DateTime提供的與時間格式相關的方法進行完成。一定要避免使用字符串進行日期和時間的運算,即使是在SQL語句中也是如此。
這樣應該就能避免由時間格式所導致的問題,而且也不會碰到(5)里面那種詭異的問題:即使知道問題出在哪,也不知道怎么樣設置才不會出這種問題。
上面是著重闡述的與數據庫相關的日期格式問題,下面再提兩種我遇到的小問題。
二、Web Server的日期格式問題
微軟的.NET與JSON格式兼容得不是很好,我碰到了兩個問題。
(1)JSON解析DateTime格式
JSON的時間格式和.NET完全不一樣。DateTime說白了就是一個8字節(jié)的二進制位,而JSON是以字符串來表示日期的。JSON的日期類似于“/Date(1242357713797+0800)/”。
一種比較懶的處理方式是直接使用eval語句讓系統(tǒng)自動解析,這種方法的效果比較好(性能就不知道了),轉換后可以按照當地的日期格式進行輸出,比較方便。
(2).NET解析JSON的日期格式
從JSON的字符串進行解析時,除了把中間的數字進行運算外(1970年1月1日+數字*10000作為DateTime類型的Ticks),還要考慮后面的時區(qū)。處理比較麻煩。
當然可以想一個偷懶的辦法,那就是直接傳字符串而不通過JSON的日期格式:既然客戶端顯示的是本地時間格式,那么如果服務器的時間格式也是一樣的就可以直接用DateTime的Parse就完事了。這種方式適合偷懶人士(比如我),暫時也沒有發(fā)現(xiàn)什么問題,除了接口類型不太好看以外。
(3)微軟的bug
又是時間格式引發(fā)的bug!將Web Server的格式設置為南非的以后,發(fā)現(xiàn)所有將DateTime序列化成JSON格式的代碼全都報錯:日期時間轉換越界。在調用堆棧中全部都是系統(tǒng)的代碼,將時間格式設置成美國后問題消失,因此可以肯定這是微軟的一個bug(在4.0版本的.NET Framework中,不知道是否修正了)。
那么只能讓客戶使用美國的時間格式湊合著用了,我們也只能在下一個版本解決這個問題。方案很簡單,直接轉換成字符串傳給客戶端就行了,這樣客戶端也不必寫代碼去解析DateTime格式。
由于.NET本身就和JSON的兼容有缺陷,所以使用上述辦法也并無不妥。假如客戶有國際化的需求,其實可以定義一種中間格式(比如傳輸的過程中都以中國的格式為準),客戶端和服務器按照這個中間格式進行解析,就能避免由格式不配套產生的轉型錯誤。
三、文本的國際化處理
這個說起來很簡單,只要改變編碼習慣就可以了。在一個資源文件(比如XML格式)中首先以默認語言(比如中文)存儲所有需要顯示出來的字符串以及對應的名稱(名稱最好按功能+內容來命名,不要用數字編號,這樣可以避免在有10000個記錄的資源文件中不確定是否有某個需要的字符串的尷尬),然后依葫蘆畫瓢制作其他的語言包。
在應用程序中,推薦使用MVVM或者MVP模式進行數據綁定,令其作為靜態(tài)資源定位到一個中間的靜態(tài)類,由這個類決定使用哪種語言包。這樣做的好處是我們不需要寫代碼去初始化界面上各種文本標簽(試想我們在窗體加載的使用寫一大堆語句去設置Label或者Menu的Text?)。使用數據綁定,系統(tǒng)會在需要顯示這個資源的時候訪問靜態(tài)類去取文本內容。
微軟已經考慮到這個問題并把它做得很完善(除了一個小bug外我們暫時沒發(fā)現(xiàn)其他的bug,被上面的兩個微軟的bug搞怕了吧?這個小bug影響不大在此不做說明),你可以參考這里:http://msdn.microsoft.com/en-us/kb/ms745650(v=vs.80)
這樣做的好處是:在程序編譯后會生成幾個文件夾,這些文件夾按語言的名字來命名(比如zh-cn表示簡體中文),文件夾中有資源文件的dll。在系統(tǒng)啟動的時候可以讓用戶選一種語言,然后設置CultureInfo,這樣系統(tǒng)就會自動去相應的文件夾取這個語言包里的字符串。還有一個很人性化的功能,就是如果在對應語言的資源文件中沒有找到這個字符串,就會從默認的語言包中取出并顯示。(比如我們產品有繁體中文的語言包,如果有些地方沒有進行翻譯,在繁體中文的語言包中就不會有這個字符串,此時系統(tǒng)會顯示出英文語言包中的字符串。)
以上是我遇到的處理國際化問題的一些典型例子,如果你遇到了一些這樣的問題,歡迎指出并共同探討。
相關文章
C# Winform實現(xiàn)導出DataGridView當前頁以及全部數據
基本上,所有的業(yè)務系統(tǒng)都會要求有導出的功能,所以這篇文章主要為大家介紹了如何利用Winform實現(xiàn)原生DataGridView的導出功能,需要的可以參考一下2023-07-07
在C#使用字典存儲事件示例及實現(xiàn)自定義事件訪問器
這篇文章主要介紹了在C#使用字典存儲事件示例及實現(xiàn)自定義事件訪問器的方法,是C#事件編程中的基礎知識,需要的朋友可以參考下2016-02-02
在C#中創(chuàng)建和讀取XML文件的實現(xiàn)方法
項目中需要將前臺頁面中的信息保存下來并存儲為xml文件格式到數據庫中去。因此我先在這里通過一個小實例來學習xml的創(chuàng)建與讀取2013-09-09
C#實現(xiàn)將HTML網頁或HTML字符串轉換為PDF
將HTML轉換為PDF可實現(xiàn)格式保留、可靠打印、文檔歸檔等多種用途,滿足不同領域和情境下的需求,所以本文就來介紹一下如何使用C#實現(xiàn)將HTML網頁或HTML字符串轉換為PDF,有需要的可以參考下2024-01-01

