JForum源碼分析筆記

我的開發環境:

JForum2.1.8

tomcat5.X

JDK 1.6X

 

以不能脫俗的套路開始。從web.xml開始

web.xml中包括一個filter,一個listener,和兩個servlet,內容不多。

寫道
可以看到裏邊有個監聽器ForumSessionListener,*.page的過濾器ClickstreamFilter,還有2個*.page的處理器,其中InstallServlet是安裝相關的,JForum則是前端處理器。基本上整個流程就是client request -> ForumSessionListener -> ClickstreamFilter -> JForum -> server response.

 

 

filter:net.jforum.util.legacy.clickstream.ClickstreamFilter.java

內部功能:大致爲過濾每一個客戶端請求判斷是否是機器人或者蜘蛛。

代碼如下:

Java代碼 複製代碼
  1. package net.jforum.util.legacy.clickstream;   
  2. //導包部分省略   
  3.   
  4. /**  
  5.  * The filter that keeps track of a new entry in the clickstream for <b>every request</b>.  
  6.  *   
  7.  * @author <a href="[email protected]">Patrick Lightbody</a>  
  8.  * @author Rafael Steil (little hacks for JForum)  
  9.  * @version $Id: ClickstreamFilter.java,v 1.1 2010/02/02 11:20:04 cvsr Exp $  
  10.  */  
  11. public class ClickstreamFilter implements Filter   
  12. {   
  13.     //日誌記錄   
  14.     private static final Logger log = Logger.getLogger(ClickstreamFilter.class);   
  15.   
  16.     /**  
  17.      * Attribute name indicating the filter has been applied to a given request.  
  18.      * 參數暗示一個被經過過濾器檢查的請求  
  19.      */  
  20.     private final static String FILTER_APPLIED = "_clickstream_filter_applied";   
  21.   
  22.     /**  
  23.      * Processes the given request and/or response.  
  24.      * 處理給出的請求 和/或者相應  
  25.      *   
  26.      * @param request The request  
  27.      * @param response The response  
  28.      * @param chain The processing chain  
  29.      * @throws IOException If an error occurs  
  30.      * @throws ServletException If an error occurs  
  31.      */  
  32.     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,   
  33.             ServletException   
  34.     {   
  35.         //確保過濾器在一個請求上只應用一次   
  36.         // Ensure that filter is only applied once per request.   
  37.         if (request.getAttribute(FILTER_APPLIED) == null) {   
  38.             request.setAttribute(FILTER_APPLIED, Boolean.TRUE);   
  39.                
  40.             //調用同級目錄下的BotChecker類的isBot方法檢測請求是否爲一個機器人   
  41.             String bot = BotChecker.isBot((HttpServletRequest)request);   
  42.                
  43.             //如果當前請求是機器人,並且日誌開關處於打開狀態時,記錄   
  44.             if (bot != null && log.isDebugEnabled()) {   
  45.                 System.out.println("Found a bot: " + bot);   
  46.                 log.debug("Found a bot: " + bot);   
  47.             }   
  48.                
  49.             //設置clickstream.is.bot是否爲真   
  50.             request.setAttribute(ConfigKeys.IS_BOT, Boolean.valueOf(bot != null));   
  51.         }   
  52.            
  53.         //繼續傳遞請求   
  54.         // Pass the request on   
  55.         chain.doFilter(request, response);   
  56.     }   
  57.   
  58.     /**  
  59.      * Initializes this filter.  
  60.      *   
  61.      * @param filterConfig The filter configuration  
  62.      * @throws ServletException If an error occurs  
  63.      */  
  64.     public void init(FilterConfig filterConfig) throws ServletException {}   
  65.   
  66.     /**  
  67.      * Destroys this filter.  
  68.      */  
  69.     public void destroy() {}   
  70. }  
package net.jforum.util.legacy.clickstream;
//導包部分省略

/**
 * The filter that keeps track of a new entry in the clickstream for <b>every request</b>.
 * 
 * @author <a href="[email protected]">Patrick Lightbody</a>
 * @author Rafael Steil (little hacks for JForum)
 * @version $Id: ClickstreamFilter.java,v 1.1 2010/02/02 11:20:04 cvsr Exp $
 */
public class ClickstreamFilter implements Filter
{
	//日誌記錄
	private static final Logger log = Logger.getLogger(ClickstreamFilter.class);

	/**
	 * Attribute name indicating the filter has been applied to a given request.
	 * 參數暗示一個被經過過濾器檢查的請求
	 */
	private final static String FILTER_APPLIED = "_clickstream_filter_applied";

	/**
	 * Processes the given request and/or response.
	 * 處理給出的請求 和/或者相應
	 * 
	 * @param request The request
	 * @param response The response
	 * @param chain The processing chain
	 * @throws IOException If an error occurs
	 * @throws ServletException If an error occurs
	 */
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
			ServletException
	{
		//確保過濾器在一個請求上只應用一次
		// Ensure that filter is only applied once per request.
		if (request.getAttribute(FILTER_APPLIED) == null) {
			request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
			
			//調用同級目錄下的BotChecker類的isBot方法檢測請求是否爲一個機器人
			String bot = BotChecker.isBot((HttpServletRequest)request);
			
			//如果當前請求是機器人,並且日誌開關處於打開狀態時,記錄
			if (bot != null && log.isDebugEnabled()) {
				System.out.println("Found a bot: " + bot);
				log.debug("Found a bot: " + bot);
			}
			
			//設置clickstream.is.bot是否爲真
			request.setAttribute(ConfigKeys.IS_BOT, Boolean.valueOf(bot != null));
		}
		
		//繼續傳遞請求
		// Pass the request on
		chain.doFilter(request, response);
	}

	/**
	 * Initializes this filter.
	 * 
	 * @param filterConfig The filter configuration
	 * @throws ServletException If an error occurs
	 */
	public void init(FilterConfig filterConfig) throws ServletException {}

	/**
	 * Destroys this filter.
	 */
	public void destroy() {}
}

 

寫道
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方式)。

 

Listener:

net.jforum.ForumSessionListener.java

實現了javax.servlet.http.HttpSessionListener接口。其主要功能就是重寫了sessionDestroyed方法,對當前session進行了判斷,如果存在session,存儲session信息到DB,之後移除此session信息。

重寫了HttpSessionListener的sessionCreated和sessionDestroyed方法。

sessionCreated方法體爲空。

代碼如下:

Java代碼 複製代碼
  1. /**  
  2.  * @author Rafael Steil  
  3.  * @version $Id: ForumSessionListener.java,v 1.1 2010/02/02 11:20:04 cvsr Exp $  
  4.  */  
  5. public class ForumSessionListener implements HttpSessionListener    
  6. {   
  7.     //實例化日記記錄   
  8.     private static final Logger logger = Logger.getLogger(ForumSessionListener.class);   
  9.        
  10.     /**   
  11.      * @see javax.servlet.http.HttpSessionListener#sessionCreated(javax.servlet.http.HttpSessionEvent)  
  12.      * 空方法  
  13.      */  
  14.     public void sessionCreated(HttpSessionEvent event) {}   
  15.   
  16.     /**   
  17.      * @see javax.servlet.http.HttpSessionListener#sessionDestroyed(javax.servlet.http.HttpSessionEvent)  
  18.      */  
  19.     public void sessionDestroyed(HttpSessionEvent event)    
  20.     {   
  21.         HttpSession session = event.getSession();   
  22.            
  23.         if (session == null) {   
  24.             return;   
  25.         }   
  26.            
  27.         String sessionId = session.getId();   
  28.   
  29.         try {   
  30.             //持久化當前session中信息   
  31.             SessionFacade.storeSessionData(sessionId);   
  32.         }   
  33.         catch (Exception e) {   
  34.             logger.warn(e);   
  35.         }   
  36.   
  37.         //移除此session信息   
  38.         SessionFacade.remove(sessionId);   
  39.     }   
  40. }  
/**
 * @author Rafael Steil
 * @version $Id: ForumSessionListener.java,v 1.1 2010/02/02 11:20:04 cvsr Exp $
 */
public class ForumSessionListener implements HttpSessionListener 
{
	//實例化日記記錄
	private static final Logger logger = Logger.getLogger(ForumSessionListener.class);
	
	/** 
	 * @see javax.servlet.http.HttpSessionListener#sessionCreated(javax.servlet.http.HttpSessionEvent)
	 * 空方法
	 */
	public void sessionCreated(HttpSessionEvent event) {}

	/** 
	 * @see javax.servlet.http.HttpSessionListener#sessionDestroyed(javax.servlet.http.HttpSessionEvent)
	 */
	public void sessionDestroyed(HttpSessionEvent event) 
	{
		HttpSession session = event.getSession();
		
		if (session == null) {
			return;
		}
		
		String sessionId = session.getId();

		try {
			//持久化當前session中信息
			SessionFacade.storeSessionData(sessionId);
		}
		catch (Exception e) {
			logger.warn(e);
		}

		//移除此session信息
		SessionFacade.remove(sessionId);
	}
}

 

Servlet:

寫道
可以看到 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 -> 結束

 net.jforum.JForum.java它繼承了JForumBaseServlet類

這個servlet相當於Struts中的前端控制器,所有請求都將通過這個類轉發。

 

寫道
上面簡單提到了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集成,還是比較清晰明瞭的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章