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