一個較完整的關鍵字過濾解決方案(下)
2009-01-05 14:59 Jeffrey Zhao 閱讀(19661) 評論(29) 收藏 舉報在這篇文章里,我們來針對一些問題進行討論。如果您覺得有哪些您感興趣但是沒有涉及到的問題則請在評論中補充,我會修改文章添加一下內容。
陷阱何在?
首先,我們來分析上一篇文章最后談到的“陷阱”。很可惜啊,過了兩個星期還是沒有朋友能夠指出這個問題,其實很簡單,運行一下就能發覺有異常拋出:
public partial class Default : System.Web.UI.Page, IForbiddenWordFilter { ... FilterForbiddenWordType IForbiddenWordFilter.GetFilterType(string key) { if (key.EndsWith(this.txtPassword.ID)) return FilterForbiddenWordType.Ignored; return FilterForbiddenWordType.Normal; } }
在運行至this.txtPassword.ID時會拋出NullReferenceException。其原因就是,我們的FilterForbiddenWordModule在OnPostMapRequestHandler過程中進行調用,而此時Handler對象已經生成(意味著IForbiddenWordFilter.GetFilterType方法已經可以調用),但是直到Handler被執行時this.txtPassword才被實例化(從現象得出的結論,是否確切有待考證),自然會拋出NullReferenceException了。可是我實在想不出一個辦法可以在得到this.txtPassword.ID的值,甚至退一步講,我無法在運行時得到this.txtPassword這個field的名稱——即一個字符串“txtPassword”。我不可能直接使用這個字符串常量,這樣就會使我們的改進效果付之東流。我們需要通過代碼來訪問,因為我們需要能夠得到編譯及重構的支持,不是嗎?
起初我很絕望,但是10分鐘后忽然靈光一閃,想到了這種方式來獲得field的名稱:
FilterForbiddenWordType IForbiddenWordFilter.GetFilterType(string key) { Expression<Func<object>> action = () => this.txtPassword; var name = (action.Body as MemberExpression).Member.Name; if (key.EndsWith(name)) return FilterForbiddenWordType.Ignored; return FilterForbiddenWordType.Normal; }
這是一個非常實用的技巧:通過Lambda表達式來構造一個表達式樹,然后通過這個表達式樹的成員來獲取field的名稱。我們享受到了我們所需的便利,因為個中實現已經由編譯器完成(或許我會另寫文章來闡述一下我在關于這個方面的思維過程)。
適用場合
有的時候我覺得談適用場合比較虛,因為其實關鍵是在“思考”。“官方”提出的適用場合并不一定完整和正確,了解了一個解決方案之后慢慢會有更好的體會,甚至更真實。如果一個解決方案是通過一個適用場合引發的,那么這個解決方案的適用場合“似乎”不言而喻。此外,如果一個解決方案是像我們現在的這樣一樣,從實際出發,再發散,慢慢將功能補充完整,最后幾經權衡之后反而有些違背初衷,那么談適用場合其實就是在談“理解”,當你理解了這個解決方案的特性,適用場合和不適用場合都可以簡單地判斷出來。所以再虛還是要談,至少要擺個樣子思考一下。
例如:我們是在輸入的時候進行過濾,那么服務器端得到的數據已經是替換后的內容,因此如果你要用戶原本輸入的內容,肯定就不能采用這個方法。
嗯?完了?當然沒完,但是下面就要由您來進行思考了。:)
輸入過濾和輸出過濾
關于這個問題,討論得糾結啊。我們現在整理一下輸入過濾和輸出過濾的優點和缺點(歡迎補充):
輸入過濾:
- 優點:
- 在輸入時控制,需要替換的次數少,性能高。
- 可控制的粒度小,方便地對于輸入定制各種過濾方式。
- 缺點:
- 解決方案相對不夠普適,有時需要為不同的Handler定制不同替換策略,雖然這點很簡單。
- 無法獲得用戶原始輸入。
輸出過濾:
- 優點:
- 普適,Plug & Play,過濾一切輸出。
- 可以保留用戶原始輸入。
- 缺點:
- 每次輸出都需要替換,性能低下。
可以發現,基本上輸出緩存是在實用性能換取絕對透明、以及。有朋友說某些場景下只能使用輸出緩存,不該把它一棒子打死——但是至少也要打個半死不活。原因就在于這個性能問題實在過于難以處理了。
首先是輸出過濾時在每次生成HTML時都要對完整的字符串進行替換,首先HTML中大部分的字符是不用替換的(因為是我們自定義的文字或HTML代碼),其次每秒過濾數百次大字符串是一個很傷CPU運算的做法——無論在哪個平臺下。而避免大量運算的常用手段就是將運算結果保留起來并多次使用。這就是所謂的緩存,可惜……
輸出過濾難以緩存。這一點不是因為實現困難而放棄,而是實在是沒有好的辦法進行緩存。輸出過濾往往使用Response.Filter,它的最小單位是“一個Response”,因此我們傳統緩存機制中唯一可用的可能只有整頁靜態化了(連局部內容緩存都無法生效)。現在的Web應用大都“變化多端”,整頁靜態的適用程度愈發有限,這是由于整頁緩存難以設過期依賴,一是依賴項過多,頁面所表現的業務中任何一個數據的變化都會造成整頁修改,這種業務與頁面之間多對多的關系使維度急劇增加,難以操作。再者就是這樣的緩存依賴項往往要打通表現層和業務邏輯層甚至數據訪問層,在一個設計良好的系統里不能出現這樣的狀況。
當然輸出緩存既然有優點,我們可能也就需要想一些辦法來緩解一部分問題。思路就是使用比Response粒度低的輸出過濾。例如在CRUD的R方法上做文章,這樣內容緩存就變成了數據緩存(關于這兩種緩存的優劣我在《輸出緩存與CachePanel》一文中有過簡單討論)。如果頁面中需要替換的內容部分變化不多,也可以使用一個簡單的帶有過濾功能的CachePanel來進行此部分工作。但我思考了很久,還是覺得不容易。不知哪位朋友會有更好的想法,只希望能有個確實的示例或說明,而不要簡單的一句話思路,似乎有道理卻讓人無從考究。
如果真要保留用戶原始輸入,其實我認為最恰當的方式是保留原始拷貝——當然這也很麻煩。弟兄們還是權衡為上。
我們真的需要HttpModule嗎?
第一篇文章里我就說了,全站級別的操作,往往解決方案只有一個,那就是HttpModule——其實這句話補充完整應該是:在使用統一模型的解決方案中,可以使用橫切的方式來為該模型的數據輸入作統一處理。換句話說,假如我們整站都使用了一個統一的自定義模型,那么我們自然可以在這個模型上做文章。如果沒有這樣的(自定義)模型,那么我們能找到的唯一共同之處就只有“ASP.NET網站”這一點了。此時針對這一模型的橫切方式,自然就是HttpModule。
那么我們可能還會有哪些模型呢?至少我們現在已經有一個了:那就是ASP.NET MVC。ASP.NET MVC改變了之前開發ASP.NET站點的理念,它統一了服務器端對于客戶端請求處理方式,將請求與方法進行了映射。如果說在使用ASP.NET WebForms時不可避免的需要編寫Generic Handler(ashx)來進行非頁面的請求處理,那么在ASP.NET MVC中也應該使用同樣的Controller-Action方式——如果還出現ashx的話,您就要思考一下這么做的合理性了。好,既然我們將全站統一至ASP.NET MVC模型之上,則接下來要做的就是在它的數據輸入方式上做文章了。ASP.NET MVC使用一個名為Model Binder的機制將Request中的數據轉化為Action方法的參數。如果我們使用一個自定義的Model Binder,參數構造時進行文字過濾,自然也可以滿足我們的要求。
性能
全站替換從感覺上似乎會影響性能,但是細想之下,并沒有帶來多大損害,因為“需要過濾的地方”它“總歸要被過濾”嘛。但是一些措施可能還是需要的:例如在GET請求時不替換Form里的數據(其實本就應該沒有數據)、對Handler做合適標記(盡可能減少需要過濾的內容)、在沒有替換任何內容時不更新原有集合(減少折騰次數),亦或是Filter on Demand(只在讀取某字段時替換內容)。
最后,其實最影響性能的可能就是過濾算法了——這本不該在文章中出現,但是我還是想提一下。字符串操作往往是系統的命門,處理不好會因此大量字符串的產生,加大GC壓力,因此StringBuilder自然是不可少的。還有關鍵的一點是String的Replace方法絕對不可以使用,因為需要過濾的關鍵字往往不在少數,使用Replace方法會在內存中出現大量的字符串。更進一步,假如有N個關鍵字,需要過濾一個長度為M的字符串,那么使用Replace方法的時間復雜度至少是O(M * N);而如果換種方式,例如使用前綴樹構造一個索引,實現復雜度最多也就是O(M * H)了——H為樹的高度,比N要小許許多多,而且與N的數量無關。
關于這一點,第一篇文章一開始引用的兩篇文章中方式大體是正確的,值得參考。
相關文章
- 一個較完整的關鍵字過濾解決方案(上)
- 一個較完整的關鍵字過濾解決方案(中)
- 一個較完整的關鍵字過濾解決方案(下)
浙公網安備 33010602011771號