文/For A
怎麼才算好的原始碼分析呢?當然我這個肯定不算。我想大概分為幾個層面吧,寫寫註釋那算最基本的了,寫寫要點思路和難點,算是還不錯拉,再難的就是跳出原始碼舉一反三,形成自己的一套思路吧。
這次針對的是jforum2.1.8,大概jforum團隊已經沒心情理這個版本了,都衝著jforum3去了。選擇這個版本,主要是因為jforum在java論壇類應用中算是佼佼者了,很多人都拿這個來做二次開發,而jforum3使用的是另外一套架構了,而且還沒完全release,所以斟酌一下,還是選擇這個經典的版本。
關於jforum的介紹網上已經很多了,這裡也簡單抄錄一段:JForum 是一個功能強大 ,易於管理的論壇。它的設計完全遵從MVC設計模式,能夠在任何Servlet容器與EJB伺服器上執行。而且可以輕鬆的定製與擴充套件JForum論壇。
上面這段簡述還是中肯的。另外,jforum是模仿phpbb寫的,使用的是classic-blue風格,但不能自己選擇風格,要的話只能自己修改了。
再說幾句,說jforum比較優秀是因為java開源的論壇系列精品少,而且jforum的bug也真的不少,不信試試就知道了。不過作為一個成型的元件,功能強大並且適合二次開發,還是應該列入考慮範圍的。
不管怎樣,jforum是個不錯的學習範本,至少讓你覺得寫個山寨框架不是什麼難事,而事實也的確是這樣的。重要的一點是,不要輕易拿出來害人就是了:)這裡先列舉出可能一些分析點:
web.xml
初始化流程
處理請求流程(mvc)
檔案監控
快取實現
資料庫訪問實現
許可權控制
首先了解一個web應用,首要的就是知道處理流程。首先來看看入口web.xml,裡邊的內容還是挺清晰的,可以看到裡邊有個監聽器ForumSessionlistener,*.page的過濾器ClickstreamFilter,還有2個*.page的處理器,其中InstallServlet是安裝相關的,JForum則是前端處理器。基本上整個流程就是client request -> ForumSessionlistener -> ClickstreamFilter -> JForum -> server response.
ForumSessionlistener實現了HttpSessionlistener介面,但是隻是對session destory做了處理,在這個過程中,儲存session的歷史記錄到DB,並清除使用者資訊和相關的security資訊。
ClickstreamFilter實現了Filter介面,主要的任務就交給BotChecker了,是用來檢測client是不是一個robot來的。
主要的工作還是在JForum上面,不過先來看看jforum是怎麼檢測robot的?
BotChecker只有一個靜態工具方法isBot,首先是檢測是否請求robot.txt(這是標準的robot協議檔案),接下去判斷User-Agent頭部,最後是判斷remotehost。而已知的robot都是寫在檔案clickstream-jforum.xml裡邊的(包括agent和host),並透過ConfigLoader載入進來的(SAX方式)。
可以看到JForum和InstallServlet都繼承了JForumBaseServlet這個HttpServlet,而JForumBaseServlet包括2個重要的方法init和startApplication。眾所周知,init是servlet初始化時呼叫的方法,JForumBaseServlet裡邊的init方法的流程是:
呼叫父類的init(正常情況這是必須呼叫的) -> 配置log4j -> startSystemglobals(載入全域性引數配置SystemGlobals.properties -> 載入資料庫配置database.driver.config(如MySQL就是WEB-INF/config/database/mysql/mysql.properties) -> 載入自定義配置(預設的是jforum-custom.conf)) -> 配置快取引擎 -> 配置freemarker模板引擎 -> 載入模組配置modulesMapping.properties -> 載入url對映配置urlPattern.properties -> 載入I18n配置(languages/*) -> 載入頁面對映配置(templatesMapping.properties) -> 載入BBcode配置bb_config.xml -> 結束
jforum實現了自己的mvc,整個mvc的脈絡就是client request -> 解析url(urlPattern.properties),獲取module/action/param -> 透過module獲取相應的module class,並透過action識別並呼叫相應的方法(modulesMapping.properties) -> 使用dao完成業務邏輯 -> 呼叫template進行渲染(templatesMapping.properties),其實整個mvc和struts沒什麼兩樣的,具體的流程以後再提。
JForumBaseServlet裡邊的startApplication方法的流程是:
載入通用sql檔案sql.queries.driver(就是/database/generic/generic_queries.sql) -> 載入特定sql檔案(如mysql就是/database/mysql/mysql.sql) -> 載入Quartz定時任務配置 -> 載入登入驗證器(驗證方式) -> 載入Dao實現方式 -> 載入檔案修改監聽器 -> 載入查詢索引管理器 -> 載入定時統計任務
jforum實現了自己的orm,當然不是hibernate那種,是類似ibatis的那種sql mapping,並提供了多套的sql檔案來實現資料庫無關的特性,整個流程也是比較清晰的,載入資料庫配置 -> 載入sql mapping file -> 設定DAO實現 -> 透過named sql找到對應的sql(在*.sql裡邊對應著) -> 執行出資料
繼續重點。JForum的init流程如下:
JForumBaseServlet.init -> JForumBaseServlet.startApplication -> 啟動資料庫 -> 預載入一些資料到快取中(ForumRepository[Categories,Forums,同時線上最大人數,最後登入使用者,註冊使用者數等等],使用者等級,表情資料,遮蔽列表) -> 結束
上面簡單提到了Jforum處理請求的過程,現在在來看看這個過程,就是service方法,這次採用程式碼概要的方式展示:
// 初始化JForumExecutionContext
JForumExecutionContext ex = JForumExecutionContext.get();
// 包裝request和response
request = new WebRequestContext(req);
response = new WebResponseContext(res);
// 檢查資料庫狀態
this.checkDatabaseStatus();
// 建立JForumContext並設定到JForumExecutionContext中去
.......
JForumExecutionContext.set(ex);
// 重新整理session
utils.refreshSession();
// 載入使用者許可權
SecurityRepository.load(SessionFacade.getUserSession().getUserId());
// 預載入模板需要的上下文
utils.prepareTemplateContext(context, forumContext);
// 從request中解析module name
String module = request.getModule();
// module name -> module class
String moduleClass = module != null ? ModulesRepository.getModuleClass(module) : null;
// 判斷是否在ban list裡邊
......
boolean shouldBan = this.shouldBan(request.getremoteAddr());
// 主角出場
out = this.processCommand(out, request, response, encoding, context, moduleClass);
// 掃尾工作,例如db的rollback
this.handleFinally(out, forumContext, response);
processCommand會呼叫Command的process方法:
// 獲取一個module例項(繼承了Command)
Command c = this.retrieveCommand(moduleClass);
// 進入process
Template template = c.process(request, response, context);
// 這裡開始是process方法
//獲取action
String action = this.request.getAction();
//如果不是ignore的,就呼叫這個action
if (!this.ignoreAction) {this.getClass().getMethod(action, NO_ARGS_CLASS).invoke(this, NO_ARGS_OBJECT);}
//如果是轉發的,就把TemplateName清空
if (JForumExecutionContext.getredirectTo() != null) {this.setTemplateName(TemplateKeys.EMPTY);}
//不是轉發且attribute裡邊存在template,則設定為templateName
else if (request.getAttribute("template") != null) {this.setTemplateName((String)request.getAttribute("template"));}
//是否coustomContent?例如下載,驗證碼子類的不需要頁面的操作
if (JForumExecutionContext.isCustomContent()) {return null;}
//返回一個template
return JForumExecutionContext.templateConfig().getTemplate(
new StringBuffer(SystemGlobals.getValue(ConfigKeys.TEMPLATE_DIR)).
append('/').append(this.templateName).toString());
}
// 從process出來,回到processCommand
// 設定content type
response.setContentType(contentType);
//生成頁面並flush
if (!JForumExecutionContext.isCustomContent()) {
out = new BufferedWriter(new OutputStreamWriter(response.getOutputStream(), encoding));
template.process(JForumExecutionContext.getTemplateContext(), out);
out.flush();
}
}
這是一般的流程,就像上面提到的customContent,就是要自己處理了,可以參考CaptchaAction.generate().