1 Jive功能需求
Jive功能需求分析類似于一個新系統的需求分析。只有了解Jive系統實現了哪些論壇功能,才能進一步研究和學習它是怎樣
巧妙、優雅地實現這些功能的。


在Jive論壇系統中,用戶角色和權限是緊密聯系在一起的。主要分兩大角色:普通用戶和管理員,具體的表現形式是通過權限組合來體現的。管理方面的權限有:

         SYSTEM_ADMIN,系統管理員,可以管理整個系統。

          FORUM_ADMIN,論壇管理員,可以管理某個特定的論壇。

          USER_ADMIN和GROUP_ADMIN,用戶和組管理員,可以管理一些特定用戶和用戶組。

論壇的讀寫權限包括:讀權限,創建一個新主題,創建一個新的帖子等。

Jive中沒有明確定義普通用戶和管理員角色,而是直接通過以上權限組合和具體用戶直接建立聯系,并將這種直接聯系保存到數據庫中。

在權限不是很復雜的情況下,這種沒有引入角色的做法比較簡單直接。但由于用戶和權限直接掛鉤,而用戶和權限都可能在不斷地動態變化,那么它們之間由于聯系太直接和緊密,對各自變化形成了限制。所以,對于復雜的權限系統,引入了基于角色的權限系統,這將在以后章節中進一步討論。

Jive論壇業務對象主要分為ForumForumThreadForumMessage,它們之間的關系如圖3-2所示。

每個論壇Forum包含一系列ForumThread(主題),而每個主題都是由很多內容帖子ForumMessage組成的,這是一個聚集關系。這3種對象中每一個對象都涉及到對象數據的創建、編輯、查詢和刪除,這些對象數據分別保存在數據庫中。這3個對象對于不同的角色可操作訪問權限是不一樣的,只有系統管理員和論壇管理員可以對Forum相關數據實行操作,普通用戶可以創建或編輯ForumThreadForumMessage

Jive論壇為了實現不同用戶對不同基本對象的不同操作權限,通過設定一個統一的入口,在這個入口將檢查客戶端每次對數據的操作權限,如圖3-3所示。

        

客戶端每次對數據庫的操作,都要經過ForumFactory入口進入。在ForumFactory中會動態生成一個訪問控制代理ForumFactoryProxy,通過ForumFactoryProxy檢查客戶端訪問方法是否符合整體權限訪問控制要求。

ForumFactory與工廠模式

GOF設計模式中,工廠模式分為工廠方法模式和抽象工廠模式。兩者主要區別是,工廠方法是創建一種產品接口下的產品對象,而抽象工廠模式是創建多種產品接口下的產品對象,非常類似Builder生成器模式。在平時實踐中,使用較多的基本是工廠方法模式。

以類SampleOne為例,要創建SampleOne的對象實例:

SampleOne sampleOne = new SampleOne();

如果Sample類有幾個相近的類:SampleTwoSampleThree,那么創建它們的實例分別是:

SampleTwo sampleTwo = new SampleTwo();

SampleThree sampleThree = new SampleThree();

其實這3個類都有一些共同的特征,如網上商店中銷售書籍、玩具或者化妝品。雖然它們是不同的具體產品,但是它們有一個共同特征,可以抽象為“商品”。日常生活中很多東西都可以這樣高度抽象成一種接口形式。上面這3個類如果可以抽象為一個統一接口SampleIF,那么上面語句就可以成為:

SampleIF sampleOne = new SampleOne();

SampleIF sampleTwo = new SampleTwo();

SampleIF sampleThree = new SampleThree();

在實際情況中,有時并不需要同時生成3種對象,而是根據情況在3者之中選一個。在這種情況下,需要使用工廠方法來完成了,創建一個叫SampleFactory的抽象類:

public class SampleFactory{

   public abstract SampleIF creator();

}

在這個抽象工廠類中有一個抽象方法creator,但是沒有具體實現,而是延遲到它的子類中實現,創建子類SampleFactoryImp

public class SampleFactoryImp extends SampleFactory{

   public SampleIF creator(){

    //根據其他因素綜合判斷返回具體產品

    //假設應該返回SampleOne對象

       return new SampleOne();

}

}

SampleFactoryImp中根據具體情況來選擇返回SampleOneSampleTwoSampleThree。所謂具體情況有很多種:上下文其他過程計算結果;直接根據配置文件中配置。

上述工廠方法模式中涉及到一個抽象產品接口Sample,如果還有其他完全不同的產品接口,如Product等,一個子類SampleFactoryImp只能實現一套系列產品方案的生產,如果還需要另外一套系統產品方案,就可能需要另外一個子類SampleFactoryImpTwo來實現。這樣,多個產品系列、多個工廠方法就形成了抽象工廠模式。
前面已經討論在Jive中設置了論壇統一入口,這個統一入口就是ForumFactory,以下是ForumFactory的主要代碼:

public abstract class ForumFactory {

  
private static Object initLock = new Object();
  
private static String className = " com.Yasna.forum.database.DbForumFactory";
  
private static ForumFactory factory = null
  
public static ForumFactory getInstance(Authorization authorization) {
    
if (authorization == null{
      
return null;
    }

    
//以下使用了Singleton 單態模式,將在2.3節討論
    if (factory == null{
      
synchronized(initLock) {
        
if (factory == null{
             
//從配置文件中獲得當前className
          try {
             
//動態裝載類

              Class c 
= Class.forName(className);

              factory 
= (ForumFactory)c.newInstance();
          }

          
catch (Exception e) {

              
return null;
          }

        }

      }

    }

    
//返回 proxy.用來限制授權對forum的訪問
    return new ForumFactoryProxy(authorization, factory,factory.getPermissions(authorization));
  }


 
//創鍵產品接口Forum的具體對象實例

  
public abstract Forum createForum(String name, String description)

  
throws UnauthorizedException, ForumAlreadyExistsException;

   
//創鍵產品接口ForumThread的具體對象實例

public abstract ForumThread createThread(ForumMessage rootMessage) 

throws UnauthorizedException;

//創鍵產品接口ForumMessage的具體對象實例ForumFactory中卻不立即執行,而是推遲到ForumFactory子類中實現。 




ForumFactory的子類實現是com.Yasna.forum.database.DbForumFactory,這是一種數據庫實現方式。即在DbForumFactory中分別實現了在數據庫中createForum、createThread和createMessage()等3種方法,當然也提供了動態擴展到另外一套系列產品的生產方案的可能。如果使用XML來實現,那么可以編制一個XmlForumFactory的具體工廠子類來分別實現3種創建方法。
因此,Jive論壇在統一入口處使用了抽象工廠模式來動態地創建論壇中所需要的各種產品

XmlForumFactory和DbForumFactory作為抽象工廠ForumFactory的兩個具體實現,而Forum、ForumThread和ForumMessage分別作為3個系列抽象產品接口,依靠不同的工廠實現方式,會產生不同的產品對象。

從抽象工廠模式去理解Jive論壇統一入口處,可以一步到位掌握了幾個類之間的大概關系。因為使用了抽象工廠模式這種通用的設計模式,可以方便源碼閱讀者快速地掌握整個系統的結構和來龍去脈,這張圖已經初步展示了Jive的主要框架結構。

統一入口與單態模式
在上面ForumFactory的getInstance方法使用單態(SingleTon)模式。單態模式是保證一個類有且僅有一個對象實例,并提供一個訪問它的全局訪問點。

前面曾提到ForumFactory是Jive提供客戶端訪問數據庫系統的統一入口。為了保證所有的客戶端請求都要經過這個ForumFactory,如果不使用單態模式,客戶端下列調用語句表示生成了ForumFactory實例:
ForumFactory factory = new DbForumFactory();

客戶端每發生一次請求都調用這條語句,這就會發生每次都生成不同factory對象實例,這顯然不符合設計要求,因此必須使用單態模式。
一般在Java實現單態模式有幾種選擇,最常用而且安全的用法如下:

public class Singleton 
  
private Singleton(){}
  
//在自己內部定義自己一個實例,是不是很奇怪

  
//注意這是private,只供內部調用
  private static Singleton instance = new Singleton(); 
  
//這里提供了一個供外部訪問本class的靜態方法,可以直接訪問
  public static Singleton getInstance() 
    
return instance; 
  }
 
}

單態模式一共使用了兩條語句實現:第一條直接生成自己的對象,第二條提供一個方法供外部調用這個對象,同時最好將構造函數設置為private,以防止其他程序員直接使用new Singleton生成實例。
還有一種Java單態模式實現:

public class Singleton 
  
private Singleton(){}
  
private static Singleton instance = null
  
public static synchronized Singleton getInstance() 
    
if (instance==null)
      instance=
new Singleton() 
   
return instance; 
  }


在上面代碼中,使用了判斷語句。如果instance為空,再進行實例化,這成為lazy initialization。注意getInstance()方法的synchronized,這個synchronized很重要。如果沒有synchronized,那么使用getInstance()在第一次被訪問時有可能得到多個Singleton實例。

關于lazy initialization的Singleton有很多涉及double-checked locking (DCL)的討論,有興趣者可以進一步研究。一般認為第一種形式要更加安全些;但是后者可以用在類初始化時需要參數輸入的情況下。

在Jive的ForumFactory中采取了后者lazy initialization形式,這是為了能夠動態配置指定ForumFactory的具體子類。在getInstance中,從配置文件中獲得當前工廠的具體實現,如果需要啟動XmlForumFactory,就不必修改ForumFactory代碼,直接在配置文件中指定className的名字為XmlForumFactory。這樣通過下列動態裝載機制生成ForumFactory具體對象:


Class c = Class.forName(className);

factory = (ForumFactory)c.newInstance();

這是利用Java的反射機制,可以通過動態指定className的數值而達到生成對象的方式。

使用單態模式的目標是為了控制對象的創建,單態模式經常使用在控制資源的訪問上。例如數據庫連接或Socket連接等。單態模式可以控制在某個時刻只有一個線程訪問資源。由于Java中沒有全局變量的概念,因此使用單態模式有時可以起到這種作用,當然需要注意是在一個JVM中。
 

訪問控制與代理模式
仔細研究會發現,在ForumFactory的getInstance方法中最后的返回值有些奇怪。按照單態模式的概念應該直接返回factory這個對象實例,但是卻返回了ForumFactoryProxy的一個實例,這實際上改變了單態模式的初衷。這樣客戶端每次通過調用ForumFactory的getInstance返回的就不是ForumFactory的惟一實例,而是新的對象。之所以這樣做是為了訪問權限的控制,姑且不論這樣做的優劣,先看看什么是代理模式。代理模式是屬于設計模式結構型模式中一種,它是實際訪問對象的代理對象,或者影子對象,主要達到控制實際對象的訪問。這種控制的目的很多,例如提高性能等。即遠程代理模式,這種模式將在以后章節討論。

其中一個主要的控制目的是控制客戶端對實際對象的訪問權限。在Jive系統中,因為有角色權限的分別,對于Forum、ForumThread和FroumMessage的訪問操作必須經過權限機制驗證后才能進行。


以ForumFactoryProxy中的createForum方法為例,其實ForumFactoryProxy也是FroumFactory的一種工廠實現,它的createForum具體實現如下:

public Forum createForum(String name, String description)
            
throws UnauthorizedException, ForumAlreadyExistsException
    
{
        
if (permissions.get(ForumPermissions.SYSTEM_ADMIN)) {
            Forum newForum 
= factory.createForum(name, description);
            
return new ForumProxy(newForum, authorization, permissions);
        }

        
else {
            
throw new UnauthorizedException();
        }

}


在這個方法中進行了權限驗證,判斷是否屬于系統管理員。如果是,將直接從DbForumFactory對象factory的方法createForum中獲得一個新的Forum對象,然后再返回Forum的子類代理對象ForumProxy。因為在Forum中也還有很多屬性和操作方法,這些也需要進行權限驗證。ForumProxy和ForumFactoryProxy起到類似的作用。

Jive中有下列幾個代理類:

    ForumFactoryProxy:客戶端和DbForumFactory之間的代理。客戶端訪問DbForumFactory的任何方法都要先經過ForumFactoryProxy相應方法代理一次。以下意思相同。
    ForumProxy:客戶端和DbForum之間的代理,研究Forum對象的每個方法,必須先看ForumProxy對象的方法。
    ForumMessageProxy:客戶端和DbForumMessage之間的代理。
    ForumThreadProxy:客戶端和DbForumThread之間的代理。

User和Group也有相應的代理類。

由以上分析看出,每個數據對象都有一個代理。如果系統中數據對象非常多,依據這種一對一的代理關系,會有很多代理類,將使系統變得不是非常干凈,因此可以使用動態代理來代替這所有的代理類.


批量分頁查詢與迭代模式
迭代(Iterator)模式是提供一種順序訪問某個集合各個元素的方法,確保不暴露該集合的內部表現。迭代模式應用于對大量數據的訪問,Java Collection API中Iterator就是迭代模式的一種實現。

在前面章節已經討論過,用戶查詢大量數據,從數據庫不應該直接返回ResultSet,應該是Collection。但是有一個問題,如果這個數據很大,需要分頁面顯示。如果一下子將所有頁面要顯示的數據都查詢出來放在Collection,會影響性能。而使用迭代模式則不必將全部集合都展現出來,只有遍歷到某個元素時才會查詢數據庫獲得這個元素的數據。

以論壇中顯示帖子主題為例,在一個頁面中不可能顯示所有主題,只有分頁面顯示


一共分15頁來顯示所有論壇帖子,可以從顯示Forum.jsp中發現下列語句可以完成上述結果:

ResultFilter filter = new ResultFilter(); //設置結果過濾器
   filter.setStartIndex(start);             //設置開始點
   filter.setNumResults(range);          //設置范圍
   ForumThreadIterator threads = forum.threads(filter); //獲得迭代器
   while(threads.hasNext){
       
//逐個顯示threads中帖子主題,輸出圖3-5中的每一行 
   }


上述代碼中主要是從Forum的threads方法獲得迭代器ForumThreadIterator的實例,依據前面代理模式中分析、研究Forum對象的方法,首先是看ForumProxy中對應方法,然后再看DbForum中對應方法的具體實現。在ForumProxy中,threads方法如下:
public ForumThreadIterator threads(ResultFilter resultFilter) {
     ForumThreadIterator iterator 
= forum.threads(resultFilter);
     
return new ForumThreadIteratorProxy(iterator, authorization, permissions);
}


首先是調用了DbForum中具體的threads方法,再追蹤到DbForum中看看,它的threads方法代碼如下:
public ForumThreadIterator threads(ResultFilter resultFilter) {
//按resultFilter設置范圍要求獲得SQL查詢語句
   String query = getThreadListSQL(resultFilter, false); 
 
//獲得resultFilter設置范圍內的所有ThreadID集合
   long [] threadBlock = getThreadBlock(query.toString(), resultFilter.getStartIndex());
   
//以下是計算查詢區域的開始點和終點
   int startIndex = resultFilter.getStartIndex();
   
int endIndex;
   
// If number of results is set to inifinite, set endIndex to the total
// number of threads in the forum.
   if (resultFilter.getNumResults() == ResultFilter.NULL_INT) {
     endIndex 
= (int)getThreadCount(resultFilter);
    }
else {
endIndex 
= resultFilter.getNumResults() + startIndex; 
   }

   
return new ForumThreadBlockIterator(threadBlock, query.toString(),
               startIndex, endIndex, 
this.id, factory);
}


ResultFilter是一個查詢結果類,可以對論壇主題Thread和帖子內容Message進行過濾或排序,這樣就可以根據用戶要求定制特殊的查詢范圍。如查詢某個用戶去年在這個論壇發表的所有帖子,那只要創建一個ResultFilter對象就可以代表這個查詢要求。

在上面threads方法代碼中,第一步是先定制出相應的動態SQL查詢語句,然后使用這個查詢語句查詢數據庫,獲得查詢范圍內所有的ForumThread的ID集合,然后在這個ID集合中獲得當前頁面的ID子集合,這是非常關鍵的一步。

在這關鍵的一步中,有兩個重要的方法getThreadListSQL和getThreadBlock:

    GetThreadListSQL:獲得SQL查詢語句query的值,這個方法Jive實現起來顯得非常地瑣碎。

    GetThreadBlock:獲得當前頁面的ID子集合,那么如何確定ID子集合的開始位置呢?查看getThreadBlock方法代碼,可以發現,它是使用最普遍的ResultSet next()方法來逐個跳躍到開始位置。

上面代碼的Threads方法中最后返回的是ForumThreadBlockIterator,它是抽象類ForumThreadIterator的子類,而ForumThreadIterator繼承了Collection的Iterator,以此聲明自己是一個迭代器,ForumMessageBlockIterator實現的具體方法如下:

public boolean hasNext();     //判斷是否有下一個元素

public boolean hasPrevious() //判斷是否有前一個元素

public Object next() throws java.util.NoSuchElementException //獲得下一個元素實例

ForumThreadBlockIterator中的Block是“頁”的意思,它的一個主要類變量threadBlock包含的是一個頁面中所有ForumThread的ID,next()方法實際是對threadBlock中ForumThread進行遍歷,如果這個頁面全部遍歷完成,將再獲取下一頁(Block)數據。

在ForumThreadBlockIterator重要方法getElement中實現了兩個功能:

       如果當前遍歷指針超過當前頁面,將使用getThreadBlock獲得下一個頁面的ID子集合;

       如果當前遍歷指針在當前頁面之內,根據ID獲得完整的數據對象,實現輸出;



ForumThreadBlockIterator的getElement方法代碼如下:
private Object getElement(int index) {
   
if (index < 0{        return null;        }
   
// 檢查所要獲得的 element 是否在本查詢范圍內(當前頁面內)
   if (index < blockStart || 
index 
>= blockStart + DbForum.THREAD_BLOCK_SIZE) {   
     
try {
          
//從緩沖中獲得Forum實例
          DbForum forum = factory.cacheManager.forumCache.get(forumID);
          
//獲得下一頁的內容
          this.threadBlock = forum.getThreadBlock(query, index);
          
this.blockID = index / DbForum.THREAD_BLOCK_SIZE; 
this.blockStart = blockID * DbForum.THREAD_BLOCK_SIZE;
      }
 catch (ForumNotFoundException fnfe) {
               
return null;
       }

     }

     Object element 
= null;
     
// 計算這個元素在當前查詢范圍內的相對位置
     int relativeIndex = index % DbForum.THREAD_BLOCK_SIZE;
     
// Make sure index isn't too large
     if (relativeIndex < threadBlock.length) {
        
try {
            
// 從緩沖中獲得實際thread 對象
            element = factory.cacheManager.threadCache.get(
                         threadBlock[relativeIndex]);
        }
 catch (ForumThreadNotFoundException tnfe) { }
     }

     
return element;
}




ForumThreadBlockIterator是真正實現分頁查詢的核心功能,ForumThreadBlockIterator對象返回到客戶端的過程中,遭遇ForumThreadIteratorProxy的截獲,可以回頭看看ForumProxy中的threads方法,它最終返回給調用客戶端Forum.jsp的是ForumThreadIteratorProxy實例。

ForumThreadIteratorProxy也是迭代器ForumThreadIterator的一個子類,它的一個具體方法中:


public Object next() {

return new ForumThreadProxy((ForumThread)iterator.next(), authorization,
            permissions);
}


這一句是返回一個ForumThreadProxy實例,返回就是一個ForumThread實例的代理。這里,Jive使用代理模式實現訪問控制實現得不是很巧妙,似乎有代理到處“飛”的感覺,這是可以對之進行改造的。

從以上可以看出,Jive在輸出如圖3-5所示的多頁查詢結果時,采取了下列步驟:

(1)先查詢出符合查詢條件的所有對象元素的ID集合,注意不是所有對象元素,只是其ID的集合,這樣節約了大量內存
(2)每個頁面視為一個Block,每當進入下一頁時,獲得下一個頁面的所有對象的ID集合。
(3)輸出當前頁面的所有對象時,首先從緩沖中獲取,如果緩沖中沒有,再根據ID從數據庫中獲取完整的對象數據。

上述實現方法完全基于即查即顯,相比于一般批量查詢做法:一次性獲得所有數據,然后遍歷數據結果集ResultSet,Jive這種批量查詢方式是一種比較理想的選擇。

以上是ForumThread的批量顯示,有關帖子內容ForumMessage也是采取類似做法。在每個ForumThread中可能有很多帖子內容(ForumMessage對象集合),也不能在一個頁面中全部顯示,所以也是使用迭代模式來實現的。顯示一個Forum主題下所有帖子內容的功能由ForumThread的messages()方法完成,檢查它的代理類FroumThreadProxy如何具體完成:

public Iterator messages(ResultFilter resultFilter) {
   Iterator iterator 
= thread.messages(resultFilter);
   
return new IteratorProxy(JiveGlobals.MESSAGE, iterator, authorization, permissions); 
}


實現的原理基本相同,返回的都是一個Iterator代理類,在這些代理類中都是進行用戶權限檢驗的。

Jive中也有關于一次性獲得所有數據,然后遍歷ResultSet的做法。這種做法主要適合一次性查詢數據庫的所有數據,例如查詢當前所有論壇Forum,首先實現SQL語句:

SELECT forumID FROM jiveForum

獲得所有Forum的forumID,這段代碼位于DbForumFactory.java的forums方法中,如下:


public Iterator forums() {
    
if (forums == null{
      LongList forumList 
= new LongList();
      Connection con 
= null;
      PreparedStatement pstmt 
= null;
      
try {
        con 
= ConnectionManager.getConnection();
        
// GET_FORUMS值是SELECT forumID FROM jiveForum
        pstmt = con.prepareStatement(GET_FORUMS);
        ResultSet rs 
= pstmt.executeQuery();
       
while (rs.next()) {
          forumList.add(rs.getLong(
1));                 //將所有查詢ID結果放入forumList中
        }

      }
catch (SQLException sqle) {
        sqle.printStackTrace();
      }
 finally {
        …
    }

    
return new DatabaseObjectIterator(JiveGlobals.FORUM, forums, this);
}


forums方法是返回一個DatabaseObjectIterator,這個DatabaseObjectIterator也是一個迭代器,但是實現原理要比ForumThreadBlockIterator簡單。它只提供了一個遍歷指針,在所有ID結果集中遍歷,然后也是通過ID獲得完整的數據對象。

總之,Jive中關于批量查詢有兩種實現方式:以ForumThreadBlockIterator為代表的實現方式適合在數據量巨大、需要多頁查詢時使用;而DatabaseObjectIterator則是推薦在一個頁面中顯示少量數據時使用。