jforum2源碼分析

來源:互聯網
上載者:User
jforum2源碼分析

作者:蔡曉建

怎麼才算好的源碼分析呢?當然我這個肯定不算。我想大概分 為幾個層面吧,寫寫注釋那算最基本的了,寫寫要點思路和痛點,算是還不錯拉,再難的就是跳出源碼舉一反三,形成自己的一套思路吧。好好努力吧。

這 次針對的是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().

這 樣的話,如果我們要增加一些action進行二次開發的話,大體的流程就是,增加一個繼承了 Command的類,例如叫ExampleAction,定義一個方法,例如叫test(),在urlPattern.properties中定義一個映 射,例如為example.test.1 = forum_id,再在modulesMapping.properties中定義module class的映射,如example = ExampleAction,最後我們在templatesMapping.properties定義個模板的映射,如:example.test = example_test.htm。現在假設我們的請求url是/example/test/1,再來看看test裡邊的一些方法:
this.request.getIntParameter("forum_id"))  //擷取參數,得到1
this.context.put("obj", obj); //把結果寫入context,這樣可以在template中擷取到
this.setTemplateName("example.test");// 設定template的名字

這樣的簡單流程應該還比較好理解吧?

另外,還可以看出,jforum使用了自己的一套映射機 制,這是通過urlPattern.properties來定義的(參考上面 JForumBaseServlet的init流程),這是在JForumBaseServlet的loadConfigStuff方法的第一行實現的, 並載入到UrlPatternCollection中去,如下所示:
Properties p = new Properties();
fis = new FileInputStream(SystemGlobals.getValue(ConfigKeys.CONFIG_DIR) + "/urlPattern.properties");
p.load(fis);

for (Iterator iter = p.entrySet().iterator(); iter.hasNext(); ) {
   Map.Entry entry = (Map.Entry) iter.next();
   UrlPatternCollection.addPattern((String)entry.getKey(), (String)entry.getValue());
}
可以知道這裡的key和value都是String來的
UrlPatternCollection.patternsMap.put(name, new UrlPattern(name, value));
但在addPattern方法裡邊其實是產生一個UrlPattern作為 value,如何構造一個UrlPattern可以看看代碼,舉例來說把,對於 example.hello.2=a,b,這樣會產生一個UrlPattern,裡邊的內容是name為example.hello.2,value為 a,b.而size和vars是用a,b解析出來的,用來表示一共有多少個參數,參數名組成的數組。所以UrlPattern儲存的就是一個url格式的 定義,而放在UrlPatternCollection裡邊的一系列的url映射格式是在請求的url解析的時候用到的。

現在再分析一下 jforum怎麼使用這個UrlPatternCollection的?按照我們不嚴格的思路,應該是service中處理url,擷取.page前面的 一部分,如/example/hello/2/1,用/做一下split,擷取module name,action name,把最後的作為參數,用module,action,參數個數組成一個key(example.hello.2),通過 UrlPatternCollection找到對應的UrlPattern,通過裡邊的格式對應(vars裡邊的參數名和url的參數值)就可以把參數添 加到request的parameters裡邊去。實際的情況也差不多就這個樣。在說到jforum中的service方法的時候,簡單提到過 request和response是經過封裝的:
request = new WebRequestContext(req);
response = new WebResponseContext(res);
WebResponseContext只是簡單的delegate給 HttpServletResponse(這樣做的好處是全部方法都限制在ResponseContext中),而WebRequestContext是 繼承了HttpServletRequestWrapper並實現了RequestContext介面。所以WebRequestContext是一個 HttpRequest,但是通過RequestContext介面實現了一些特定的方法就是了,例如getModule/getAction,而這個解 析url的過程是在構建WebRequestContext對象的過程中實現的。可以看看WebResponseContext的構造方法,這裡就不詳細 說了。注意的是,所有的parameters最後都儲存到query(一個私人的map)裡邊去的。還有就是上面說到的jforum的特定url映射機 制,這是通過WebRequestContext的parseFriendlyURL方法實 現的,原理就和上面提到的那樣,也不詳說了。

到這裡,基本上整個處理流程就差不多了。現在來說說jforum裡邊的檔案修改監聽器(JForumBaseServer的startApplication流程),如果你在 使用jforum的過程中,修改了某些檔案如*.sql,jforum就會重新載入修改後的配置。我原來以為是用quartz架構來實現的,後來才知道是 用jdk的TimerTask類來實現的。請看ConfigLoader的listenForChanges方法:
FileMonitor.getInstance().addFileChangeListener(new QueriesFileListener(),
                SystemGlobals.getValue(ConfigKeys.SQL_QUERIES_GENERIC), fileChangesDelay);

這裡給各個部分分一下責任,FileMonitor是大管家,負責管理所有的檔案監聽 器;FileChangeListener是一個監聽器介面,只有一個方法,就是fileChanged(String filename),意思就是對某個filename的修改作出怎樣的反應。使用的方法也很簡單,就是實現一個FileChangeListener,並 和監控的檔案名稱,檢查間隔作為參數傳入就可以生效了。FileMonitor裡邊的實現原理就是,通過一個map(timerEntries)來儲存(文 件名/timertask),每次加入一個監聽器的時候,會根據檔案名稱先移出原來的檔案監聽器(缺點是只能能對一個檔案添加一個監聽器),然後構建一個 TimerTask並加入到timerEntries中去。關於TimerTask的具體用法,可以參考api。

作為一個論壇,應用程式層緩 存這樣的東西似乎必不可少,jforum也提供了緩衝配置(上面也提到一些)。jforum提供了數種緩衝 實現(JForumBaseServlet的init流程),分別是DefaultCacheEngine(簡單的記憶體實 現),JBossCacheEngine,EhCacheEngine。,請看ConfigLoader的startCacheEngine方法,流程大 概就是得到cacheEngine的實現配置(SystemGlobals.properties中配置 cache.engine.implementation),然後產生CacheEngine的執行個體,調用它的init方法進行初始化,然後找到所有的可 緩衝類(實現了Cacheable介面,並在SystemGlobals.properties中配置cacheable.objects),最後把 cacheEngine注入進去獲得cache的能力。雖然jforum自己實現了許多這樣的注入(除了cacheEngine,還有db,dao等 等),雖然達到了一定的的目的,可是怎麼說還是到處充滿了Singleton的實現(參考spring2.5文檔3.9. 粘合代碼和可怕的singleton),為了尋求更好的組織方式(例如使用ioc來管理對象,使用成熟的orm來隔離資料庫)和獲得更多的使用者群(選擇更 廣泛使用的架構協助),大概才會萌發jforum3的想法吧。

順便提一下jforum的Dao 實現方式(參考JForumBaseServlet的startApplication流程),參考ConfigLoader的 loadDaoImplementation方法,原理就是通過配置dao.driver(在特定的資料庫配置裡邊如mysql.properties) 擷取到DataAccessDriver的實現 -> 初始化DataAccessDriver -> 擷取到所有的Dao實現。可以這麼理解,實現一個DataAccessDriver就獲得一整套Dao的實現方式,對於dao裡邊的實現方法,給個範例:
// 例行公事
PreparedStatement p = null;
ResultSet rs = null;
//獲得 connect,並執行named sql
p = JForumExecutionContext.getConnection().prepareStatement(SystemGlobals.getSql("GroupModel.selectById"));
p.setInt(1, groupId);
rs = p.executeQuery();
Group g = new Group();
//迴圈 resultset進行處理
if (rs.next()) {g = this.getGroup(rs);}

整個實現很直白, 就是一個jdbc實現方式來的。對於如何擷取connection,查看JForumExecutionContext的 getConnection(),可以注意到這麼一句:
c = DBConnection.getImplementation().getConnection();
也是比較清晰的,另外可以知道的是,在每 次請求的過程中,connection只會擷取一次,並在第一次擷取到以後放到ThreadLocal裡邊去,這樣在每個線程中保留一份資料(正確理解TheradLocal ),在請求請求結束以後才釋放connection(service流程中的handleFinally方法)。

JForumExecutionContext, 如字面意,就是請求執行的上下文,例如上面提到的資料庫連接,還有ForumContext(放著和request,response相關的信 息),context(freemarker的上下文變數),redirectTo(轉寄地址),contentType(響應內容格 式),isCustomContent(不使用預設渲染,上面有提到),enableRollback(db是否會滾)。

jforum是 可以配置許可權的,可控制的權限類別型放在SecurityConstants裡邊,對應的配置 介面是根據permissions.xml產生的(參考GroupAction的permissions)。而每個使用者的許可權 (PermissionControl)是通過SecurityRepository來管理的,最用形成的許可權系統是role(許可權)-group(使用者 組,可以多級)-使用者這樣的結構圖。

如何判斷許可權?
對於一個使用者來說,為了擷取使用者的許可權 (PermissionControl),流程是這樣的(詳細看SecurityRepository的load方法):擷取使用者資訊 -> 擷取使用者的所有groupid並組成一個用逗號隔開的字串groupids  -> 根據groupids擷取所有的name/role_value -> 組裝成RoleValueCollection -> 產生RoleCollection -> 最後產生PermissionControl

判斷許可權是使用 SecurityRepository的canAccess(int userId, String roleName, String value)方法:
根據userid擷取PermissionControl-> 如果value參數為空白的話,就判斷是否擁有該roleName(通過內部的RoleCollection對象的keys),就是是否含有該許可權 -> 如果value參數不為空白的話,除了需要含有該許可權,還要擁有相應的rolevalue(通過內部的RoleCollection對象的values)。 參數中的value指數可以為論壇分類id,論壇id之類,隨業務而定。

總體上jforum還算清晰,大部分的業務代碼沒有細看(那些 Command類),有興趣可以對照著寫,大體分為三個包(admin是管理,jforum是公用頁面,install是安裝頁面)。

既 然說到驗證,就順便要說說jforum的sso驗證機制
官方文檔:
http://www.jforum.net/doc/SSO
http://www.jforum.net/doc/ImplementSSO
http://www.jforum.net/doc/SSOcookies
http://www.jforum.net/doc/SSOremote
有 上面這些文檔基本可以自己實現一個,主要就是實現net.jforum.sso介面就是了。

在Jforum的service方法裡邊有段 (service流程中的重新整理session):
ControllerUtils utils = new ControllerUtils()
utils.refreshSession();// 重點
裡邊提到,在沒有usersession的情況下,如果配置的驗證類型是sso(authentication.type),就調用 checkSSO(UserSession userSession)的方法
-> 產生SSO執行個體(使用sso.implementation來配置) -> 調用authenticateUser(RequestContext request)返回username
-> 假如取不到的username,就設為匿名 -> 否則,如果不存在該使用者(utils.userExists(username)則註冊一個(utils.register(password, email)) -> 假如已經存在,則讓使用者登入(configureUserSession(userSession, utils.getUser()))
當已經存在usersession的時候,並且驗證方式是sso的時候,就是驗證是否有效 (sso.isSessionValid(userSession, request))。
所以,整個過程和官方文檔提到的流程是一樣的,如果要 實現自己的sso,這是實現SSO介面,使用authenticateUser來驗證不存在usersession的情況,並返回username or null,而使用isSessionValid來判斷一個已經存在的usersession是否有效。參考上面幾個串連文檔,實現和已有系統的sso集 成,還是比較清晰明了的。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.