jhengfei
          愛JAVA,愛生活

          2006年6月23日

          北北 發表于 2006-8-21 20:46:15

          前言:

          半年前我對正則表達式產生了興趣,在網上查找過不少資料,看過不少的教程,最后在使用一個正則表達式工具RegexBuddy時發現他的教程寫的非常好,可以說是我目前見過最好的正則表達式教程。于是一直想把他翻譯過來。這個愿望直到這個五一長假才得以實現,結果就有了這篇文章。關于本文的名字,使用“深入淺出”似乎已經太俗。但是通讀原文以后,覺得只有用“深入淺出”才能準確的表達出該教程給我的感受,所以也就不能免俗了。

          本文是Jan Goyvaerts為RegexBuddy寫的教程的譯文,版權歸原作者所有,歡迎轉載。但是為了尊重原作者和譯者的勞動,請注明出處!謝謝!

          ?1.什么是正則表達式

          基本說來,正則表達式是一種用來描述一定數量文本的模式。Regex代表Regular Express。本文將用<<regex>>來表示一段具體的正則表達式。

          一段文本就是最基本的模式,簡單的匹配相同的文本。

          ?2.不同的正則表達式引擎

          正則表達式引擎是一種可以處理正則表達式的軟件。通常,引擎是更大的應用程序的一部分。在軟件世界,不同的正則表達式并不互相兼容。本教程會集中討論Perl 5 類型的引擎,因為這種引擎是應用最廣泛的引擎。同時我們也會提到一些和其他引擎的區別。許多近代的引擎都很類似,但不完全一樣。例如.NET正則庫,JDK正則包。

          ?3.文字符號

          最基本的正則表達式由單個文字符號組成。如<<a>>,它將匹配字符串中第一次出現的字符“a”。如對字符串“Jack is a boy”。“J”后的“a”將被匹配。而第二個“a”將不會被匹配。

          正則表達式也可以匹配第二個“a”,這必須是你告訴正則表達式引擎從第一次匹配的地方開始搜索。在文本編輯器中,你可以使用“查找下一個”。在編程語言中,會有一個函數可以使你從前一次匹配的位置開始繼續向后搜索。

          類似的,<<cat>>會匹配“About cats and dogs”中的“cat”。這等于是告訴正則表達式引擎,找到一個<<c>>,緊跟一個<<a>>,再跟一個<<t>>。

          要注意,正則表達式引擎缺省是大小寫敏感的。除非你告訴引擎忽略大小寫,否則<<cat>>不會匹配“Cat”。

          ?· 特殊字符

          對于文字字符,有11個字符被保留作特殊用途。他們是:

          [ ] \ ^ $ . | ? * + ( )

          這些特殊字符也被稱作元字符。

          如果你想在正則表達式中將這些字符用作文本字符,你需要用反斜杠“\”對其進行換碼 (escape)。例如你想匹配“1+1=2”,正確的表達式為<<1\+1=2>>.

          需要注意的是,<<1+1=2>>也是有效的正則表達式。但它不會匹配“1+1=2”,而會匹配“123+111=234”中的“111=2”。因為“+”在這里表示特殊含義(重復1次到多次)。

          在編程語言中,要注意,一些特殊的字符會先被編譯器處理,然后再傳遞給正則引擎。因此正則表達式<<1\+2=2>>在C++中要寫成“1\\+1=2”。為了匹配“C:\temp”,你要用正則表達式<<C:\\temp>>。而在C++中,正則表達式則變成了“C:\\\\temp”。

          ?·不可顯示字符

          可以使用特殊字符序列來代表某些不可顯示字符:

          <<\t>>代表Tab(0x09)

          <<\r>>代表回車符(0x0D)

          <<\n>>代表換行符(0x0A)

          要注意的是Windows中文本文件使用“\r\n”來結束一行而Unix使用“\n”。

          ?4.正則表達式引擎的內部工作機制

          知道正則表達式引擎是如何工作的有助于你很快理解為何某個正則表達式不像你期望的那樣工作。

          有兩種類型的引擎:文本導向(text-directed)的引擎和正則導向(regex-directed)的引擎。Jeffrey Friedl把他們稱作DFA和NFA引擎。本文談到的是正則導向的引擎。這是因為一些非常有用的特性,如“惰性”量詞(lazy quantifiers)和反向引用(backreferences),只能在正則導向的引擎中實現。所以毫不意外這種引擎是目前最流行的引擎。

          你可以輕易分辨出所使用的引擎是文本導向還是正則導向。如果反向引用或“惰性”量詞被實現,則可以肯定你使用的引擎是正則導向的。你可以作如下測試:將正則表達式<<regex|regex not>>應用到字符串“regex not”。如果匹配的結果是regex,則引擎是正則導向的。如果結果是regex not,則是文本導向的。因為正則導向的引擎是“猴急”的,它會很急切的進行表功,報告它找到的第一個匹配 。

          ?·正則導向的引擎總是返回最左邊的匹配

          這是需要你理解的很重要的一點:即使以后有可能發現一個“更好”的匹配,正則導向的引擎也總是返回最左邊的匹配。

          當把<<cat>>應用到“He captured a catfish for his cat”,引擎先比較<<c>>和“H”,結果失敗了。于是引擎再比較<<c>>和“e”,也失敗了。直到第四個字符,<<c>>匹配了“c”。<<a>>匹配了第五個字符。到第六個字符<<t>>沒能匹配“p”,也失敗了。引擎再繼續從第五個字符重新檢查匹配性。直到第十五個字符開始,<<cat>>匹配上了“catfish”中的“cat”,正則表達式引擎急切的返回第一個匹配的結果,而不會再繼續查找是否有其他更好的匹配。

          ?5.字符集

          字符集是由一對方括號“[]”括起來的字符集合。使用字符集,你可以告訴正則表達式引擎僅僅匹配多個字符中的一個。如果你想匹配一個“a”或一個“e”,使用<<[ae]>>。你可以使用<<gr[ae]y>>匹配gray或grey。這在你不確定你要搜索的字符是采用美國英語還是英國英語時特別有用。相反,<<gr[ae]y>>將不會匹配graay或graey。字符集中的字符順序并沒有什么關系,結果都是相同的。

          你可以使用連字符“-”定義一個字符范圍作為字符集。<<[0-9]>>匹配0到9之間的單個數字。你可以使用不止一個范圍。<<[0-9a-fA-F] >>匹配單個的十六進制數字,并且大小寫不敏感。你也可以結合范圍定義與單個字符定義。<<[0-9a-fxA-FX]>>匹配一個十六進制數字或字母X。再次強調一下,字符和范圍定義的先后順序對結果沒有影響。

          ?·字符集的一些應用

          查找一個可能有拼寫錯誤的單詞,比如<<sep[ae]r[ae]te>> 或 <<li[cs]en[cs]e>>。

          查找程序語言的標識符,<<A-Za-z_][A-Za-z_0-9]*>>。(*表示重復0或多次)

          查找C風格的十六進制數<<0[xX][A-Fa-f0-9]+>>。(+表示重復一次或多次)

          ?·取反字符集

          在左方括號“[”后面緊跟一個尖括號“^”,將會對字符集取反。結果是字符集將匹配任何不在方括號中的字符。不像“.”,取反字符集是可以匹配回車換行符的。

          需要記住的很重要的一點是,取反字符集必須要匹配一個字符。<<q[^u]>>并不意味著:匹配一個q,后面沒有u跟著。它意味著:匹配一個q,后面跟著一個不是u的字符。所以它不會匹配“Iraq”中的q,而會匹配“Iraq is a country”中的q和一個空格符。事實上,空格符是匹配中的一部分,因為它是一個“不是u的字符”。

          如果你只想匹配一個q,條件是q后面有一個不是u的字符,我們可以用后面將講到的向前查看來解決。

          ?·字符集中的元字符

          需要注意的是,在字符集中只有4個 字符具有特殊含義。它們是:“] \ ^ -”。“]”代表字符集定義的結束;“\”代表轉義;“^”代表取反;“-”代表范圍定義。其他常見的元字符在字符集定義內部都是正常字符,不需要轉義。例如,要搜索星號*或加號+,你可以用<<[+*]>>。當然,如果你對那些通常的元字符進行轉義,你的正則表達式一樣會工作得很好,但是這會降低可讀性。

          在字符集定義中為了將反斜杠“\”作為一個文字字符而非特殊含義的字符,你需要用另一個反斜杠對它進行轉義。<<[\\x]>>將會匹配一個反斜杠和一個X。“]^-”都可以用反斜杠進行轉義,或者將他們放在一個不可能使用到他們特殊含義的位置。我們推薦后者,因為這樣可以增加可讀性。比如對于字符“^”,將它放在除了左括號“[”后面的位置,使用的都是文字字符含義而非取反含義。如<<[x^]>>會匹配一個x或^。<<[]x]>>會匹配一個“]”或“x”。<<[-x]>>或<<[x-]>>都會匹配一個“-”或“x”。

          ?·字符集的簡寫

          因為一些字符集非常常用,所以有一些簡寫方式。

          <<\d>>代表<<[0-9]>>;

          <<\w>>代表單詞字符。這個是隨正則表達式實現的不同而有些差異。絕大多數的正則表達式實現的單詞字符集都包含了<<A-Za-z0-9_]>>。

          <<\s>>代表“白字符”。這個也是和不同的實現有關的。在絕大多數的實現中,都包含了空格符和Tab符,以及回車換行符<<\r\n>>。

          字符集的縮寫形式可以用在方括號之內或之外。<<\s\d>>匹配一個白字符后面緊跟一個數字。<<[\s\d]>>匹配單個白字符或數字。<<[\da-fA-F]>>將匹配一個十六進制數字。

          取反字符集的簡寫

          <<[\S]>> = <<[^\s]>>

          <<[\W]>> = <<[^\w]>>

          <<[\D]>> = <<[^\d]>>

          ·字符集的重復

          如果你用“?*+”操作符來重復一個字符集,你將會重復整個字符集。而不僅是它匹配的那個字符。正則表達式<<[0-9]+>>會匹配837以及222。

          如果你僅僅想重復被匹配的那個字符,可以用向后引用達到目的。我們以后將講到向后引用。

          ?6.使用?*或+ 進行重復

          ?:告訴引擎匹配前導字符0次或一次。事實上是表示前導字符是可選的。

          +:告訴引擎匹配前導字符1次或多次

          *:告訴引擎匹配前導字符0次或多次

          <[A-Za-z][A-Za-z0-9]*>匹配沒有屬性的HTML標簽,“<”以及“>”是文字符號。第一個字符集匹配一個字母,第二個字符集匹配一個字母或數字。

          我們似乎也可以用<[A-Za-z0-9]+>。但是它會匹配<1>。但是這個正則表達式在你知道你要搜索的字符串不包含類似的無效標簽時還是足夠有效的。

          ?·限制性重復

          許多現代的正則表達式實現,都允許你定義對一個字符重復多少次。詞法是:{min,max}。min和max都是非負整數。如果逗號有而max被忽略了,則max沒有限制。如果逗號和max都被忽略了,則重復min次。

          因此{0,}和*一樣,{1,}和+ 的作用一樣。

          你可以用<<\b[1-9][0-9]{3}\b>>匹配1000~9999之間的數字(“\b”表示單詞邊界)。<<\b[1-9][0-9]{2,4}\b>>匹配一個在100~99999之間的數字。

          ?·注意貪婪性

          假設你想用一個正則表達式匹配一個HTML標簽。你知道輸入將會是一個有效的HTML文件,因此正則表達式不需要排除那些無效的標簽。所以如果是在兩個尖括號之間的內容,就應該是一個HTML標簽。

          許多正則表達式的新手會首先想到用正則表達式<< <.+> >>,他們會很驚訝的發現,對于測試字符串,“This is a <EM>first</EM> test”,你可能期望會返回<EM>,然后繼續進行匹配的時候,返回</EM>。

          但事實是不會。正則表達式將會匹配“<EM>first</EM>”。很顯然這不是我們想要的結果。原因在于“+”是貪婪的。也就是說,“+”會導致正則表達式引擎試圖盡可能的重復前導字符。只有當這種重復會引起整個正則表達式匹配失敗的情況下,引擎會進行回溯。也就是說,它會放棄最后一次的“重復”,然后處理正則表達式余下的部分。

          和“+”類似,“?*”的重復也是貪婪的。

          ?·深入正則表達式引擎內部

          讓我們來看看正則引擎如何匹配前面的例子。第一個記號是“<”,這是一個文字符號。第二個符號是“.”,匹配了字符“E”,然后“+”一直可以匹配其余的字符,直到一行的結束。然后到了換行符,匹配失敗(“.”不匹配換行符)。于是引擎開始對下一個正則表達式符號進行匹配。也即試圖匹配“>”。到目前為止,“<.+”已經匹配了“<EM>first</EM> test”。引擎會試圖將“>”與換行符進行匹配,結果失敗了。于是引擎進行回溯。結果是現在“<.+”匹配“<EM>first</EM> tes”。于是引擎將“>”與“t”進行匹配。顯然還是會失敗。這個過程繼續,直到“<.+”匹配“<EM>first</EM”,“>”與“>”匹配。于是引擎找到了一個匹配“<EM>first</EM>”。記住,正則導向的引擎是“急切的”,所以它會急著報告它找到的第一個匹配。而不是繼續回溯,即使可能會有更好的匹配,例如“<EM>”。所以我們可以看到,由于“+”的貪婪性,使得正則表達式引擎返回了一個最左邊的最長的匹配。

          ?·用懶惰性取代貪婪性

          一個用于修正以上問題的可能方案是用“+”的惰性代替貪婪性。你可以在“+”后面緊跟一個問號“?”來達到這一點。“*”,“{}”和“?”表示的重復也可以用這個方案。因此在上面的例子中我們可以使用“<.+?>”。讓我們再來看看正則表達式引擎的處理過程。

          再一次,正則表達式記號“<”會匹配字符串的第一個“<”。下一個正則記號是“.”。這次是一個懶惰的“+”來重復上一個字符。這告訴正則引擎,盡可能少的重復上一個字符。因此引擎匹配“.”和字符“E”,然后用“>”匹配“M”,結果失敗了。引擎會進行回溯,和上一個例子不同,因為是惰性重復,所以引擎是擴展惰性重復而不是減少,于是“<.+”現在被擴展為“<EM”。引擎繼續匹配下一個記號“>”。這次得到了一個成功匹配。引擎于是報告“<EM>”是一個成功的匹配。整個過程大致如此。

          ?·惰性擴展的一個替代方案

          我們還有一個更好的替代方案。可以用一個貪婪重復與一個取反字符集:“<[^>]+>”。之所以說這是一個更好的方案在于使用惰性重復時,引擎會在找到一個成功匹配前對每一個字符進行回溯。而使用取反字符集則不需要進行回溯。

          最后要記住的是,本教程僅僅談到的是正則導向的引擎。文本導向的引擎是不回溯的。但是同時他們也不支持惰性重復操作。

          ?7.使用“.”匹配幾乎任意字符

          在正則表達式中,“.”是最常用的符號之一。不幸的是,它也是最容易被誤用的符號之一。

          “.”匹配一個單個的字符而不用關心被匹配的字符是什么。唯一的例外是新行符。在本教程中談到的引擎,缺省情況下都是不匹配新行符的。因此在缺省情況下,“.”等于是字符集[^\n\r](Window)或[^\n]( Unix)的簡寫。

          這個例外是因為歷史的原因。因為早期使用正則表達式的工具是基于行的。它們都是一行一行的讀入一個文件,將正則表達式分別應用到每一行上去。在這些工具中,字符串是不包含新行符的。因此“.”也就從不匹配新行符。

          現代的工具和語言能夠將正則表達式應用到很大的字符串甚至整個文件上去。本教程討論的所有正則表達式實現都提供一個選項,可以使“.”匹配所有的字符,包括新行符。在RegexBuddy, EditPad Pro或PowerGREP等工具中,你可以簡單的選中“點號匹配新行符”。在Perl中,“.”可以匹配新行符的模式被稱作“單行模式”。很不幸,這是一個很容易混淆的名詞。因為還有所謂“多行模式”。多行模式只影響行首行尾的錨定(anchor),而單行模式只影響“.”。

          其他語言和正則表達式庫也采用了Perl的術語定義。當在.NET Framework中使用正則表達式類時,你可以用類似下面的語句來激活單行模式:Regex.Match(“string”,”regex”,RegexOptions.SingleLine)

          ?·保守的使用點號“.”

          點號可以說是最強大的元字符。它允許你偷懶:用一個點號,就能匹配幾乎所有的字符。但是問題在于,它也常常會匹配不該匹配的字符。

          我會以一個簡單的例子來說明。讓我們看看如何匹配一個具有“mm/dd/yy”格式的日期,但是我們想允許用戶來選擇分隔符。很快能想到的一個方案是<<\d\d.\d\d.\d\d>>。看上去它能匹配日期“02/12/03”。問題在于02512703也會被認為是一個有效的日期。

          <<\d\d[-/.]\d\d[-/.]\d\d>>看上去是一個好一點的解決方案。記住點號在一個字符集里不是元字符。這個方案遠不夠完善,它會匹配“99/99/99”。而<<[0-1]\d[-/.][0-3]\d[-/.]\d\d>>又更進一步。盡管他也會匹配“19/39/99”。你想要你的正則表達式達到如何完美的程度取決于你想達到什么樣的目的。如果你想校驗用戶輸入,則需要盡可能的完美。如果你只是想分析一個已知的源,并且我們知道沒有錯誤的數據,用一個比較好的正則表達式來匹配你想要搜尋的字符就已經足夠。

          ?8.字符串開始和結束的錨定

          錨定和一般的正則表達式符號不同,它不匹配任何字符。相反,他們匹配的是字符之前或之后的位置。“^”匹配一行字符串第一個字符前的位置。<<^a>>將會匹配字符串“abc”中的a。<<^b>>將不會匹配“abc”中的任何字符。

          類似的,$匹配字符串中最后一個字符的后面的位置。所以<<c$>>匹配“abc”中的c。

          ?·錨定的應用

          在編程語言中校驗用戶輸入時,使用錨定是非常重要的。如果你想校驗用戶的輸入為整數,用<<^\d+$>>。

          用戶輸入中,常常會有多余的前導空格或結束空格。你可以用<<^\s*>>和<<\s*$>>來匹配前導空格或結束空格。

          ?·使用“^”和“$”作為行的開始和結束錨定

          如果你有一個包含了多行的字符串。例如:“first line\n\rsecond line”(其中\n\r表示一個新行符)。常常需要對每行分別處理而不是整個字符串。因此,幾乎所有的正則表達式引擎都提供一個選項,可以擴展這兩種錨定的含義。“^”可以匹配字串的開始位置(在f之前),以及每一個新行符的后面位置(在\n\r和s之間)。類似的,$會匹配字串的結束位置(最后一個e之后),以及每個新行符的前面(在e與\n\r之間)。

          在.NET中,當你使用如下代碼時,將會定義錨定匹配每一個新行符的前面和后面位置:Regex.Match("string", "regex", RegexOptions.Multiline)

          應用:string str = Regex.Replace(Original, "^", "> ", RegexOptions.Multiline)--將會在每行的行首插入“> ”。

          · 絕對錨定

          <<\A>>只匹配整個字符串的開始位置,<<\Z>>只匹配整個字符串的結束位置。即使你使用了“多行模式”,<<\A>>和<<\Z>>也從不匹配新行符。

          即使\Z和$只匹配字符串的結束位置,仍然有一個例外的情況。如果字符串以新行符結束,則\Z和$將會匹配新行符前面的位置,而不是整個字符串的最后面。這個“改進”是由Perl引進的,然后被許多的正則表達式實現所遵循,包括Java,.NET等。如果應用<<^[a-z]+$>>到“joe\n”,則匹配結果是“joe”而不是“joe\n”。

          posted @ 2007-02-28 18:35 點滴鑄就輝煌 閱讀(207) | 評論 (0)編輯 收藏
           

          radic 發表于 2006-12-15 12:24:05
          作者:Radic???? 來源:sun
          評論數:5 點擊數:592???? 投票總得分:6 投票總人次:2
          關鍵字:Java;安全編碼

          摘要:

          本文是來自Sun官方站點的一篇關于如何編寫安全的Java代碼的指南,開發者在編寫一般代碼時,可以參照本文的指南
          本文是來自Sun官方站點的一篇關于如何編寫安全的Java代碼的指南,開發者在編寫一般代碼時,可以參照本文的指南:

          ?????????靜態字段
          ?????????縮小作用域
          ?????????公共方法和字段
          ?????????保護包
          ?????????equals方法
          ?????????如果可能使對象不可改變
          ?????????不要返回指向包含敏感數據的內部數組的引用
          ?????????不要直接存儲用戶提供的數組
          ?????????序列化
          ?????????原生函數
          ?????????清除敏感信息


          靜態字段
          ?????????避免使用非final的公共靜態變量
          應盡可能地避免使用非final公共靜態變量,因為無法判斷代碼有無權限改變這些變量值。
          ?????????一般地,應謹慎使用易變的靜態狀態,因為這可能導致設想中相互獨立的子系統之間發生不可預知的交互。

          縮小作用域
          作為一個慣例,盡可能縮小方法和字段的作用域。檢查包訪問權限的成員能否改成私有的,保護類型的成員可否改成包訪問權限的或者私有的,等等。

          公共方法/字段
          避免使用公共變量,而是使用訪問器方法訪問這些變量。用這種方式,如果需要,可能增加集中安全控制。
          對于任何公共方法,如果它們能夠訪問或修改任何敏感內部狀態,務必使它們包含安全控制。
          參考如下代碼段,該代碼段中不可信任代碼可能設置TimeZone的值:
          private static TimeZone??defaultZone = null;

          ??????public static synchronized void setDefault(TimeZone zone)
          ??????{
          ??????????defaultZone = zone;
          ??????}


          保護包
          有時需要在全局防止包被不可信任代碼訪問,本節描述了一些防護技術:
          ?????????防止包注入:如果不可信任代碼想要訪問類的包保護成員,可以嘗試在被攻擊的包內定義自己的新類用以獲取這些成員的訪問權。防止這類攻擊的方式有兩種:
          1.????????通過向java.security.properties文件中加入如下文字防止包內被注入惡意類。
          ??????????... 
          package.definition=Package#1 [,Package#2,...,Package#n]

          ...


          這會導致當試圖在包內定義新類時類裝載器的defineClass方法會拋出異常,除非賦予代碼一下權限:
          ... 
          RuntimePermission("defineClassInPackage."+package)

          ...


          2.????????另一種方式是通過將包內的類加入到封裝的Jar文件里。
          (參看http://java.sun.com/j2se/sdk/1.2/docs/guide/extensions/spec.html)
          ????通過使用這種技巧,代碼無法獲得擴展包的權限,因此也無須修改java.security.properties文件。
          ?????????防止包訪問:通過限制包訪問并僅賦予特定代碼訪問權限防止不可信任代碼對包成員的訪問。通過向java.security.properties文件中加入如下文字可以達到這一目的:
          ??????... 
          package.access=Package#1 [,Package#2,...,Package#n]

          ...


          這會導致當試圖在包內定義新類時類裝載器的defineClass方法會拋出異常,除非賦予代碼一下權限:
          ... 
          RuntimePermission("defineClassInPackage."+package)

          ...


          如果可能使對象不可改變
          如果可能,使對象不可改變。如果不可能,使得它們可以被克隆并返回一個副本。如果返回的對象是數組、向量或哈希表等,牢記這些對象不能被改變,調用者修改這些對象的內容可能導致安全漏洞。此外,因為不用上鎖,不可改變性能夠提高并發性。參考Clear sensitive information了解該慣例的例外情況。

          不要返回指向包含敏感數據的內部數組的引用
          該慣例僅僅是不可變慣例的變型,在這兒提出是因為常常在這里犯錯。即使數組中包含不可變的對象(如字符串),也要返回一個副本這樣調用者不能修改數組中的字符串。不要傳回一個數組,而是數組的拷貝。

          不要直接在用戶提供的數組里存儲
          該慣例僅僅是不可變慣例的另一個變型。使用對象數組的構造器和方法,比如說PubicKey數組,應當在將數組存儲到內部之前克隆數組,而不是直接將數組引用賦給同樣類型的內部變量。缺少這個警惕,用戶對外部數組做得任何變動(在使用討論中的構造器創建對象后)可能意外地更改對象的內部狀態,即使該對象可能是無法改變的

          序列化
          當對對象序列化時,直到它被反序列化,它不在Java運行時環境的控制之下,因此也不在Java平臺提供的安全控制范圍內。
          在實現Serializable時務必將以下事宜牢記在心:
          ?????????transient

          在包含系統資源的直接句柄和相對地址空間信息的字段前使用transient關鍵字。 如果資源,如文件句柄,不被聲明為transient,該對象在序列化狀態下可能會被修改,從而使得被反序列化后獲取對資源的不當訪問。

          ?????????特定類的序列化/反序列化方法

          為了確保反序列化對象不包含違反一些不變量集合的狀態,類應該定義自己的反序列化方法并使用ObjectInputValidation接口驗證這些變量。

          如果一個類定義了自己的序列化方法,它就不能向任何DataInput/DataOuput方法傳遞內部數組。所有的DataInput/DataOuput方法都能被重寫。注意默認序列化不會向DataInput/DataOuput字節數組方法暴露私有字節數組字段。

          如果Serializable類直接向DataOutput(write(byte [] b))方法傳遞了一個私有數組,那么黑客可以創建ObjectOutputStream的子類并覆蓋write(byte [] b)方法,這樣他可以訪問并修改私有數組。下面示例說明了這個問題。
          你的類:
          ??????public class YourClass implements Serializable {

          ????????????private byte [] internalArray;
          ....
          private synchronized void writeObject(ObjectOutputStream stream) {
          ...

          ?????????????? stream.write(internalArray);
          ????????????????...
          }
          }


          黑客代碼

          ?????? public class HackerObjectOutputStream extends ObjectOutputStream{
          ????????????public void write (byte [] b) {
          ?????????????? Modify b
          ??????}
          }
          ...
          ???????????? YourClass yc = new YourClass();
          ??????????????...

          ???????????? HackerObjectOutputStream hoos = new HackerObjectOutputStream();

          ??????????????hoos.writeObject(yc);


          ?????????字節流加密

          保護虛擬機外的字節流的另一方式是對序列化包產生的流進行加密。字節流加密防止解碼或讀取被序列化的對象的私有狀態。如果決定加密,應該管理好密鑰,密鑰的存放地點以及將密鑰交付給反序列化程序的方式等。

          ?????????需要提防的其他事宜

          如果不可信任代碼無法創建對象,務必確保不可信任代碼也不能反序列化對象。切記對對象反序列化是創建對象的另一途徑。
          比如說,如果一個applet創建了一個frame,在該frame上創建了警告標簽。如果該frame被另一應用程序序列化并被一個applet反序列化,務必使該frame出現時帶有同一個警告標簽。

          原生方法
          應從以下幾個方面檢查原生方法:
          ?????????它們返回什么
          ?????????它們需要什么參數
          ?????????它們是否繞過了安全檢查
          ?????????它們是否是公共的,私有的等
          ?????????它們是否包含能繞過包邊界的方法調用,從而繞過包保護

          清除敏感信息
          當保存敏感信息時,如機密,盡量保存在如數組這樣的可變數據類型中,而不是保存在字符串這樣的不可變對象中,這樣使得敏感信息可以盡早顯式地被清除。不要指望Java平臺的自動垃圾回收來做這種清除,因為回收器可能不會清除這段內存,或者很久后才會回收。盡早清除信息使得來自虛擬機外部的堆檢查攻擊變得困難。
          posted @ 2006-12-19 09:29 點滴鑄就輝煌 閱讀(217) | 評論 (0)編輯 收藏
           

          MySQL從3.23.15版本以后提供數據庫復制功能。利用該功能可以實現兩個數據庫同步,主從模式,互相備份模式的功能

          數據庫同步復制功能的設置都在mysql的設置文件中體現。mysql的配置文件(一般是my.cnf),在unix環境下在/etc/mysql/my.cnf 或者在mysql用戶的home目錄下的my.cnf。

          window環境中,如果c:根目錄下有my.cnf文件則取該配置文件。當運行mysql的winmysqladmin.exe工具時候,該工具會把c:根目錄下的my.cnf 命名為mycnf.bak。并在winnt目錄下創建my.ini。mysql服務器啟動時候會讀該配置文件。所以可以把my.cnf中的內容拷貝到my.ini文件中,用my.ini文件作為mysql服務器的配置文件。

          設置方法:

          設置范例環境:

          操作系統:window2000 professional

          mysql:4.0.4-beta-max-nt-log

          A ip:10.10.10.22

          B ip:10.10.10.53

          A:設置

          1.增加一個用戶最為同步的用戶帳號:

          																GRANT FILE ON *.* TO backup@'10.10.10.53' IDENTIFIED BY ‘1234’
          														

          2.增加一個數據庫作為同步數據庫:

          																create database backup
          														

          B:設置

          1.增加一個用戶最為同步的用戶帳號:

          																GRANT FILE ON *.* TO backup@'10.10.10.22' IDENTIFIED BY ‘1234’
          														

          2.增加一個數據庫作為同步數據庫:

          																create database backup
          														

          主從模式:A->B

          A為master

          修改A mysql的my.ini文件。在mysqld配置項中加入下面配置:

          server-id=1log-bin#設置需要記錄log 可以設置log-bin=c:mysqlbakmysqllog 設置日志文件的目錄,#其中mysqllog是日志文件的名稱,mysql將建立不同擴展名,文件名為mysqllog的幾個日志文件。binlog-do-db=backup #指定需要日志的數據庫

          重起數據庫服務。

          用show master status 命令看日志情況。

          B為slave

          修改B mysql的my.ini文件。在mysqld配置項中加入下面配置:

          																server-id=2master-host=10.10.10.22master-user=backup
          														

          #同步用戶帳號

          																master-password=1234master-port=3306master-connect-retry=60
          														

          預設重試間隔60秒replicate-do-db=backup 告訴slave只做backup數據庫的更新

          重起數據庫

          用show slave status看同步配置情況。

          注意:由于設置了slave的配置信息,mysql在數據庫目錄下生成master.info,所以如有要修改相關slave的配置要先刪除該文件。否則修改的配置不能生效。

          雙機互備模式。

          如果在A加入slave設置,在B加入master設置,則可以做B->A的同步。

          在A的配置文件中 mysqld 配置項加入以下設置:

          																master-host=10.10.10.53master-user=backupmaster-password=1234replicate-do-db=
          backupmaster-connect-retry=10
          														

          在B的配置文件中 mysqld 配置項加入以下設置:

          																log-bin=c:mysqllogmysqllogbinlog-do-db=backup
          														

          注意:當有錯誤產生時*.err日志文件。同步的線程退出,當糾正錯誤后要讓同步機制進行工作,運行slave start

          重起AB機器,則可以實現雙向的熱備。

          測試:

          向B批量插入大數據量表AA(1872000)條,A數據庫每秒鐘可以更新2500條數據。

          posted @ 2006-11-14 11:29 點滴鑄就輝煌 閱讀(265) | 評論 (0)編輯 收藏
           
          優化數據庫的思想:
          ? ================
          ? 1、關鍵字段建立索引。
          ? 2、使用存儲過程,它使SQL變得更加靈活和高效。
          ? 3、備份數據庫和清除垃圾數據。
          ? 4、SQL語句語法的優化。(可以用Sybase的SQL Expert,可惜我沒找到unexpired的
          序列號)
          ? 5、清理刪除日志。

          ? SQL語句優化的原則: ?
          ? ==================
          ? 1、使用索引來更快地遍歷表。
          ? 缺省情況下建立的索引是非群集索引,但有時它并不是最佳的。在非群集索引下,數據在物理上隨機存放在數據頁上。合理的索引設計要建立在對各種查詢的分析和預測上。一般來說:
          ? ①.有大量重復值、且經常有范圍查詢
          ? (between, > ,< ,> =,< =)和order by、group by發生的列,可考慮建立群集索引;
          ? ②.經常同時存取多列,且每列都含有重復值可考慮建立組合索引;
          ? ③.組合索引要盡量使關鍵查詢形成索引覆蓋,其前導列一定是使用最頻繁的列。索引雖有助于提高性能但不是索引越多越好,恰好相反過多的索引會導致系統低效。用戶在表中每加進一個索引,維護索引集合就要做相應的更新工作。
          ? 2、IS NULL 與 IS NOT NULL
          ? 不能用null作索引,任何包含null值的列都將不會被包含在索引中。即使索引有多列這樣的情況下,只要這些列中有一列含有null,該列就會從索引中排除。也就是說如果某列存在空值,即使對該列建索引也不會提高性能。任何在where子句中使用is null或is not null的語句優化器是不允許使用索引的。
          ? 3、IN和EXISTS
          ? EXISTS要遠比IN的效率高。里面關系到full table scan和range scan。幾乎將所有的IN操作符子查詢改寫為使用EXISTS的子查詢。
          ? 4、在海量查詢時盡量少用格式轉換。
          ? 5、當在SQL SERVER 2000中,如果存儲過程只有一個參數,并且是OUTPUT類型的,必須在調用這個存儲過程的時候給這個參數一個初始的值,否則會出現調用錯誤。
          ? 6、ORDER BY和GROPU BY
          ? 使用ORDER BY和GROUP BY短語,任何一種索引都有助于SELECT的性能提高。注意如果索引列里面有NULL值,Optimizer將無法優化。
          ? 7、任何對列的操作都將導致表掃描,它包括數據庫函數、計算表達式等等,查詢時要盡可能將操作移至等號右邊。
          ? 8、IN、OR子句常會使用工作表,使索引失效。如果不產生大量重復值,可以考慮把子句拆開。拆開的子句中應該包含索引。
          ? 9、SET SHOWPLAN_ALL ON 查看執行方案。DBCC檢查數據庫數據完整性。
          ? DBCC(DataBase Consistency Checker)是一組用于驗證 SQL Server 數據庫完整性的程序。
          ? 10、慎用游標
          ? 在某些必須使用游標的場合,可考慮將符合條件的數據行轉入臨時表中,再對臨時表定義游標進行操作,這樣可使性能得到明顯提高。

          ? 總結:
          ? 所謂優化即WHERE子句利用了索引,不可優化即發生了表掃描或額外開銷。經驗顯示,SQL Server性能的最大改進得益于邏輯的數據庫設計、索引設計和查詢設計方面。反過來說,最大的性能問題常常是由其中這些相同方面中的不足引起的。其實SQL優化的實質就是在結果正確的前提下,用優化器可以識別的語句,充份利用索引,減少表掃描的I/O次數,盡量避免表搜索的發生。其實SQL的性能優化是一個復雜的過程,上述這些只是在應用層次的一種體現,深入研究還會涉及數據庫層的資源配置、網絡層的流量控制以及操作系統層的總體設計。?
          posted @ 2006-11-12 11:27 點滴鑄就輝煌 閱讀(241) | 評論 (0)編輯 收藏
           

          關注beanaction時,查到的資料,順便做個備份

          多數IT 組織都必須解決三個主要問題:1.幫助組織減少成本 2.增加并且保持客戶 3.加快業務效率。完成這些問題一般都需要實現對多個業務系統的數據和業務邏輯的無縫訪問,也就是說,要實施系統集成工程,以便聯結業務流程、實現數據的訪問與共享。

          JpetStore 4.0是ibatis的最新示例程序,基于Struts MVC框架(注:非傳統Struts開發模式),以ibatis作為持久化層。該示例程序設計優雅,層次清晰,可以學習以及作為一個高效率的編程模型參考。本文是在其基礎上,采用Spring對其中間層(業務層)進行改造。使開發量進一步減少,同時又擁有了Spring的一些好處…

          1. 前言
          JpetStore 4.0是ibatis的最新示例程序。ibatis是開源的持久層產品,包含SQL Maps 2.0 和 Data Access Objects 2.0 框架。JpetStore示例程序很好的展示了如何利用ibatis來開發一個典型的J2EE web應用程序。JpetStore有如下特點:

          • ibatis數據層
          • POJO業務層
          • POJO領域類
          • Struts MVC
          • JSP 表示層

          以下是本文用到的關鍵技術介紹,本文假設您已經對Struts,SpringFramewok,ibatis有一定的了解,如果不是,請首先查閱附錄中的參考資料。

          • Struts 是目前Java Web MVC框架中不爭的王者。經過長達五年的發展,Struts已經逐漸成長為一個穩定、成熟的框架,并且占有了MVC框架中最大的市場份額。但是Struts某些技術特性上已經落后于新興的MVC框架。面對Spring MVC、Webwork2 這些設計更精密,擴展性更強的框架,Struts受到了前所未有的挑戰。但站在產品開發的角度而言,Struts仍然是最穩妥的選擇。本文的原型例子JpetStore 4.0就是基于Struts開發的,但是不拘泥于Struts的傳統固定用法,例如只用了一個自定義Action類,并且在form bean類的定義上也是開創性的,令人耳目一新,稍后將具體剖析一下。
          • Spring Framework 實際上是Expert One-on-One J2EE Design and Development 一書中所闡述的設計思想的具體實現。Spring Framework的功能非常多。包含AOP、ORM、DAO、Context、Web、MVC等幾個部分組成。Web、MVC暫不用考慮,JpetStore 4.0用的是更成熟的Struts和JSP;DAO由于目前Hibernate、JDO、ibatis的流行,也不考慮,JpetStore 4.0用的就是ibatis。因此最需要用的是AOP、ORM、Context。Context中,最重要的是Beanfactory,它能將接口與實現分開,非常強大。目前AOP應用最成熟的還是在事務管理上。
          • ibatis 是一個功能強大實用的SQL Map工具,不同于其他ORM工具(如hibernate),它是將SQL語句映射成Java對象,而對于ORM工具,它的SQL語句是根據映射定義生成的。ibatis 以SQL開發的工作量和數據庫移植性上的讓步,為系統設計提供了更大的自由空間。有ibatis代碼生成的工具,可以根據DDL自動生成ibatis代碼,能減少很多工作量。

          2. JpetStore簡述

          2.1. 背景
          最初是Sun公司的J2EE petstore,其最主要目的是用于學習J2EE,但是其缺點也很明顯,就是過度設計了。接著Oracle用J2EE petstore來比較各應用服務器的性能。微軟推出了基于.Net平臺的 Pet shop,用于競爭J2EE petstore。而JpetStore則是經過改良的基于struts的輕便框架J2EE web應用程序,相比來說,JpetStore設計和架構更優良,各層定義清晰,使用了很多最佳實踐和模式,避免了很多"反模式",如使用存儲過程,在java代碼中嵌入SQL語句,把HTML存儲在數據庫中等等。最新版本是JpetStore 4.0。

          2.2. JpetStore開發運行環境的建立
          1、開發環境

          • Java SDK 1.4.2
          • Apache Tomcat 4.1.31
          • Eclipse-SDK-3.0.1-win32
          • HSQLDB 1.7.2

          2、Eclipse插件

          • EMF SDK 2.0.1:Eclipse建模框架,lomboz插件需要,可以使用runtime版本。
          • lomboz 3.0:J2EE插件,用來在Eclipse中開發J2EE應用程序
          • Spring IDE 1.0.3:Spring Bean配置管理插件
          • xmlbuddy_2.0.10:編輯XML,用免費版功能即可
          • tomcatPluginV3:tomcat管理插件
          • Properties Editor:編輯java的屬性文件,并可以預覽以及自動存盤為Unicode格式。免去了手工或者ANT調用native2ascii的麻煩。

          3、示例源程序

          • ibatis示例程序JpetStore 4.0 http://www.ibatis.com/jpetstore/jpetstore.html
          • 改造后的源程序(+spring)(源碼鏈接)

          2.3. 架構

          圖1 JpetStore架構圖
          圖1 JpetStore架構圖

          圖1 是JPetStore架構圖,更詳細的內容請參見JPetStore的白皮書。參照這個架構圖,讓我們稍微剖析一下源代碼,得出JpetStore 4.0的具體實現圖(見圖2),思路一下子就豁然開朗了。前言中提到的非傳統的struts開發模式,關鍵就在struts Action類和form bean類上。

          struts Action類只有一個:BeanAction。沒錯,確實是一個!與傳統的struts編程方式很不同。再仔細研究BeanAction類,發現它其實是一個通用類,利用反射原理,根據URL來決定調用formbean的哪個方法。BeanAction大大簡化了struts的編程模式,降低了對struts的依賴(與struts以及WEB容器有關的幾個類都放在com.ibatis.struts包下,其它的類都可以直接復用)。利用這種模式,我們會很容易的把它移植到新的框架如JSF,spring。

          這樣重心就轉移到form bean上了,它已經不是普通意義上的form bean了。查看源代碼,可以看到它不僅僅有數據和校驗/重置方法,而且已經具有了行為,從這個意義上來說,它更像一個BO(Business Object)。這就是前文講到的,BeanAction類利用反射原理,根據URL來決定調用form bean的哪個方法(行為)。form bean的這些方法的簽名很簡單,例如:


          												
           public String myActionMethod() {
             //..work
             return "success";
           }
           
          										

          方法的返回值直接就是字符串,對應的是forward的名稱,而不再是ActionForward對象,創建ActionForward對象的任務已經由BeanAction類代勞了。

          另外,程序還提供了ActionContext工具類,該工具類封裝了request 、response、form parameters、request attributes、session attributes和 application attributes中的數據存取操作,簡單而線程安全,form bean類使用該工具類可以進一步從表現層框架解耦。

          在這里需要特別指出的是,BeanAction類是對struts擴展的一個有益嘗試,雖然提供了非常好的應用開發模式,但是它還非常新,一直在發展中。

          圖2 JpetStore 4.0具體實現
          圖2 JpetStore 4.0具體實現

          2.4. 代碼剖析
          下面就讓我們開始進一步分析JpetStore4.0的源代碼,為下面的改造鋪路。

          • BeanAction.java是唯一一個Struts action類,位于com.ibatis.struts包下。正如上文所言,它是一個通用的控制類,利用反射機制,把控制轉移到form bean的某個方法來處理。詳細處理過程參考其源代碼,簡單明晰。
          • Form bean類位于com.ibatis.jpetstore.presentation包下,命名規則為***Bean。Form bean類全部繼承于BaseBean類,而BaseBean類實際繼承于ActionForm,因此,Form bean類就是Struts的 ActionForm,Form bean類的屬性數據就由struts框架自動填充。而實際上,JpetStore4.0擴展了struts中ActionForm的應用: Form bean類還具有行為,更像一個BO,其行為(方法)由BeanAction根據配置(struts-config.xml)的URL來調用。雖然如此,我們還是把Form bean類定位于表現層。

            Struts-config.xml的配置里有3種映射方式,來告訴BeanAction把控制轉到哪個form bean對象的哪個方法來處理。

            以這個請求連接為例http://localhost/jpetstore4/shop/viewOrder.do

            1. URL Pattern

            																
                <action path="/shop/viewOrder" type="com.ibatis.struts.BeanAction"
                name="orderBean" scope="session"
                validate="false">
                <forward name="success" path="/order/ViewOrder.jsp"/>
              </action>
              
            														

            此種方式表示,控制將被轉發到"orderBean"這個form bean對象 的"viewOrder"方法(行為)來處理。方法名取"path"參數的以"/"分隔的最后一部分。

            2. Method Parameter

            																
                <action path="/shop/viewOrder" type="com.ibatis.struts.BeanAction"
                name="orderBean" parameter="viewOrder" scope="session"
                validate="false">
                <forward name="success" path="/order/ViewOrder.jsp"/>
              </action>
              
            														

            此種方式表示,控制將被轉發到"orderBean"這個form bean對象的"viewOrder"方法(行為)來處理。配置中的"parameter"參數表示form bean類上的方法。"parameter"參數優先于"path"參數。

            3. No Method call

            																
                <action path="/shop/viewOrder" type="com.ibatis.struts.BeanAction"
                name="orderBean" parameter="*" scope="session"
                validate="false">
                <forward name="success" path="/order/ViewOrder.jsp"/>
              </action>
              
            														

            此種方式表示,form bean上沒有任何方法被調用。如果存在"name"屬性,則struts把表單參數等數據填充到form bean對象后,把控制轉發到"success"。否則,如果name為空,則直接轉發控制到"success"。

            這就相當于struts內置的org.apache.struts.actions.ForwardAction的功能

            																
             <action path="/shop/viewOrder" type="org.apache.struts.actions.ForwardAction"
                parameter="/order/ViewOrder.jsp " scope="session" validate="false">
             </action>
             
            														
          • Service類位于com.ibatis.jpetstore.service包下,屬于業務層。這些類封裝了業務以及相應的事務控制。Service類由form bean類來調用。
          • com.ibatis.jpetstore.persistence.iface包下的類是DAO接口,屬于業務層,其屏蔽了底層的數據庫操作,供具體的Service類來調用。DaoConfig類是工具類(DAO工廠類),Service類通過DaoConfig類來獲得相應的DAO接口,而不用關心底層的具體數據庫操作,實現了如圖2中{耦合2}的解耦。
          • com.ibatis.jpetstore.persistence.sqlmapdao包下的類是對應DAO接口的具體實現,在JpetStore4.0中采用了ibatis來實現ORM。這些實現類繼承BaseSqlMapDao類,而BaseSqlMapDao類則繼承ibatis DAO 框架中的SqlMapDaoTemplate類。ibatis的配置文件存放在com.ibatis.jpetstore.persistence.sqlmapdao.sql目錄下。這些類和配置文件位于數據層
          • Domain類位于com.ibatis.jpetstore.domain包下,是普通的javabean。在這里用作數據傳輸對象(DTO),貫穿視圖層、業務層和數據層,用于在不同層之間傳輸數據。

          剩下的部分就比較簡單了,請看具體的源代碼,非常清晰。

          2.5. 需要改造的地方
          JpetStore4.0的關鍵就在struts Action類和form bean類上,這也是其精華之一(雖然該實現方式是試驗性,待擴充和驗證),在此次改造中我們要保留下來,即控制層一點不變,表現層獲取相應業務類的方式變了(要加載spring環境),其它保持不變。要特別關注的改動是業務層和持久層,幸運的是JpetStore4.0設計非常好,需要改動的地方非常少,而且由模式可循,如下:

          1. 業務層和數據層用Spring BeanFactory機制管理。

          2. 業務層的事務由spring 的aop通過聲明來完成。

          3. 表現層(form bean)獲取業務類的方法改由自定義工廠類來實現(加載spring環境)。

          3. JPetStore的改造

          3.1. 改造后的架構


          其中紅色部分是要增加的部分,藍色部分是要修改的部分。下面就讓我們逐一剖析。

          3.2. Spring Context的加載
          為了在Struts中加載Spring Context,一般會在struts-config.xml的最后添加如下部分:


          												
          <plug-in className="org.springframework.web.struts.ContextLoaderPlugIn">
          <set-property property="contextConfigLocation"
          value="/WEB-INF/applicationContext.xml" />
          </plug-in>
          
          										

          Spring在設計時就充分考慮到了與Struts的協同工作,通過內置的Struts Plug-in在兩者之間提供了良好的結合點。但是,因為在這里我們一點也不改動JPetStore的控制層(這是JpetStore4.0的精華之一),所以本文不準備采用此方式來加載ApplicationContext。我們利用的是spring framework 的BeanFactory機制,采用自定義的工具類(bean工廠類)來加載spring的配置文件,從中可以看出Spring有多靈活,它提供了各種不同的方式來使用其不同的部分/層次,您只需要用你想用的,不需要的部分可以不用。

          具體的來說,就是在com.ibatis.spring包下創建CustomBeanFactory類,spring的配置文件applicationContext.xml也放在這個目錄下。以下就是該類的全部代碼,很簡單:


          												
          public final class CustomBeanFactory {
          	static XmlBeanFactory factory = null;
          	static {
          		Resource is = new
          InputStreamResource( CustomBeanFactory.class.getResourceAsStream("applicationContext.xml"));
          		factory = new XmlBeanFactory(is);			
          	}
          	public static Object getBean(String beanName){
          		return factory.getBean(beanName);
          	}
          }
          
          										

          實際上就是封裝了Spring 的XMLBeanFactory而已,并且Spring的配置文件只需要加載一次,以后就可以直接用CustomBeanFactory.getBean("someBean")來獲得需要的對象了(例如someBean),而不需要知道具體的類。CustomBeanFactory類用于{耦合1}的解耦。

          CustomBeanFactory類在本文中只用于表現層的form bean對象獲得service類的對象,因為我們沒有把form bean對象配置在applicationContext.xml中。但是,為什么不把表現層的form bean類也配置起來呢,這樣就用不著這CustomBeanFactory個類了,Spring會幫助我們創建需要的一切?問題的答案就在于form bean類是struts的ActionForm類!如果大家熟悉struts,就會知道ActionForm類是struts自動創建的:在一次請求中,struts判斷,如果ActionForm實例不存在,就創建一個ActionForm對象,把客戶提交的表單數據保存到ActionForm對象中。因此formbean類的對象就不能由spring來創建,但是service類以及數據層的DAO類可以,所以只有他們在spring中配置。

          所以,很自然的,我們就創建了CustomBeanFactory類,在表現層來銜接struts和spring。就這么簡單,實現了另一種方式的{耦合一}的解耦。

          3.3. 表現層
          上 面分析到,struts和spring是在表現層銜接起來的,那么表現層就要做稍微的更改,即所需要的service類的對象創建上。以表現層的AccountBean類為例:

          原來的源代碼如下


          												
          														 private static final AccountService accountService = AccountService.getInstance();
            private static final CatalogService catalogService = CatalogService.getInstance();
            
          												
          										

          改造后的源代碼如下


          												
            private static final AccountService accountService = (AccountService)CustomBeanFactory.getBean("AccountService");
            private static final CatalogService catalogService = (CatalogService)CustomBeanFactory.getBean("CatalogService");
          
          										

          其他的幾個presentation類以同樣方式改造。這樣,表現層就完成了。關于表現層的其它部分如JSP等一概不動。也許您會說,沒有看出什么特別之處的好處啊?你還是額外實現了一個工廠類。別著急,帷幕剛剛開啟,spring是在表現層引入,但您發沒發現:

          • presentation類僅僅面向service類的接口編程,具體"AccountService"是哪個實現類,presentation類不知道,是在spring的配置文件里配置。(本例中,為了最大限度的保持原來的代碼不作變化,沒有抽象出接口)。Spring鼓勵面向接口編程,因為是如此的方便和自然,當然您也可以不這么做。
          • CustomBeanFactory這個工廠類為什么會如此簡單,因為其直接使用了Spring的BeanFactory。Spring從其核心而言,是一個DI容器,其設計哲學是提供一種無侵入式的高擴展性的框架。為了實現這個目標,Spring 大量引入了Java 的Reflection機制,通過動態調用的方式避免硬編碼方式的約束,并在此基礎上建立了其核心組件BeanFactory,以此作為其依賴注入機制的實現基礎。org.springframework.beans包中包括了這些核心組件的實現類,核心中的核心為BeanWrapper和BeanFactory類。

          3.4. 持久層
          在討論業務層之前,我們先看一下持久層,如下圖所示:


          在上文中,我們把iface包下的DAO接口歸為業務層,在這里不需要做修改。ibatis的sql配置文件也不需要改。要改的是DAO實現類,并在spring的配置文件中配置起來。

          1、修改基類

          所有的DAO實現類都繼承于BaseSqlMapDao類。修改BaseSqlMapDao類如下:


          												
          public class BaseSqlMapDao extends SqlMapClientDaoSupport {
            protected static final int PAGE_SIZE = 4;
            protected SqlMapClientTemplate smcTemplate = this.getSqlMapClientTemplate();
            public BaseSqlMapDao() { 
          	}
          }
          
          										

          使BaseSqlMapDao類改為繼承于Spring提供的SqlMapClientDaoSupport類,并定義了一個保護屬性smcTemplate,其類型為SqlMapClientTemplate。關于SqlMapClientTemplate類的詳細說明請參照附錄中的"Spring中文參考手冊"

          2、修改DAO實現類

          所有的DAO實現類還是繼承于BaseSqlMapDao類,實現相應的DAO接口,但其相應的DAO操作委托SqlMapClientTemplate來執行,以AccountSqlMapDao類為例,部分代碼如下:


          												
              public List getUsernameList() {
              return smcTemplate.queryForList("getUsernameList", null);
            }
            public Account getAccount(String username, String password) {
              Account account = new Account();
              account.setUsername(username);
              account.setPassword(password);
              return (Account) smcTemplate.queryForObject("getAccountByUsernameAndPassword", account);
            }
            public void insertAccount(Account account) {
            	smcTemplate.update("insertAccount", account);
            	smcTemplate.update("insertProfile", account);
            	smcTemplate.update("insertSignon", account);
            }
            
          										

          就這么簡單,所有函數的簽名都是一樣的,只需要查找替換就可以了!

          3、除去工廠類以及相應的配置文件

          除去DaoConfig.java這個DAO工廠類和相應的配置文件dao.xml,因為DAO的獲取現在要用spring來管理。

          4、DAO在Spring中的配置(applicationContext.xml)


          												
              <bean id="dataSource" 
                  class="org.springframework.jdbc.datasource.DriverManagerDataSource">
                  <property name="driverClassName">
                      <value>org.hsqldb.jdbcDriver</value>
                  </property>
                  <property name="url">
                      <value>jdbc:hsqldb:hsql://localhost/xdb</value>
                  </property>
                  <property name="username">
                      <value>sa</value>
                  </property>
                  <property name="password">
                      <value></value>
                  </property>
              </bean>    
              <!-- ibatis sqlMapClient config -->
              <bean id="sqlMapClient" 
                  class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
                  <property name="configLocation">
                      <value> 
                          classpath:com\ibatis\jpetstore\persistence\sqlmapdao\sql\sql-map-config.xml
                      </value>
                  </property>
                  <property name="dataSource">
                      <ref bean="dataSource"/>
                  </property>    
              </bean>
              <!-- Transactions -->
              <bean id="TransactionManager" 
                  class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
                  <property name="dataSource">
                      <ref bean="dataSource"/>
                  </property>
              </bean>
              <!-- persistence layer -->
              <bean id="AccountDao" 
                  class="com.ibatis.jpetstore.persistence.sqlmapdao.AccountSqlMapDao">
                  <property name="sqlMapClient">
                      <ref local="sqlMapClient"/>
                  </property>
              </bean>
              
          										

          具體的語法請參照附錄中的"Spring中文參考手冊"。在這里只簡單解釋一下:

          1. 我們首先創建一個數據源dataSource,在這里配置的是hsqldb數據庫。如果是ORACLE數據庫,driverClassName的值是"oracle.jdbc.driver.OracleDriver",URL的值類似于"jdbc:oracle:thin:@wugfMobile:1521:cdcf"。數據源現在由spring來管理,那么現在我們就可以去掉properties目錄下database.properties這個配置文件了;還有不要忘記修改sql-map-config.xml,去掉<properties resource="properties/database.properties"/>對它的引用。

          2. sqlMapClient節點。這個是針對ibatis SqlMap的SqlMapClientFactoryBean配置。實際上配置了一個sqlMapClient的創建工廠類。configLocation屬性配置了ibatis映射文件的名稱。dataSource屬性指向了使用的數據源,這樣所有使用sqlMapClient的DAO都默認使用了該數據源,除非在DAO的配置中另外顯式指定。

          3. TransactionManager節點。定義了事務,使用的是DataSourceTransactionManager。

          4. 下面就可以定義DAO節點了,如AccountDao,它的實現類是com.ibatis.jpetstore.persistence.sqlmapdao.AccountSqlMapDao,使用的SQL配置從sqlMapClient中讀取,數據庫連接沒有特別列出,那么就是默認使用sqlMapClient配置的數據源datasource。

          這樣,我們就把持久層改造完了,其他的DAO配置類似于AccountDao。怎么樣?簡單吧。這次有接口了:) AccountDao接口->AccountSqlMapDao實現。

          3.5. 業務層
          業務層的位置以及相關類,如下圖所示:


          在這個例子中只有3個業務類,我們以OrderService類為例來改造,這個類是最復雜的,其中涉及了事務。

          1、在ApplicationContext配置文件中增加bean的配置:


          												
              <bean id="OrderService" 
                  class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
                  <property name="transactionManager">
                      <ref local="TransactionManager"></ref>
                  </property>
                  <property name="target">
                      <bean class="com.ibatis.jpetstore.service.OrderService">
                          <property name="itemDao">
                              <ref bean="ItemDao"/>
                          </property>
                          <property name="orderDao">
                              <ref bean="OrderDao"/>
                          </property>
                          <property name="sequenceDao">
                              <ref bean="SequenceDao"/>
                          </property>
                      </bean>
                  </property>
                  <property name="transactionAttributes">
                      <props>
                          <prop key="insert*">PROPAGATION_REQUIRED</prop>
                      </props>
                  </property>
              </bean>
              
          										

          定義了一個OrderService,還是很容易懂的。為了簡單起見,使用了嵌套bean,其實現類是com.ibatis.jpetstore.service.OrderService,分別引用了ItemDao,OrderDao,SequenceDao。該bean的insert*實現了事務管理(AOP方式)。TransactionProxyFactoryBean自動創建一個事務advisor, 該advisor包括一個基于事務屬性的pointcut,因此只有事務性的方法被攔截。

          2、業務類的修改

          以OrderService為例:


          												
          public class OrderService {
          
             /* Private Fields */
            private ItemDao itemDao;
            private OrderDao orderDao;
            private SequenceDao sequenceDao;
          
            /* Constructors */
          
            public OrderService() {
            }
          
          /**
           * @param itemDao 要設置的 itemDao。
           */
          public final void setItemDao(ItemDao itemDao) {
          	this.itemDao = itemDao;
          }
          /**
           * @param orderDao 要設置的 orderDao。
           */
          public final void setOrderDao(OrderDao orderDao) {
          	this.orderDao = orderDao;
          }
          /**
           * @param sequenceDao 要設置的 sequenceDao。
           */
          public final void setSequenceDao(SequenceDao sequenceDao) {
          	this.sequenceDao = sequenceDao;
          }
          //剩下的部分
          …….
          }
          
          										

          紅色部分為修改部分。Spring采用的是Type2的設置依賴注入,所以我們只需要定義屬性和相應的設值函數就可以了,ItemDao,OrderDao,SequenceDao的值由spring在運行期間注入。構造函數就可以為空了,另外也不需要自己編寫代碼處理事務了(事務在配置中聲明),daoManager.startTransaction();等與事務相關的語句也可以去掉了。和原來的代碼比較一下,是不是處理精簡了很多!可以更關注業務的實現。

          4. 結束語
          ibatis是一個功能強大實用的SQL Map工具,可以直接控制SQL,為系統設計提供了更大的自由空間。其提供的最新示例程序JpetStore 4.0,設計優雅,應用了迄今為止很多最佳實踐和設計模式,非常適于學習以及在此基礎上創建輕量級的J2EE WEB應用程序。JpetStore 4.0是基于struts的,本文在此基礎上,最大程度保持了原有設計的精華以及最小的代碼改動量,在業務層和持久化層引入了Spring。在您閱讀了本文以及改造后的源代碼后,會深切的感受到Spring帶來的種種好處:自然的面向接口的編程,業務對象的依賴注入,一致的數據存取框架和聲明式的事務處理,統一的配置文件…更重要的是Spring既是全面的又是模塊化的,Spring有分層的體系結構,這意味著您能選擇僅僅使用它任何一個獨立的部分,就像本文,而它的架構又是內部一致。

          posted @ 2006-11-11 09:59 點滴鑄就輝煌 閱讀(380) | 評論 (0)編輯 收藏
           

          因了需要用到這些信息,所以總結一下,方便以后參閱
          通過request.getHeader("User-Agent")大致可以取得用戶瀏覽器的信息
          如果里面包含:
          "msie"-->MicroSoft?
          "opera" -->Opera Software
          "mozilla"-->Netscape Communications

          如果取瀏覽器版本信息
          String str = request.getHeader("User-Agent");
          MS :? str.substring(str.indexOf("msie") + 5);
          Other :
          tmpString = (str.substring(tmpPos = (str.indexOf("/")) + 1, tmpPos + str.indexOf(" "))).trim(); ?//沒有親自試

          操作系統部分,不啰嗦了
          private void setOs()
          {
          if (this.userAgent.indexOf("win") > -1){
          ? if (this.userAgent.indexOf("windows 95") > -1 || this.userAgent.indexOf("win95") > -1){
          ???? this.os = "Windows 95";
          ? }
          ? if (this.userAgent.indexOf("windows 98") > -1 || this.userAgent.indexOf("win98") > -1){
          ???? this.os = "Windows 98";
          ? }
          ? if (this.userAgent.indexOf("windows nt") > -1 || this.userAgent.indexOf("winnt") > -1){
          ????? this.os = "Windows NT";
          ? }
          ? if (this.userAgent.indexOf("win16") > -1 || this.userAgent.indexOf("windows 3.") > -1){
          ????? this.os = "Windows 3.x";
          ? }
          ?}
          }

          獲取語言request.getHeader("Accept-Language");

          詳細信息可以再分解....

          posted @ 2006-10-24 10:25 點滴鑄就輝煌 閱讀(1089) | 評論 (0)編輯 收藏
           
          Lucene 是基于 Java 的全文信息檢索包,它目前是 Apache Jakarta 家族下面的一個開源項目。在這篇文章中,我們首先來看如何利用 Lucene 實現高級搜索功能,然后學習如何利用 Lucene 來創建一個健壯的 Web 搜索應用程序。

          在本篇文章中,你會學習到如何利用 Lucene 實現高級搜索功能以及如何利用 Lucene 來創建 Web 搜索應用程序。通過這些學習,你就可以利用 Lucene 來創建自己的搜索應用程序。

          架構概覽

          通常一個 Web 搜索引擎的架構分為前端和后端兩部分,就像圖一中所示。在前端流程中,用戶在搜索引擎提供的界面中輸入要搜索的關鍵詞,這里提到的用戶界面一般是一個帶有輸入框的 Web 頁面,然后應用程序將搜索的關鍵詞解析成搜索引擎可以理解的形式,并在索引文件上進行搜索操作。在排序后,搜索引擎返回搜索結果給用戶。在后端流程中,網絡爬蟲或者機器人從因特網上獲取 Web 頁面,然后索引子系統解析這些 Web 頁面并存入索引文件中。如果你想利用 Lucene 來創建一個 Web 搜索應用程序,那么它的架構也和上面所描述的類似,就如圖一中所示。


          Figure 1. Web 搜索引擎架構
          Web搜索引擎架構

          利用 Lucene 實現高級搜索

          Lucene 支持多種形式的高級搜索,我們在這一部分中會進行探討,然后我會使用 Lucene 的 API 來演示如何實現這些高級搜索功能。

          布爾操作符

          大多數的搜索引擎都會提供布爾操作符讓用戶可以組合查詢,典型的布爾操作符有 AND, OR, NOT。Lucene 支持 5 種布爾操作符,分別是 AND, OR, NOT, 加(+), 減(-)。接下來我會講述每個操作符的用法。

          • OR: 如果你要搜索含有字符 A 或者 B 的文檔,那么就需要使用 OR 操作符。需要記住的是,如果你只是簡單的用空格將兩個關鍵詞分割開,其實在搜索的時候搜索引擎會自動在兩個關鍵詞之間加上 OR 操作符。例如,“Java OR Lucene” 和 “Java Lucene” 都是搜索含有 Java 或者含有 Lucene 的文檔。
          • AND: 如果你需要搜索包含一個以上關鍵詞的文檔,那么就需要使用 AND 操作符。例如,“Java AND Lucene” 返回所有既包含 Java 又包含 Lucene 的文檔。
          • NOT: Not 操作符使得包含緊跟在 NOT 后面的關鍵詞的文檔不會被返回。例如,如果你想搜索所有含有 Java 但不含有 Lucene 的文檔,你可以使用查詢語句 “Java NOT Lucene”。但是你不能只對一個搜索詞使用這個操作符,比如,查詢語句 “NOT Java” 不會返回任何結果。
          • 加號(+): 這個操作符的作用和 AND 差不多,但它只對緊跟著它的一個搜索詞起作用。例如,如果你想搜索一定包含 Java,但不一定包含 Lucene 的文檔,就可以使用查詢語句“+Java Lucene”。
          • 減號(-): 這個操作符的功能和 NOT 一樣,查詢語句 “Java -Lucene” 返回所有包含 Java 但不包含 Lucene 的文檔。

          接下來我們看一下如何利用 Lucene 提供的 API 來實現布爾查詢。清單1 顯示了如果利用布爾操作符進行查詢的過程。


          清單1:使用布爾操作符
            //Test boolean operator
          public void testOperator(String indexDirectory) throws Exception{
             Directory dir = FSDirectory.getDirectory(indexDirectory,false);
             IndexSearcher indexSearcher = new IndexSearcher(dir);
             String[] searchWords = {"Java AND Lucene", "Java NOT Lucene", "Java OR Lucene", 
                              "+Java +Lucene", "+Java -Lucene"};
             Analyzer language = new StandardAnalyzer();
             Query query;
             for(int i = 0; i < searchWords.length; i++){
                query = QueryParser.parse(searchWords[i], "title", language);
                Hits results = indexSearcher.search(query);
                System.out.println(results.length() + "search results for query " + searchWords[i]);
             }
          }
          

          域搜索(Field Search)

          Lucene 支持域搜索,你可以指定一次查詢是在哪些域(Field)上進行。例如,如果索引的文檔包含兩個域,TitleContent,你就可以使用查詢 “Title: Lucene AND Content: Java” 來返回所有在 Title 域上包含 Lucene 并且在 Content 域上包含 Java 的文檔。清單 2 顯示了如何利用 Lucene 的 API 來實現域搜索。


          清單2:實現域搜索
          //Test field search
          public void testFieldSearch(String indexDirectory) throws Exception{
              Directory dir = FSDirectory.getDirectory(indexDirectory,false);
              IndexSearcher indexSearcher = new IndexSearcher(dir);
              String searchWords = "title:Lucene AND content:Java";
              Analyzer language = new StandardAnalyzer();
              Query query = QueryParser.parse(searchWords, "title", language);
              Hits results = indexSearcher.search(query);
              System.out.println(results.length() + "search results for query " + searchWords);
          }
          

          通配符搜索(Wildcard Search)

          Lucene 支持兩種通配符:問號(?)和星號(*)。你可以使用問號(?)來進行單字符的通配符查詢,或者利用星號(*)進行多字符的通配符查詢。例如,如果你想搜索 tiny 或者 tony,你就可以使用查詢語句 “t?ny”;如果你想查詢 Teach, Teacher 和 Teaching,你就可以使用查詢語句 “Teach*”。清單3 顯示了通配符查詢的過程。


          清單3:進行通配符查詢
          //Test wildcard search
          public void testWildcardSearch(String indexDirectory)throws Exception{
             Directory dir = FSDirectory.getDirectory(indexDirectory,false);
             IndexSearcher indexSearcher = new IndexSearcher(dir);
             String[] searchWords = {"tex*", "tex?", "?ex*"};
             Query query;
             for(int i = 0; i < searchWords.length; i++){
                query = new WildcardQuery(new Term("title",searchWords[i]));
                Hits results = indexSearcher.search(query);
                System.out.println(results.length() + "search results for query " + searchWords[i]);
             }
          }
          

          模糊查詢

          Lucene 提供的模糊查詢基于編輯距離算法(Edit distance algorithm)。你可以在搜索詞的尾部加上字符 ~ 來進行模糊查詢。例如,查詢語句 “think~” 返回所有包含和 think 類似的關鍵詞的文檔。清單 4 顯示了如果利用 Lucene 的 API 進行模糊查詢的代碼。


          清單4:實現模糊查詢
          //Test fuzzy search
          public void testFuzzySearch(String indexDirectory)throws Exception{
             Directory dir = FSDirectory.getDirectory(indexDirectory,false);
             IndexSearcher indexSearcher = new IndexSearcher(dir);
             String[] searchWords = {"text", "funny"};
             Query query;
             for(int i = 0; i < searchWords.length; i++){
                query = new FuzzyQuery(new Term("title",searchWords[i]));
                Hits results = indexSearcher.search(query);
                System.out.println(results.length() + "search results for query " + searchWords[i]);
             }
          }
          

          范圍搜索(Range Search)

          范圍搜索匹配某個域上的值在一定范圍的文檔。例如,查詢 “age:[18 TO 35]” 返回所有 age 域上的值在 18 到 35 之間的文檔。清單5顯示了利用 Lucene 的 API 進行返回搜索的過程。


          清單5:測試范圍搜索
          //Test range search
          public void testRangeSearch(String indexDirectory)throws Exception{
              Directory dir = FSDirectory.getDirectory(indexDirectory,false);
              IndexSearcher indexSearcher = new IndexSearcher(dir);
              Term begin = new Term("birthDay","20000101");
              Term end   = new Term("birthDay","20060606");
              Query query = new RangeQuery(begin,end,true);
              Hits results = indexSearcher.search(query);
              System.out.println(results.length() + "search results is returned");
          }
          





          回頁首


          在 Web 應用程序中集成 Lucene

          接下來我們開發一個 Web 應用程序利用 Lucene 來檢索存放在文件服務器上的 HTML 文檔。在開始之前,需要準備如下環境:

          1. Eclipse 集成開發環境
          2. Tomcat 5.0
          3. Lucene Library
          4. JDK 1.5

          這個例子使用 Eclipse 進行 Web 應用程序的開發,最終這個 Web 應用程序跑在 Tomcat 5.0 上面。在準備好開發所必需的環境之后,我們接下來進行 Web 應用程序的開發。

          1、創建一個動態 Web 項目

          1. 在 Eclipse 里面,選擇 File > New > Project,然后再彈出的窗口中選擇動態 Web 項目,如圖二所示。

          圖二:創建動態Web項目
          創建動態Web項目
          1. 在創建好動態 Web 項目之后,你會看到創建好的項目的結構,如圖三所示,項目的名稱為 sample.dw.paper.lucene。

          圖三:動態 Web 項目的結構
          動態 Web 項目的結構

          2. 設計 Web 項目的架構

          在我們的設計中,把該系統分成如下四個子系統:

          1. 用戶接口: 這個子系統提供用戶界面使用戶可以向 Web 應用程序服務器提交搜索請求,然后搜索結果通過用戶接口來顯示出來。我們用一個名為 search.jsp 的頁面來實現該子系統。
          2. 請求管理器: 這個子系統管理從客戶端發送過來的搜索請求并把搜索請求分發到搜索子系統中。最后搜索結果從搜索子系統返回并最終發送到用戶接口子系統。我們使用一個 Servlet 來實現這個子系統。
          3. 搜索子系統: 這個子系統負責在索引文件上進行搜索并把搜索結構傳遞給請求管理器。我們使用 Lucene 提供的 API 來實現該子系統。
          4. 索引子系統: 這個子系統用來為 HTML 頁面來創建索引。我們使用 Lucene 的 API 以及 Lucene 提供的一個 HTML 解析器來創建該子系統。

          圖4 顯示了我們設計的詳細信息,我們將用戶接口子系統放到 webContent 目錄下面。你會看到一個名為 search.jsp 的頁面在這個文件夾里面。請求管理子系統在包 sample.dw.paper.lucene.servlet 下面,類 SearchController 負責功能的實現。搜索子系統放在包 sample.dw.paper.lucene.search 當中,它包含了兩個類,SearchManagerSearchResultBean,第一個類用來實現搜索功能,第二個類用來描述搜索結果的結構。索引子系統放在包 sample.dw.paper.lucene.index 當中。類 IndexManager 負責為 HTML 文件創建索引。該子系統利用包 sample.dw.paper.lucene.util 里面的類 HTMLDocParser 提供的方法 getTitlegetContent 來對 HTML 頁面進行解析。


          圖四:項目的架構設計
          項目的架構設計

          3. 子系統的實現

          在分析了系統的架構設計之后,我們接下來看系統實現的詳細信息。

          1. 用戶接口: 這個子系統有一個名為 search.jsp 的 JSP 文件來實現,這個 JSP 頁面包含兩個部分。第一部分提供了一個用戶接口去向 Web 應用程序服務器提交搜索請求,如圖5所示。注意到這里的搜索請求發送到了一個名為 SearchController 的 Servlet 上面。Servlet 的名字和具體實現的類的對應關系在 web.xml 里面指定。

          圖5:向Web服務器提交搜索請求
          向Web服務器提交搜索請求

          這個JSP的第二部分負責顯示搜索結果給用戶,如圖6所示:


          圖6:顯示搜索結果
          顯示搜索結果
          1. 請求管理器: 一個名為 SearchController 的 servlet 用來實現該子系統。清單6給出了這個類的源代碼。

          清單6:請求管理器的實現
          package sample.dw.paper.lucene.servlet;
          
          import java.io.IOException;
          import java.util.List;
          
          import javax.servlet.RequestDispatcher;
          import javax.servlet.ServletException;
          import javax.servlet.http.HttpServlet;
          import javax.servlet.http.HttpServletRequest;
          import javax.servlet.http.HttpServletResponse;
          
          import sample.dw.paper.lucene.search.SearchManager;
          
          /**
           * This servlet is used to deal with the search request
           * and return the search results to the client
           */
          public class SearchController extends HttpServlet{
          
              private static final long serialVersionUID = 1L;
          
              public void doPost(HttpServletRequest request, HttpServletResponse response)
                                throws IOException, ServletException{
                  String searchWord = request.getParameter("searchWord");
                  SearchManager searchManager = new SearchManager(searchWord);
                  List searchResult = null;
                  searchResult = searchManager.search();
                  RequestDispatcher dispatcher = request.getRequestDispatcher("search.jsp");
                  request.setAttribute("searchResult",searchResult);
                  dispatcher.forward(request, response);
              }
          
              public void doGet(HttpServletRequest request, HttpServletResponse response)
                               throws IOException, ServletException{
                  doPost(request, response);
              }
          }
          

          清單6中,doPost 方法從客戶端獲取搜索詞并創建類 SearchManager 的一個實例,其中類 SearchManager 在搜索子系統中進行了定義。然后,SearchManager 的方法 search 會被調用。最后搜索結果被返回到客戶端。

          1. 搜索子系統: 在這個子系統中,我們定義了兩個類:SearchManagerSearchResultBean。第一個類用來實現搜索功能,第二個類是個JavaBean,用來描述搜索結果的結構。清單7給出了類 SearchManager 的源代碼。

          清單7:搜索功能的實現
          package sample.dw.paper.lucene.search;
          
          import java.io.IOException;
          import java.util.ArrayList;
          import java.util.List;
          
          import org.apache.lucene.analysis.Analyzer;
          import org.apache.lucene.analysis.standard.StandardAnalyzer;
          import org.apache.lucene.queryParser.ParseException;
          import org.apache.lucene.queryParser.QueryParser;
          import org.apache.lucene.search.Hits;
          import org.apache.lucene.search.IndexSearcher;
          import org.apache.lucene.search.Query;
          
          import sample.dw.paper.lucene.index.IndexManager;
          
          /**
           * This class is used to search the 
           * Lucene index and return search results
           */
          public class SearchManager {
          	
              private String searchWord;
              
              private IndexManager indexManager;
              
              private Analyzer analyzer;
              
              public SearchManager(String searchWord){
                  this.searchWord   =  searchWord;
                  this.indexManager =  new IndexManager();
                  this.analyzer     =  new StandardAnalyzer();
              }
              
              /**
               * do search
               */
              public List search(){
                  List searchResult = new ArrayList();
                  if(false == indexManager.ifIndexExist()){
                  try {
                      if(false == indexManager.createIndex()){
                          return searchResult;
                      }
                  } catch (IOException e) {
                    e.printStackTrace();
                    return searchResult;
                  }
                  }
              	
                  IndexSearcher indexSearcher = null;
          
                  try{
                      indexSearcher = new IndexSearcher(indexManager.getIndexDir());
                  }catch(IOException ioe){
                      ioe.printStackTrace();
                  }
          
                  QueryParser queryParser = new QueryParser("content",analyzer);
                  Query query = null;
                  try {
                      query = queryParser.parse(searchWord);
                  } catch (ParseException e) {
                    e.printStackTrace();
                  }
                  if(null != query >> null != indexSearcher){			
                      try {
                          Hits hits = indexSearcher.search(query);
                          for(int i = 0; i < hits.length(); i ++){
                              SearchResultBean resultBean = new SearchResultBean();
                              resultBean.setHtmlPath(hits.doc(i).get("path"));
                              resultBean.setHtmlTitle(hits.doc(i).get("title"));
                              searchResult.add(resultBean);
                          }
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
                  return searchResult;
              }
          }
           

          清單7中,注意到在這個類里面有三個私有屬性。第一個是 searchWord,代表了來自客戶端的搜索詞。第二個是 indexManager,代表了在索引子系統中定義的類 IndexManager 的一個實例。第三個是 analyzer,代表了用來解析搜索詞的解析器。現在我們把注意力放在方法 search 上面。這個方法首先檢查索引文件是否已經存在,如果已經存在,那么就在已經存在的索引上進行檢索,如果不存在,那么首先調用類 IndexManager 提供的方法來創建索引,然后在新創建的索引上進行檢索。搜索結果返回后,這個方法從搜索結果中提取出需要的屬性并為每個搜索結果生成類 SearchResultBean 的一個實例。最后這些 SearchResultBean 的實例被放到一個列表里面并返回給請求管理器。

          在類 SearchResultBean 中,含有兩個屬性,分別是 htmlPathhtmlTitle,以及這個兩個屬性的 get 和 set 方法。這也意味著我們的搜索結果包含兩個屬性:htmlPathhtmlTitle,其中 htmlPath 代表了 HTML 文件的路徑,htmlTitle 代表了 HTML 文件的標題。

          1. 索引子系統: 類 IndexManager 用來實現這個子系統。清單8 給出了這個類的源代碼。

          清單8:索引子系統的實現
          package sample.dw.paper.lucene.index;
          
          import java.io.File;
          import java.io.IOException;
          import java.io.Reader;
          
          import org.apache.lucene.analysis.Analyzer;
          import org.apache.lucene.analysis.standard.StandardAnalyzer;
          import org.apache.lucene.document.Document;
          import org.apache.lucene.document.Field;
          import org.apache.lucene.index.IndexWriter;
          import org.apache.lucene.store.Directory;
          import org.apache.lucene.store.FSDirectory;
          
          import sample.dw.paper.lucene.util.HTMLDocParser;
          
          /**
           * This class is used to create an index for HTML files
           *
           */
          public class IndexManager {
          
              //the directory that stores HTML files 
              private final String dataDir  = "c:\\dataDir";
          
              //the directory that is used to store a Lucene index
              private final String indexDir = "c:\\indexDir";
          
              /**
               * create index
               */
              public boolean createIndex() throws IOException{
                  if(true == ifIndexExist()){
                      return true;	
                  }
                  File dir = new File(dataDir);
                  if(!dir.exists()){
                      return false;
                  }
                  File[] htmls = dir.listFiles();
                  Directory fsDirectory = FSDirectory.getDirectory(indexDir, true);
                  Analyzer  analyzer    = new StandardAnalyzer();
                  IndexWriter indexWriter = new IndexWriter(fsDirectory, analyzer, true);
                  for(int i = 0; i < htmls.length; i++){
                      String htmlPath = htmls[i].getAbsolutePath();
          
                      if(htmlPath.endsWith(".html") || htmlPath.endsWith(".htm")){
                  		addDocument(htmlPath, indexWriter);
                  	}
                  }
                  indexWriter.optimize();
                  indexWriter.close();
                  return true;
          
              }
          
              /**
               * Add one document to the Lucene index
               */
              public void addDocument(String htmlPath, IndexWriter indexWriter){
                  HTMLDocParser htmlParser = new HTMLDocParser(htmlPath);
                  String path    = htmlParser.getPath();
                  String title   = htmlParser.getTitle();
                  Reader content = htmlParser.getContent();
          
                  Document document = new Document();
                  document.add(new Field("path",path,Field.Store.YES,Field.Index.NO));
                  document.add(new Field("title",title,Field.Store.YES,Field.Index.TOKENIZED));
                  document.add(new Field("content",content));
                  try {
                        indexWriter.addDocument(document);
              } catch (IOException e) {
                        e.printStackTrace();
                    }
              }
          
              /**
               * judge if the index exists already
               */
              public boolean ifIndexExist(){
                  File directory = new File(indexDir);
                  if(0 < directory.listFiles().length){
                      return true;
                  }else{
                      return false;
                  }
              }
          
              public String getDataDir(){
                  return this.dataDir;
              }
          
              public String getIndexDir(){
                  return this.indexDir;
              }
          
          }
          

          這個類包含兩個私有屬性,分別是 dataDirindexDirdataDir 代表存放等待進行索引的 HTML 頁面的路徑,indexDir 代表了存放 Lucene 索引文件的路徑。類 IndexManager 提供了三個方法,分別是 createIndex, addDocumentifIndexExist。如果索引不存在的話,你可以使用方法 createIndex 去創建一個新的索引,用方法 addDocument 去向一個索引上添加文檔。在我們的場景中,一個文檔就是一個 HTML 頁面。方法 addDocument 會調用由類 HTMLDocParser 提供的方法對 HTML 文檔進行解析。你可以使用最后一個方法 ifIndexExist 來判斷 Lucene 的索引是否已經存在。

          現在我們來看一下放在包 sample.dw.paper.lucene.util 里面的類 HTMLDocParser。這個類用來從 HTML 文件中提取出文本信息。這個類包含三個方法,分別是 getContentgetTitlegetPath。第一個方法返回去除了 HTML 標記的文本內容,第二個方法返回 HTML 文件的標題,最后一個方法返回 HTML 文件的路徑。清單9 給出了這個類的源代碼。


          清單9:HTML 解析器
          package sample.dw.paper.lucene.util;
          
          import java.io.FileInputStream;
          import java.io.FileNotFoundException;
          import java.io.IOException;
          import java.io.InputStream;
          import java.io.InputStreamReader;
          import java.io.Reader;
          import java.io.UnsupportedEncodingException;
          
          import org.apache.lucene.demo.html.HTMLParser;
          
          public class HTMLDocParser {
              private String htmlPath;
          
              private HTMLParser htmlParser;
          
              public HTMLDocParser(String htmlPath){
                  this.htmlPath = htmlPath;
                  initHtmlParser();
              }
          
              private void initHtmlParser(){
                  InputStream inputStream = null;
                  try {
                      inputStream = new FileInputStream(htmlPath);
                  } catch (FileNotFoundException e) {
                      e.printStackTrace();
                  }
                  if(null != inputStream){
          	        try {
                          htmlParser = new HTMLParser(new InputStreamReader(inputStream, "utf-8"));
                      } catch (UnsupportedEncodingException e) {
                          e.printStackTrace();
                      }
                  }
              }
          
              public String getTitle(){
                  if(null != htmlParser){
                      try {
                          return htmlParser.getTitle();
                      } catch (IOException e) {
                          e.printStackTrace();
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              return "";
              }
          
              public Reader getContent(){
              if(null != htmlParser){
                      try {
                            return htmlParser.getReader();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                  }
                  return null;
              }
          
              public String getPath(){
                  return this.htmlPath;		
              }
          }
          

          5.在 Tomcat 5.0 上運行應用程序

          現在我們可以在 Tomcat 5.0 上運行開發好的應用程序。

          1. 右鍵單擊 search.jsp,然后選擇 Run as > Run on Server,如圖7所示。

          圖7:配置 Tomcat 5.0
          配置 Tomcat 5.0
          1. 在彈出的窗口中,選擇 Tomcat v5.0 Server 作為目標 Web 應用程序服務器,然后點擊 Next,如圖8 所示:

          圖8:選擇 Tomcat 5.0
          選擇 Tomcat 5.0
          1. 現在需要指定用來運行 Web 應用程序的 Apache Tomcat 5.0 以及 JRE 的路徑。這里你所選擇的 JRE 的版本必須和你用來編譯 Java 文件的 JRE 的版本一致。配置好之后,點擊 Finish。如 圖9 所示。

          圖9:完成Tomcat 5.0的配置
          完成Tomcat 5.0的配置
          1. 配置好之后,Tomcat 會自動運行,并且會對 search.jsp 進行編譯并顯示給用戶。如 圖10 所示。

          圖10:用戶界面
          用戶界面
          1. 在輸入框中輸入關鍵詞 “information” 然后單擊 Search 按鈕。然后這個頁面上會顯示出搜索結果來,如 圖11 所示。

          圖11:搜索結果
          搜索結果
          1. 單擊搜索結果的第一個鏈接,頁面上就會顯示出所鏈接到的頁面的內容。如 圖12 所示.

          圖12:詳細信息
          詳細信息

          現在我們已經成功的完成了示例項目的開發,并成功的用Lucene實現了搜索和索引功能。你可以下載這個項目的源代碼(下載)。





          回頁首


          總結

          Lucene 提供了靈活的接口使我們更加方便的設計我們的 Web 搜索應用程序。如果你想在你的應用程序中加入搜索功能,那么 Lucene 是一個很好的選擇。在設計你的下一個帶有搜索功能的應用程序的時候可以考慮使用 Lucene 來提供搜索功能。

          posted @ 2006-10-17 13:05 點滴鑄就輝煌 閱讀(271) | 評論 (0)編輯 收藏
           
          使用Windows操作系統的朋友對Excel(電子表格)一定不會陌生,但是要使用Java語言來操縱Excel文件并不是一件容易的事。在Web應用日益盛行的今天,通過Web來操作Excel文件的需求越來越強烈,目前較為流行的操作是在JSP或Servlet 中創建一個CSV (comma separated values)文件,并將這個文件以MIME,text/csv類型返回給瀏覽器,接著瀏覽器調用Excel并且顯示CSV文件。這樣只是說可以訪問到Excel文件,但是還不能真正的操縱Excel文件,本文將給大家一個驚喜,向大家介紹一個開放源碼項目,Java Excel API,使用它大家就可以方便地操縱Excel文件了。
          JAVA EXCEL API簡介

          Java Excel是一開放源碼項目,通過它Java開發人員可以讀取Excel文件的內容、創建新的Excel文件、更新已經存在的Excel文件。使用該API非Windows操作系統也可以通過純Java應用來處理Excel數據表。因為是使用Java編寫的,所以我們在Web應用中可以通過JSP、Servlet來調用API實現對Excel數據表的訪問。

          現在發布的穩定版本是V2.0,提供以下功能:

          從Excel 95、97、2000等格式的文件中讀取數據;
          讀取Excel公式(可以讀取Excel 97以后的公式);
          生成Excel數據表(格式為Excel 97);
          支持字體、數字、日期的格式化;
          支持單元格的陰影操作,以及顏色操作;
          修改已經存在的數據表;
          現在還不支持以下功能,但不久就會提供了:

          不能夠讀取圖表信息;
          可以讀,但是不能生成公式,任何類型公式最后的計算值都可以讀出;
          應用示例

          1 從Excel文件讀取數據表

          Java Excel API既可以從本地文件系統的一個文件(.xls),也可以從輸入流中讀取Excel數據表。讀取Excel數據表的第一步是創建Workbook(術語:工作薄),下面的代碼片段舉例說明了應該如何操作:(完整代碼見ExcelReading.java)


          import java.io.*;
          import jxl.*;
          … … … …
          try
          {
          //構建Workbook對象, 只讀Workbook對象
          //直接從本地文件創建Workbook
          //從輸入流創建Workbook
          ?? InputStream is = new FileInputStream(sourcefile);
          ?? jxl.Workbook rwb = Workbook.getWorkbook(is);
          }
          catch (Exception e)
          {
          e.printStackTrace();
          }



          一旦創建了Workbook,我們就可以通過它來訪問Excel Sheet(術語:工作表)。參考下面的代碼片段:


          //獲取第一張Sheet表
          Sheet rs = rwb.getSheet(0);



          我們既可能通過Sheet的名稱來訪問它,也可以通過下標來訪問它。如果通過下標來訪問的話,要注意的一點是下標從0開始,就像數組一樣。

          一旦得到了Sheet,我們就可以通過它來訪問Excel Cell(術語:單元格)。參考下面的代碼片段:


          //獲取第一行,第一列的值
          Cell c00 = rs.getCell(0, 0);
          String strc00 = c00.getContents();

          //獲取第一行,第二列的值
          Cell c10 = rs.getCell(1, 0);
          String strc10 = c10.getContents();

          //獲取第二行,第二列的值
          Cell c11 = rs.getCell(1, 1);
          String strc11 = c11.getContents();

          System.out.println("Cell(0, 0)" + " value : " + strc00 + "; type : " + c00.getType());
          System.out.println("Cell(1, 0)" + " value : " + strc10 + "; type : " + c10.getType());
          System.out.println("Cell(1, 1)" + " value : " + strc11 + "; type : " + c11.getType());



          如果僅僅是取得Cell的值,我們可以方便地通過getContents()方法,它可以將任何類型的Cell值都作為一個字符串返回。示例代碼中Cell(0, 0)是文本型,Cell(1, 0)是數字型,Cell(1,1)是日期型,通過getContents(),三種類型的返回值都是字符型。

          如果有需要知道Cell內容的確切類型,API也提供了一系列的方法。參考下面的代碼片段:


          String strc00 = null;
          double strc10 = 0.00;
          Date strc11 = null;

          Cell c00 = rs.getCell(0, 0);
          Cell c10 = rs.getCell(1, 0);
          Cell c11 = rs.getCell(1, 1);

          if(c00.getType() == CellType.LABEL)
          {
          LabelCell labelc00 = (LabelCell)c00;
          strc00 = labelc00.getString();
          }
          if(c10.getType() == CellType.NUMBER)
          {
          NmberCell numc10 = (NumberCell)c10;
          strc10 = numc10.getValue();
          }
          if(c11.getType() == CellType.DATE)
          {
          DateCell datec11 = (DateCell)c11;
          strc11 = datec11.getDate();
          }

          System.out.println("Cell(0, 0)" + " value : " + strc00 + "; type : " + c00.getType());
          System.out.println("Cell(1, 0)" + " value : " + strc10 + "; type : " + c10.getType());
          System.out.println("Cell(1, 1)" + " value : " + strc11 + "; type : " + c11.getType());



          在得到Cell對象后,通過getType()方法可以獲得該單元格的類型,然后與API提供的基本類型相匹配,強制轉換成相應的類型,最后調用相應的取值方法getXXX(),就可以得到確定類型的值。API提供了以下基本類型,與Excel的數據格式相對應,如下圖所示:





          每種類型的具體意義,請參見Java Excel API Document。

          當你完成對Excel電子表格數據的處理后,一定要使用close()方法來關閉先前創建的對象,以釋放讀取數據表的過程中所占用的內存空間,在讀取大量數據時顯得尤為重要。參考如下代碼片段:


          //操作完成時,關閉對象,釋放占用的內存空間
          rwb.close();



          Java Excel API提供了許多訪問Excel數據表的方法,在這里我只簡要地介紹幾個常用的方法,其它的方法請參考附錄中的Java Excel API Document。

          Workbook類提供的方法

          1. int getNumberOfSheets()
          獲得工作薄(Workbook)中工作表(Sheet)的個數,示例:


          jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
          int sheets = rwb.getNumberOfSheets();



          2. Sheet[] getSheets()
          返回工作薄(Workbook)中工作表(Sheet)對象數組,示例:


          jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
          Sheet[] sheets = rwb.getSheets();



          3. String getVersion()
          返回正在使用的API的版本號,好像是沒什么太大的作用。


          jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
          String apiVersion = rwb.getVersion();



          Sheet接口提供的方法

          1) String getName()
          獲取Sheet的名稱,示例:


          jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
          jxl.Sheet rs = rwb.getSheet(0);
          String sheetName = rs.getName();



          2) int getColumns()
          獲取Sheet表中所包含的總列數,示例:


          jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
          jxl.Sheet rs = rwb.getSheet(0);
          int rsColumns = rs.getColumns();



          3) Cell[] getColumn(int column)
          獲取某一列的所有單元格,返回的是單元格對象數組,示例:


          jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
          jxl.Sheet rs = rwb.getSheet(0);
          Cell[] cell = rs.getColumn(0);



          4) int getRows()
          獲取Sheet表中所包含的總行數,示例:


          jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
          jxl.Sheet rs = rwb.getSheet(0);
          int rsRows = rs.getRows();



          5) Cell[] getRow(int row)
          獲取某一行的所有單元格,返回的是單元格對象數組,示例子:


          jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
          jxl.Sheet rs = rwb.getSheet(0);
          Cell[] cell = rs.getRow(0);



          6) Cell getCell(int column, int row)
          獲取指定單元格的對象引用,需要注意的是它的兩個參數,第一個是列數,第二個是行數,這與通常的行、列組合有些不同。


          jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
          jxl.Sheet rs = rwb.getSheet(0);
          Cell cell = rs.getCell(0, 0);



          2 生成新的Excel工作薄

          下面的代碼主要是向大家介紹如何生成簡單的Excel工作表,在這里單元格的內容是不帶任何修飾的(如:字體,顏色等等),所有的內容都作為字符串寫入。(完整代碼見ExcelWriting.java)

          與讀取Excel工作表相似,首先要使用Workbook類的工廠方法創建一個可寫入的工作薄(Workbook)對象,這里要注意的是,只能通過API提供的工廠方法來創建Workbook,而不能使用WritableWorkbook的構造函數,因為類WritableWorkbook的構造函數為protected類型。示例代碼片段如下:


          import java.io.*;
          import jxl.*;
          import jxl.write.*;
          … … … …
          try
          {
          //構建Workbook對象, 只讀Workbook對象
          //Method 1:創建可寫入的Excel工作薄
          ?? jxl.write.WritableWorkbook wwb = Workbook.createWorkbook(new File(targetfile));

          //Method 2:將WritableWorkbook直接寫入到輸出流
          /*
          ?? OutputStream os = new FileOutputStream(targetfile);
          ?? jxl.write.WritableWorkbook wwb = Workbook.createWorkbook(os);
          */
          }
          catch (Exception e)
          {
          e.printStackTrace();
          }



          API提供了兩種方式來處理可寫入的輸出流,一種是直接生成本地文件,如果文件名不帶全路徑的話,缺省的文件會定位在當前目錄,如果文件名帶有全路徑的話,則生成的Excel文件則會定位在相應的目錄;另外一種是將Excel對象直接寫入到輸出流,例如:用戶通過瀏覽器來訪問Web服務器,如果HTTP頭設置正確的話,瀏覽器自動調用客戶端的Excel應用程序,來顯示動態生成的Excel電子表格。

          接下來就是要創建工作表,創建工作表的方法與創建工作薄的方法幾乎一樣,同樣是通過工廠模式方法獲得相應的對象,該方法需要兩個參數,一個是工作表的名稱,另一個是工作表在工作薄中的位置,參考下面的代碼片段:


          //創建Excel工作表
          jxl.write.WritableSheet ws = wwb.createSheet("Test Sheet 1", 0);



          "這鍋也支好了,材料也準備齊全了,可以開始下鍋了!",現在要做的只是實例化API所提供的Excel基本數據類型,并將它們添加到工作表中就可以了,參考下面的代碼片段:


          //1.添加Label對象
          jxl.write.Label labelC = new jxl.write.Label(0, 0, "This is a Label cell");
          ws.addCell(labelC);

          //添加帶有字型Formatting的對象
          jxl.write.WritableFont wf = new jxl.write.WritableFont(WritableFont.TIMES, 18, WritableFont.BOLD, true);
          jxl.write.WritableCellFormat wcfF = new jxl.write.WritableCellFormat(wf);
          jxl.write.Label labelCF = new jxl.write.Label(1, 0, "This is a Label Cell", wcfF);
          ws.addCell(labelCF);

          //添加帶有字體顏色Formatting的對象
          jxl.write.WritableFont wfc = new jxl.write.WritableFont(WritableFont.ARIAL, 10, WritableFont.NO_BOLD, false,
          Underlinestyle.NO_UNDERLINE, jxl.format.Colour.RED);
          jxl.write.WritableCellFormat wcfFC = new jxl.write.WritableCellFormat(wfc);
          jxl.write.Label labelCFC = new jxl.write.Label(1, 0, "This is a Label Cell", wcfFC);
          ws.addCell(labelCF);

          //2.添加Number對象
          jxl.write.Number labelN = new jxl.write.Number(0, 1, 3.1415926);
          ws.addCell(labelN);

          //添加帶有formatting的Number對象
          jxl.write.NumberFormat nf = new jxl.write.NumberFormat("#.##");
          jxl.write.WritableCellFormat wcfN = new jxl.write.WritableCellFormat(nf);
          jxl.write.Number labelNF = new jxl.write.Number(1, 1, 3.1415926, wcfN);
          ws.addCell(labelNF);

          //3.添加Boolean對象
          jxl.write.Boolean labelB = new jxl.write.Boolean(0, 2, false);
          ws.addCell(labelB);

          //4.添加DateTime對象
          jxl.write.DateTime labelDT = new jxl.write.DateTime(0, 3, new java.util.Date());
          ws.addCell(labelDT);

          //添加帶有formatting的DateFormat對象
          jxl.write.DateFormat df = new jxl.write.DateFormat("dd MM yyyy hh:mm:ss");
          jxl.write.WritableCellFormat wcfDF = new jxl.write.WritableCellFormat(df);
          jxl.write.DateTime labelDTF = new jxl.write.DateTime(1, 3, new java.util.Date(), wcfDF);
          ws.addCell(labelDTF);



          這里有兩點大家要引起大家的注意。第一點,在構造單元格時,單元格在工作表中的位置就已經確定了。一旦創建后,單元格的位置是不能夠變更的,盡管單元格的內容是可以改變的。第二點,單元格的定位是按照下面這樣的規律(column, row),而且下標都是從0開始,例如,A1被存儲在(0, 0),B1被存儲在(1, 0)。

          最后,不要忘記關閉打開的Excel工作薄對象,以釋放占用的內存,參見下面的代碼片段:


          //寫入Exel工作表
          wwb.write();

          //關閉Excel工作薄對象
          wwb.close();



          這可能與讀取Excel文件的操作有少少不同,在關閉Excel對象之前,你必須要先調用write()方法,因為先前的操作都是存儲在緩存中的,所以要通過該方法將操作的內容保存在文件中。如果你先關閉了Excel對象,那么只能得到一張空的工作薄了。

          3 拷貝、更新Excel工作薄

          接下來簡要介紹一下如何更新一個已經存在的工作薄,主要是下面二步操作,第一步是構造只讀的Excel工作薄,第二步是利用已經創建的Excel工作薄創建新的可寫入的Excel工作薄,參考下面的代碼片段:(完整代碼見ExcelModifying.java)


          //創建只讀的Excel工作薄的對象
          jxl.Workbook rw = jxl.Workbook.getWorkbook(new File(sourcefile));

          //創建可寫入的Excel工作薄對象
          jxl.write.WritableWorkbook??wwb = Workbook.createWorkbook(new File(targetfile), rw);
          ??????????
          //讀取第一張工作表
          jxl.write.WritableSheet ws = wwb.getSheet(0);

          //獲得第一個單元格對象
          jxl.write.WritableCell wc = ws.getWritableCell(0, 0);
          ??????????
          //判斷單元格的類型, 做出相應的轉化
          if(wc.getType() == CellType.LABEL)
          {
          Label l = (Label)wc;
          ?? l.setString("The value has been modified.");
          }

          //寫入Excel對象
          wwb.write();

          //關閉可寫入的Excel對象
          wwb.close();

          //關閉只讀的Excel對象
          rw.close();



          之所以使用這種方式構建Excel對象,完全是因為效率的原因,因為上面的示例才是API的主要應用。為了提高性能,在讀取工作表時,與數據相關的一些輸出信息,所有的格式信息,如:字體、顏色等等,是不被處理的,因為我們的目的是獲得行數據的值,既使沒有了修飾,也不會對行數據的值產生什么影響。唯一的不利之處就是,在內存中會同時保存兩個同樣的工作表,這樣當工作表體積比較大時,會占用相當大的內存,但現在好像內存的大小并不是什么關鍵因素了。

          一旦獲得了可寫入的工作表對象,我們就可以對單元格對象進行更新的操作了,在這里我們不必調用API提供的add()方法,因為單元格已經于工作表當中,所以我們只需要調用相應的setXXX()方法,就可以完成更新的操作了。

          盡單元格原有的格式化修飾是不能去掉的,我們還是可以將新的單元格修飾加上去,以使單元格的內容以不同的形式表現。

          新生成的工作表對象是可寫入的,我們除了更新原有的單元格外,還可以添加新的單元格到工作表中,這與示例2的操作是完全一樣的。

          最后,不要忘記調用write()方法,將更新的內容寫入到文件中,然后關閉工作薄對象,這里有兩個工作薄對象要關閉,一個是只讀的,另外一個是可寫入的。

          以上摘自IBM網站
          posted @ 2006-08-29 17:32 點滴鑄就輝煌 閱讀(261) | 評論 (0)編輯 收藏
           

          [原創文章,轉載請保留或注明出處:http://www.regexlab.com/zh/encoding.htm]

          級別:中級

          摘要:本文介紹了字符與編碼的發展過程,相關概念的正確理解。舉例說明了一些實際應用中,編碼的實現方法。然后,本文講述了通常對字符與編碼的幾種誤解,由于這些誤解而導致亂碼產生的原因,以及消除亂碼的辦法。本文的內容涵蓋了“中文問題”,“亂碼問題”。

          掌握編碼問題的關鍵是正確地理解相關概念,編碼所涉及的技術其實是很簡單的。因此,閱讀本文時需要慢讀多想,多思考。

          引言

          “字符與編碼”是一個被經常討論的話題。即使這樣,時常出現的亂碼仍然困擾著大家。雖然我們有很多的辦法可以用來消除亂碼,但我們并不一定理解這些辦法的內在原理。而有的亂碼產生的原因,實際上由于底層代碼本身有問題所導致的。因此,不僅是初學者會對字符編碼感到模糊,有的底層開發人員同樣對字符編碼缺乏準確的理解。

          1. 編碼問題的由來,相關概念的理解

          1.1 字符與編碼的發展

          從計算機對多國語言的支持角度看,大致可以分為三個階段:

            系統內碼 說明 系統
          階段一 ASCII 計算機剛開始只支持英語,其它語言不能夠在計算機上存儲和顯示。 英文 DOS
          階段二 ANSI編碼
          (本地化)
          為使計算機支持更多語言,通常使用 0x80~0xFF 范圍的 2 個字節來表示 1 個字符。比如:漢字 '中' 在中文操作系統中,使用 [0xD6,0xD0] 這兩個字節存儲。

          不同的國家和地區制定了不同的標準,由此產生了 GB2312, BIG5, JIS 等各自的編碼標準。這些使用 2 個字節來代表一個字符的各種漢字延伸編碼方式,稱為 ANSI 編碼。在簡體中文系統下,ANSI 編碼代表 GB2312 編碼,在日文操作系統下,ANSI 編碼代表 JIS 編碼。

          不同 ANSI 編碼之間互不兼容,當信息在國際間交流時,無法將屬于兩種語言的文字,存儲在同一段 ANSI 編碼的文本中。
          中文 DOS,中文 Windows 95/98,日文 Windows 95/98
          階段三 UNICODE
          (國際化)
          為了使國際間信息交流更加方便,國際組織制定了 UNICODE 字符集,為各種語言中的每一個字符設定了統一并且唯一的數字編號,以滿足跨語言、跨平臺進行文本轉換、處理的要求。 Windows NT/2000/XP,Linux,Java

          字符串在內存中的存放方法:

          在 ASCII 階段,單字節字符串使用一個字節存放一個字符(SBCS)。比如,"Bob123" 在內存中為:

          42 6F 62 31 32 33 00
          B o b 1 2 3 \0

          在使用 ANSI 編碼支持多種語言階段,每個字符使用一個字節或多個字節來表示(MBCS),因此,這種方式存放的字符也被稱作多字節字符。比如,"中文123" 在中文 Windows 95 內存中為7個字節,每個漢字占2個字節,每個英文和數字字符占1個字節:

          D6 D0 CE C4 31 32 33 00
          1 2 3 \0

          在 UNICODE 被采用之后,計算機存放字符串時,改為存放每個字符在 UNICODE 字符集中的序號。目前計算機一般使用 2 個字節(16 位)來存放一個序號(DBCS),因此,這種方式存放的字符也被稱作寬字節字符。比如,字符串 "中文123" 在 Windows 2000 下,內存中實際存放的是 5 個序號:

          2D 4E 87 65 31 00 32 00 33 00 00 00 ???? ← 在 x86 CPU 中,低字節在前
          1 2 3 \0  

          一共占 10 個字節。

          1.2 字符,字節,字符串

          理解編碼的關鍵,是要把字符的概念和字節的概念理解準確。這兩個概念容易混淆,我們在此做一下區分:

            概念描述 舉例
          字符 人們使用的記號,抽象意義上的一個符號。 '1', '中', 'a', '$', '¥', ……
          字節 計算機中存儲數據的單元,一個8位的二進制數,是一個很具體的存儲空間。 0x01, 0x45, 0xFA, ……
          ANSI
          字符串
          在內存中,如果“字符”是以 ANSI 編碼形式存在的,一個字符可能使用一個字節或多個字節來表示,那么我們稱這種字符串為 ANSI 字符串或者多字節字符串 "中文123"
          (占7字節)
          UNICODE
          字符串
          在內存中,如果“字符”是以在 UNICODE 中的序號存在的,那么我們稱這種字符串為 UNICODE 字符串或者寬字節字符串 L"中文123"
          (占10字節)

          由于不同 ANSI 編碼所規定的標準是不相同的,因此,對于一個給定的多字節字符串,我們必須知道它采用的是哪一種編碼規則,才能夠知道它包含了哪些“字符”。而對于 UNICODE 字符串來說,不管在什么環境下,它所代表的“字符”內容總是不變的。

          回頁首

          1.3 字符集與編碼

          各個國家和地區所制定的不同 ANSI 編碼標準中,都只規定了各自語言所需的“字符”。比如:漢字標準(GB2312)中沒有規定韓國語字符怎樣存儲。這些 ANSI 編碼標準所規定的內容包含兩層含義:

          1. 使用哪些字符。也就是說哪些漢字,字母和符號會被收入標準中。所包含“字符”的集合就叫做“字符集”。
          2. 規定每個“字符”分別用一個字節還是多個字節存儲,用哪些字節來存儲,這個規定就叫做“編碼”。

          各個國家和地區在制定編碼標準的時候,“字符的集合”和“編碼”一般都是同時制定的。因此,平常我們所說的“字符集”,比如:GB2312, GBK, JIS 等,除了有“字符的集合”這層含義外,同時也包含了“編碼”的含義。

          UNICODE 字符集”包含了各種語言中使用到的所有“字符”。用來給 UNICODE 字符集編碼的標準有很多種,比如:UTF-8, UTF-7, UTF-16, UnicodeLittle, UnicodeBig 等。

          回頁首

          1.4 常用的編碼簡介

          簡單介紹一下常用的編碼規則,為后邊的章節做一個準備。在這里,我們根據編碼規則的特點,把所有的編碼分成三類:

          分類 編碼標準 說明
          單字節字符編碼 ISO-8859-1 最簡單的編碼規則,每一個字節直接作為一個 UNICODE 字符。比如,[0xD6, 0xD0] 這兩個字節,通過 iso-8859-1 轉化為字符串時,將直接得到 [0x00D6, 0x00D0] 兩個 UNICODE 字符,即 "?D"。

          反之,將 UNICODE 字符串通過 iso-8859-1 轉化為字節串時,只能正常轉化 0~255 范圍的字符。
          ANSI 編碼 GB2312,
          BIG5,
          Shift_JIS,
          ISO-8859-2 ……
          把 UNICODE 字符串通過 ANSI 編碼轉化為“字節串”時,根據各自編碼的規定,一個 UNICODE 字符可能轉化成一個字節或多個字節。

          反之,將字節串轉化成字符串時,也可能多個字節轉化成一個字符。比如,[0xD6, 0xD0] 這兩個字節,通過 GB2312 轉化為字符串時,將得到 [0x4E2D] 一個字符,即 '中' 字。

          “ANSI 編碼”的特點:
          1. 這些“ANSI 編碼標準”都只能處理各自語言范圍之內的 UNICODE 字符。
          2. “UNICODE 字符”與“轉換出來的字節”之間的關系是人為規定的。
          UNICODE 編碼 UTF-8,
          UTF-16, UnicodeBig ……
          與“ANSI 編碼”類似的,把字符串通過 UNICODE 編碼轉化成“字節串”時,一個 UNICODE 字符可能轉化成一個字節或多個字節。

          與“ANSI 編碼”不同的是:
          1. 這些“UNICODE 編碼”能夠處理所有的 UNICODE 字符。
          2. “UNICODE 字符”與“轉換出來的字節”之間是可以通過計算得到的。

          我們實際上沒有必要去深究每一種編碼具體把某一個字符編碼成了哪幾個字節,我們只需要知道“編碼”的概念就是把“字符”轉化成“字節”就可以了。對于“UNICODE 編碼”,由于它們是可以通過計算得到的,因此,在特殊的場合,我們可以去了解某一種“UNICODE 編碼”是怎樣的規則。

          回頁首

          2. 字符與編碼在程序中的實現

          2.1 程序中的字符與字節

          在 C++ 和 Java 中,用來代表“字符”和“字節”的數據類型,以及進行編碼的方法:

          類型或操作 C++ Java
          字符 wchar_t char
          字節 char byte
          ANSI 字符串 char[] byte[]
          UNICODE 字符串 wchar_t[] String
          字節串→字符串 mbstowcs(), MultiByteToWideChar() string = new String(bytes, "encoding")
          字符串→字節串 wcstombs(), WideCharToMultiByte() bytes = string.getBytes("encoding")

          以上需要注意幾點:

          1. Java 中的 char 代表一個“UNICODE 字符(寬字節字符)”,而 C++ 中的 char 代表一個字節。
          2. MultiByteToWideChar() 和 WideCharToMultiByte() 是 Windows API 函數。

          回頁首

          2.2 C++ 中相關實現方法

          聲明一段字符串常量:

          // ANSI 字符串,內容長度 7 字節
          char
          ???? sz[20] = "中文123";

          // UNICODE 字符串,內容長度 5 個 wchar_t(10 字節)
          wchar_t wsz[20] = L"\x4E2D\x6587\x0031\x0032\x0033";

          UNICODE 字符串的 I/O 操作,字符與字節的轉換操作:

          // 運行時設定當前 ANSI 編碼,VC 格式
          setlocale(LC_ALL, ".936");

          // GCC 中格式
          setlocale(LC_ALL, "zh_CN.GBK");

          // Visual C++ 中使用小寫 %s,按照 setlocale 指定編碼輸出到文件
          // GCC 中使用大寫 %S

          fwprintf(fp, L"%s\n", wsz);

          // 把 UNICODE 字符串按照 setlocale 指定的編碼轉換成字節
          wcstombs(sz, wsz, 20);
          // 把字節串按照 setlocale 指定的編碼轉換成 UNICODE 字符串
          mbstowcs(wsz, sz, 20);

          在 Visual C++ 中,UNICODE 字符串常量有更簡單的表示方法。如果源程序的編碼與當前默認 ANSI 編碼不符,則需要使用 #pragma setlocale,告訴編譯器源程序使用的編碼:

          // 如果源程序的編碼與當前默認 ANSI 編碼不一致,
          // 則需要此行,編譯時用來指明當前源程序使用的編碼

          #pragma setlocale
          (".936")

          // UNICODE 字符串常量,內容長度 10 字節
          wchar_t wsz[20] = L"中文123";

          以上需要注意 #pragma setlocale 與 setlocale(LC_ALL, "") 的作用是不同的,#pragma setlocale 在編譯時起作用,setlocale() 在運行時起作用。

          回頁首

          2.3 Java 中相關實現方法

          字符串類 String 中的內容是 UNICODE 字符串:

          // Java 代碼,直接寫中文
          String
          string = "中文123";

          // 得到長度為 5,因為是 5 個字符
          System.out.println(string.length());

          字符串 I/O 操作,字符與字節轉換操作。在 Java 包 java.io.* 中,以“Stream”結尾的類一般是用來操作“字節串”的類,以“Reader”,“Writer”結尾的類一般是用來操作“字符串”的類。

          // 字符串與字節串間相互轉化

          // 按照 GB2312 得到字節(得到多字節字符串)

          byte
          [] bytes = string.getBytes("GB2312");

          // 從字節按照 GB2312 得到 UNICODE 字符串
          string = newString(bytes, "GB2312");

          // 要將 String 按照某種編碼寫入文本文件,有兩種方法:

          // 第一種辦法:用 Stream 類寫入已經按照指定編碼轉化好的字節串

          OutputStream os = new FileOutputStream("1.txt");
          os.write(bytes);
          os.close();

          // 第二種辦法:構造指定編碼的 Writer 來寫入字符串
          Writer ow = new OutputStreamWriter(new FileOutputStream("2.txt"), "GB2312");
          ow.write(string);
          ow.close();

          /* 最后得到的 1.txt 和 2.txt 都是 7 個字節 */

          如果 java 的源程序編碼與當前默認 ANSI 編碼不符,則在編譯的時候,需要指明一下源程序的編碼。比如:

          E:\>javac -encoding BIG5 Hello.java

          以上需要注意區分源程序的編碼與 I/O 操作的編碼,前者是在編譯時起作用,后者是在運行時起作用。

          回頁首

          3. 幾種誤解,以及亂碼產生的原因和解決辦法

          3.1 容易產生的誤解
            對編碼的誤解
          誤解一 在將“字節串”轉化成“UNICODE 字符串”時,比如在讀取文本文件時,或者通過網絡傳輸文本時,容易將“字節串”簡單地作為單字節字符串,采用每“一個字節”就是“一個字符”的方法進行轉化。

          而實際上,在非英文的環境中,應該將“字節串”作為 ANSI 字符串,采用適當的編碼來得到 UNICODE 字符串,有可能“多個字節”才能得到“一個字符”。

          通常,一直在英文環境下做開發的程序員們,容易有這種誤解。
          誤解二 在 DOS,Windows 98 等非 UNICODE 環境下,字符串都是以 ANSI 編碼的字節形式存在的。這種以字節形式存在的字符串,必須知道是哪種編碼才能被正確地使用。這使我們形成了一個慣性思維:“字符串的編碼”。

          當 UNICODE 被支持后,Java 中的 String 是以字符的“序號”來存儲的,不是以“某種編碼的字節”來存儲的,因此已經不存在“字符串的編碼”這個概念了。只有在“字符串”與“字節串”轉化時,或者,將一個“字節串”當成一個 ANSI 字符串時,才有編碼的概念。

          不少的人都有這個誤解。

          第一種誤解,往往是導致亂碼產生的原因。第二種誤解,往往導致本來容易糾正的亂碼問題變得更復雜。

          在這里,我們可以看到,其中所講的“誤解一”,即采用每“一個字節”就是“一個字符”的轉化方法,實際上也就等同于采用 iso-8859-1 進行轉化。因此,我們常常使用 bytes = string.getBytes("iso-8859-1") 來進行逆向操作,得到原始的“字節串”。然后再使用正確的 ANSI 編碼,比如 string = new String(bytes, "GB2312"),來得到正確的“UNICODE 字符串”。

          回頁首

          3.2 非 UNICODE 程序在不同語言環境間移植時的亂碼

          非 UNICODE 程序中的字符串,都是以某種 ANSI 編碼形式存在的。如果程序運行時的語言環境與開發時的語言環境不同,將會導致 ANSI 字符串的顯示失敗。

          比如,在日文環境下開發的非 UNICODE 的日文程序界面,拿到中文環境下運行時,界面上將顯示亂碼。如果這個日文程序界面改為采用 UNICODE 來記錄字符串,那么當在中文環境下運行時,界面上將可以顯示正常的日文。

          由于客觀原因,有時候我們必須在中文操作系統下運行非 UNICODE 的日文軟件,這時我們可以采用一些工具,比如,南極星,AppLocale 等,暫時的模擬不同的語言環境。

          回頁首

          3.3 網頁提交字符串

          當頁面中的表單提交字符串時,首先把字符串按照當前頁面的編碼,轉化成字節串。然后再將每個字節轉化成 "%XX" 的格式提交到 Web 服務器。比如,一個編碼為 GB2312 的頁面,提交 "中" 這個字符串時,提交給服務器的內容為 "%D6%D0"。

          在服務器端,Web 服務器把收到的 "%D6%D0" 轉化成 [0xD6, 0xD0] 兩個字節,然后再根據 GB2312 編碼規則得到 "中" 字。

          在 Tomcat 服務器中,request.getParameter() 得到亂碼時,常常是因為前面提到的“誤解一”造成的。默認情況下,當提交 "%D6%D0" 給 Tomcat 服務器時,request.getParameter() 將返回 [0x00D6, 0x00D0] 兩個 UNICODE 字符,而不是返回一個 "中" 字符。因此,我們需要使用 bytes = string.getBytes("iso-8859-1") 得到原始的字節串,再用 string = new String(bytes, "GB2312") 重新得到正確的字符串 "中"。

          回頁首

          3.4 從數據庫讀取字符串

          通過數據庫客戶端(比如 ODBC 或 JDBC)從數據庫服務器中讀取字符串時,客戶端需要從服務器獲知所使用的 ANSI 編碼。當數據庫服務器發送字節流給客戶端時,客戶端負責將字節流按照正確的編碼轉化成 UNICODE 字符串。

          如果從數據庫讀取字符串時得到亂碼,而數據庫中存放的數據又是正確的,那么往往還是因為前面提到的“誤解一”造成的。解決的辦法還是通過 string = new String( string.getBytes("iso-8859-1"), "GB2312") 的方法,重新得到原始的字節串,再重新使用正確的編碼轉化成字符串。

          回頁首

          3.5 電子郵件中的字符串

          當一段 Text 或者 HTML 通過電子郵件傳送時,發送的內容首先通過一種指定的字符編碼轉化成“字節串”,然后再把“字節串”通過一種指定的傳輸編碼(Content-Transfer-Encoding)進行轉化得到另一串“字節串”。比如,打開一封電子郵件源代碼,可以看到類似的內容:

          Content-Type: text/plain;
          ??????? charset="gb2312"
          Content-Transfer-Encoding: base64

          sbG+qcrQuqO17cf4yee74bGjz9W7+b3wudzA7dbQ0MQNCg0KvPKzxqO6uqO17cnnsaPW0NDEDQoNCg==

          最常用的 Content-Transfer-Encoding 有 Base64 和 Quoted-Printable 兩種。在對二進制文件或者中文文本進行轉化時,Base64 得到的“字節串”比 Quoted-Printable 更短。在對英文文本進行轉化時,Quoted-Printable 得到的“字節串”比 Base64 更短。

          郵件的標題,用了一種更簡短的格式來標注“字符編碼”和“傳輸編碼”。比如,標題內容為 "中",則在郵件源代碼中表示為:

          // 正確的標題格式
          Subject: =?GB2312?B?1tA=?=

          其中,

          • 第一個“=?”與“?”中間的部分指定了字符編碼,在這個例子中指定的是 GB2312。
          • “?”與“?”中間的“B”代表 Base64。如果是“Q”則代表 Quoted-Printable。
          • 最后“?”與“?=”之間的部分,就是經過 GB2312 轉化成字節串,再經過 Base64 轉化后的標題內容。

          如果“傳輸編碼”改為 Quoted-Printable,同樣,如果標題內容為 "中":

          // 正確的標題格式
          Subject: =?GB2312?Q?=D6=D0?=

          如果閱讀郵件時出現亂碼,一般是因為“字符編碼”或“傳輸編碼”指定有誤,或者是沒有指定。比如,有的發郵件組件在發送郵件時,標題 "中":

          // 錯誤的標題格式
          Subject: =?ISO-8859-1?Q?=D6=D0?=

          這樣的表示,實際上是明確指明了標題為 [0x00D6, 0x00D0],即 "?D",而不是 "中"。

          回頁首

          4. 幾種錯誤理解的糾正

          誤解:“ISO-8859-1 是國際編碼?”

          非也。iso-8859-1 只是單字節字符集中最簡單的一種,也就是“字節編號”與“UNICODE 字符編號”一致的那種編碼規則。當我們要把一個“字節串”轉化成“字符串”,而又不知道它是哪一種 ANSI 編碼時,先暫時地把“每一個字節”作為“一個字符”進行轉化,不會造成信息丟失。然后再使用 bytes = string.getBytes("iso-8859-1") 的方法可恢復到原始的字節串。

          誤解:“Java 中,怎樣知道某個字符串的內碼?”

          Java 中,字符串類 java.lang.String 處理的是 UNICODE 字符串,不是 ANSI 字符串。我們只需要把字符串作為“抽象的符號的串”來看待。因此不存在字符串的內碼的問題。

          posted @ 2006-06-23 11:30 點滴鑄就輝煌 閱讀(296) | 評論 (0)編輯 收藏
           
          主站蜘蛛池模板: 涿鹿县| 永德县| 松阳县| 绥江县| 古丈县| 蓬莱市| 塘沽区| 乌兰浩特市| 库车县| 平乡县| 墨竹工卡县| 万宁市| 望都县| 宁远县| 苏尼特右旗| 德令哈市| 集安市| 博湖县| 昭平县| 台东县| 鄂伦春自治旗| 鄂托克旗| 黄骅市| 瑞安市| 平谷区| 监利县| 朝阳县| 南充市| 海盐县| 拉萨市| 内乡县| 保德县| 个旧市| 葫芦岛市| 黄龙县| 五寨县| 浦北县| 买车| 佛教| 磴口县| 克什克腾旗|