Jive论坛系统
Jive是基于Web结构的一套论坛系统。Jive的早期版本是基于开放源代码开发的,由于其出色的代码设计水平以及优越的性能,被广泛地应用在很多网站系统中。市场的成功促使Jive从2.1版本以后不再开放其源代码。因此,本章讨论的Jive论坛系统是基于Jive 1.2.4 Yazd(http://yazd.yasna.com/)的修改版Jdon论坛。
Java系统开发学习的一个不可忽视的步骤就是研读优秀的源码,从中汲取前人探索的结晶,从而不断提高自己的设计和编程水平。本章主要是从设计模式角度来剖析Jive论坛系统,既能充分理解Jive论坛的源码,又能学习设计模式在Java系统中的使用
1 Jive功能需求
Jive功能需求分析类似于一个新系统的需求分析。只有了解Jive系统实现了哪些论坛功能,才能进一步研究和学习它是怎样巧妙、优雅地实现这些功能的。
论坛系统是网络交流的一种主要互动功能系统,如图3-1所示。通过论坛系统,用户可以共同就某个话题不断进行讨论,通过发贴功能发布新的话题,通过回贴功能回复别人的话题。Jive论坛系统可以允许管理员动态地创建新的论坛、编辑论坛的内容、设置论坛过滤信息以及管理注册用户等。
图3-1 Jive用例图
在Jive论坛系统中,用户角色和权限是紧密联系在一起的。主要分两大角色:普通用户和管理员,具体的表现形式是通过权限组合来体现的。管理方面的权限有:
·
SYSTEM_ADMIN,系统管理员,可以管理整个系统。
·
FORUM_ADMIN,论坛管理员,可以管理某个特定的论坛。
·
USER_ADMIN和GROUP_ADMIN,用户和组管理员,可以管理一些特定用户和用户组。
论坛的读写权限包括:读权限,创建一个新主题,创建一个新的帖子等。
Jive中没有明确定义普通用户和管理员角色,而是直接通过以上权限组合和具体用户直接建立联系,并将这种直接联系保存到数据库中。
在权限不是很复杂的情况下,这种没有引入角色的做法比较简单直接。但由于用户和权限直接挂钩,而用户和权限都可能在不断地动态变化,那么它们之间由于联系太直接和紧密,对各自变化形成了限制。所以,对于复杂的权限系统,引入了基于角色的权限系统,这将在以后章节中进一步讨论。
Jive论坛业务对象主要分为Forum、ForumThread和ForumMessage,它们之间的关系如图3-2所示。
每个论坛Forum包含一系列ForumThread(主题),而每个主题都是由很多内容帖子ForumMessage组成的,这是一个聚集关系。这3种对象中每一个对象都涉及到对象数据的创建、编辑、查询和删除,这些对象数据分别保存在数据库中。这3个对象对于不同的角色可操作访问权限是不一样的,只有系统管理员和论坛管理员可以对Forum相关数据实行操作,普通用户可以创建或编辑ForumThread和ForumMessage。
Jive论坛为了实现不同用户对不同基本对象的不同操作权限,通过设定一个统一的入口,在这个入口将检查客户端每次对数据的操作权限,如图3-3所示。
图3-2 基本对象关系图
图3-3 入口示意图
客户端每次对数据库的操作,都要经过ForumFactory入口进入。在ForumFactory中会动态生成一个访问控制代理ForumFactoryProxy,通过ForumFactoryProxy检查客户端访问方法是否符合整体权限访问控制要求。
下面将从ForumFactory作为Jive论坛系统分析入手,结合设计模式逐步分解论坛功能的具体实现。
2 Jive与设计模式
Jive论坛系统使用大量设计模式巧妙地实现了一系列功能。因为设计模式的通用性和可理解性,将帮助更多人很快地理解 Jive论坛源码,从而可以依据一种“协定”来动态地扩展它。那么使用设计模式还有哪些好处?
2.1 设计模式
设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。毫无疑问,设计模式于己于他人于系统都是多赢的。设计模式使代码编制真正工程化,设计模式是软件工程的基石。
GOF(设计模式作者简称)《设计模式》这本书第一次将设计模式提升到理论高度,并将之规范化,该书提出了23种基本设计模式。自此,在可复用面向对象软件的发展过程中,新的大量的设计模式不断出现。
很多人都知道Java是完全面向对象的设计和编程语言,但是由于接受教育以及经验的原因,大多数程序员或设计人员都是从传统的过程语言转变而来,因此在思维习惯上要完全转变为面向对象的设计和开发方式是困难的,而学习设计模式可以更好地帮助和坚固这种转变。
凡是学习完成设计模式的人都有一种类似重生的感觉,这种重生可以从很多方面去解释。换一种新的角度来看待和解决问题应该是一种比较贴切的解释,而这种新的思维角度培养属于基础培训,因此,设计模式是学习Java的必读基础课程之一。
由于设计模式概念比较抽象,对于初学者学习有一定的难度,因此结合Jive论坛系统学习设计模式将是一种很好的选择。
掌握了设计模式,将会帮助程序员或设计人员以更加可重用性、可伸缩性的眼光来开发应用系统,甚至开发通用的框架系统。框架系统是构成一类特定软件可复用设计的一组相互协作的类,主要是对应用系统中反复重用部分的提炼,类似一种模板,这是一种结构性的模板。
框架通常定义了应用体系的整体结构、类和对象的关系等设计参数,以便于具体应用实现者能集中精力于应用本身的特定细节。框架强调设计复用,而设计模式最小的可重用单位,因此框架不可避免地会反复使用到设计模式。关于通用框架系统的设计开发将在以后章节中讨论。
其实Jive论坛本身也形成了一个基于Web结构的通用框架系统,因为它很多设计思想是可以重用的,例如设定一个总体入口,通过入口检查用户的访问控制权限,当然还有其他各方面的功能实现方式都是值得在其他系统中借鉴的,也正因为它以模式的形式表现出来,这种可重用性和可借鉴性就更强。
2.2 ForumFactory与工厂模式
工厂模式是GOF设计模式的主要常用模式,它主要是为创建对象提供了一种接口,工厂模式主要是封装了创建对象的细节过程,从而使得外界调用一个对象时,根本无需关心这个对象是如何产生的。
在GOF设计模式中,工厂模式分为工厂方法模式和抽象工厂模式。两者主要区别是,工厂方法是创建一种产品接口下的产品对象,而抽象工厂模式是创建多种产品接口下的产品对象,非常类似Builder生成器模式。在平时实践中,使用较多的基本是工厂方法模式。
以类SampleOne为例,要创建SampleOne的对象实例:
SampleOne
sampleOne = new SampleOne();
如果Sample类有几个相近的类:SampleTwo或SampleThree,那么创建它们的实例分别是:
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中根据具体情况来选择返回SampleOne、SampleTwo或SampleThree。所谓具体情况有很多种:上下文其他过程计算结果;直接根据配置文件中配置。
上述工厂方法模式中涉及到一个抽象产品接口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的具体对象实例
public abstract ForumMessage createMessage();
....
}
ForumFactory中提供了很多抽象方法如createForum、createThread和createMessage()等,它们是创建各自产品接口下的具体对象,这3个接口就是前面分析的基本业务对象Forum、ForumThread和ForumMessage,这些创建方法在ForumFactory中却不立即执行,而是推迟到ForumFactory子类中实现。
ForumFactory的子类实现是com.Yasna.forum.database.DbForumFactory,这是一种数据库实现方式。即在DbForumFactory中分别实现了在数据库中createForum、createThread和createMessage()等3种方法,当然也提供了动态扩展到另外一套系列产品的生产方案的可能。如果使用XML来实现,那么可以编制一个XmlForumFactory的具体工厂子类来分别实现3种创建方法。
因此,Jive论坛在统一入口处使用了抽象工厂模式来动态地创建论坛中所需要的各种产品,如图3-4所示。
图3-4 ForumFactory抽象工厂模式图
图3-4中,XmlForumFactory和DbForumFactory作为抽象工厂ForumFactory的两个具体实现,而Forum、ForumThread和ForumMessage分别作为3个系列抽象产品接口,依靠不同的工厂实现方式,会产生不同的产品对象。
从抽象工厂模式去理解Jive论坛统一入口处,可以一步到位掌握了几个类之间的大概关系。因为使用了抽象工厂模式这种通用的设计模式,可以方便源码阅读者快速地掌握整个系统的结构和来龙去脉,图3-4这张图已经初步展示了Jive的主要框架结构。
细心的读者也许会发现,在上面ForumFactory有一个getInstance比较令人费解,这将在2.3节进行讨论。
2.3 统一入口与单态模式
在上面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中。
2.4 访问控制与代理模式
仔细研究会发现,在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也有相应的代理类。
由以上分析看出,每个数据对象都有一个代理。如果系统中数据对象非常多,依据这种一对一的代理关系,会有很多代理类,将使系统变得不是非常干净,因此可以使用动态代理来代替这所有的代理类,具体实现将在以后章节讨论。
2.5 批量分页查询与迭代模式
迭代(Iterator)模式是提供一种顺序访问某个集合各个元素的方法,确保不暴露该集合的内部表现。迭代模式应用于对大量数据的访问,Java Collection API中Iterator就是迭代模式的一种实现。
在前面章节已经讨论过,用户查询大量数据,从数据库不应该直接返回ResultSet,应该是Collection。但是有一个问题,如果这个数据很大,需要分页面显示。如果一下子将所有页面要显示的数据都查询出来放在Collection,会影响性能。而使用迭代模式则不必将全部集合都展现出来,只有遍历到某个元素时才会查询数据库获得这个元素的数据。
以论坛中显示帖子主题为例,在一个页面中不可能显示所有主题,只有分页面显示,如图3-5所示。
图3-5中一共分15页来显示所有论坛帖子,可以从显示Forum.jsp中发现下列语句可以完成上述结果:
ResultFilter filter = new ResultFilter(); //设置结果过滤器
filter.setStartIndex(start);
//设置开始点
filter.setNumResults(range);
//设置范围
ForumThreadIterator threads = forum.threads(filter); //获得迭代器
while(threads.hasNext){
//逐个显示threads中帖子主题,输出图3-5中的每一行
}
图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则是推荐在一个页面中显示少量数据时使用。
2.6 过滤器与装饰模式
装饰(Decorator)模式是动态给一个对象添加一些额外的职责,或者说改变这个对象的一些行为。这就类似于使用油漆为某个东西刷上油漆,在原来的对象表面增加了一层外衣。
在装饰模式中,有两个主要角色:一个是被刷油漆的对象(decoratee);另外一个是给decoratee刷油漆的对象(decorator)。这两个对象都继承同一个接口。
首先举一个简单例子来说明什么是装饰模式。
先创建一个接口:
public
interface Work
{
public
void insert();
}
这是一种打桩工作的抽象接口,动作insert表示插入,那么插入什么?下面这个实现表示方形木桩的插入:
public
class SquarePeg implements Work{
public
void insert(){
System.out.println("方形桩插入");
}
}
本来这样也许就可以满足打桩的工作需要,但是有可能土质很硬,在插入方形桩之前先要打一个洞,那么又将如何实现?可以编制一个Decorator类,同样继承Work接口,但是在实现insert方法时有些特别:
public
class Decorator implements Work{
private
Work work;
//额外增加的功能被打包在这个List中
private
ArrayList others = new ArrayList();
public
Decorator(Work work)
{
this.work=work;
others.add("打洞"); //准备好额外的功能
}
public
void insert(){
otherMethod();
work.insert();
}
public
void otherMethod()
{
ListIterator
listIterator = others.listIterator();
while
(listIterator.hasNext())
{
System.out.println(((String)(listIterator.next()))
+ " 正在进行");
}
}
}
在Decorator的方法insert中先执行otherMethod()方法,然后才实现SquarePeg的insert方法。油漆工Decorator给被油漆者SquarePeg添加了新的行为——打洞。具体客户端调用如下:
Work
squarePeg = new SquarePeg();
Work
decorator = new Decorator(squarePeg);
decorator.insert();
本例中只添加了一个新的行为(打洞),如果还有很多类似的行为,那么使用装饰模式的优点就体现出来了。因为可以通过另外一个角度(如组织新的油漆工实现子类)来对这些行为进行混合和匹配,这样就不必为每个行为创建一个类,从而减少了系统的复杂性。
使用装饰模式可以避免在被油漆对象decoratee中包装很多动态的,可能需要也可能不需要的功能,只要在系统真正运行时,通过油漆工decorator来检查那些需要加载的功能,实行动态加载。
Jive论坛实现了信息过滤功能。例如可以将帖子内容中的HTML语句过滤掉;可以将帖子内容中Java代码以特别格式显示等。这些过滤功能有很多,在实际使用时不一定都需要,是由实际情况选择的。例如有的论坛就不需要将帖子内容的HTML语句过滤掉,选择哪些过滤功能是由论坛管理者具体动态决定的。而且新的过滤功能可能随时可以定制开发出来,如果试图强行建立一种接口包含所有过滤行为,那么到时有新过滤功能加入时,还需要改变接口代码,真是一种危险的行为。
装饰模式可以解决这种运行时需要动态增加功能的问题,且看看Jive是如何实现的。
前面讨论过,在Jive中,有主要几个对象ForumFactory、Forum以及ForumThread和ForumMessage,它们之间的关系如图3-2所示。因此帖子内容ForumMessage对象的获得是从其上级FroumThread的方法getMessage中获取,但是在实际代码中,ForumThread的方法getMessage委托ForumFactory来获取ForumMessage对象。看看ForumThread的子类DbForumThread的getMessage代码:
public
ForumMessage getMessage(long messageID)
throws ForumMessageNotFoundException
{
return factory.getMessage(messageID, this.id, forumID);
}
这是一种奇怪的委托,大概是因为需要考虑到过滤器功能有意为之吧。那就看看ForumFactory的具体实现子类DbForumFactory的getMessage功能,getMessage是将数据库中的ForumMessage对象经由过滤器过滤一遍后输出(注:因为原来的Jive的getMessage代码考虑到可缓存或不可缓存的过滤,比较复杂,实际过滤功能都是可以缓存的,因此精简如下)。
protected
ForumMessage getMessage(long messageID, long threadID, long forumID)
throws ForumMessageNotFoundException
{
DbForumMessage message =
cacheManager.messageCache.get(messageID);
// Do a security check to make sure the message comes from the thread.
if (message.threadID != threadID) {
throw new ForumMessageNotFoundException();
}
ForumMessage filterMessage = null;
try {
// 应用全局过滤器
filterMessage = filterManager.applyFilters(message);
Forum forum =
getForum(forumID);
//应用本论坛过滤器
filterMessage = forum.getFilterManager().applyFilters(filterMessage);
}
catch (Exception e) { }
return filterMessage;
}
上面代码实际是装饰模式的客户端调用代码,DbForumMessage
的实例message是被油漆者decoratee。通过filterManager 或forum.getFilterManager()的applyFilter方法,将message实行了所有的过滤功能。这就类似前面示例的下列语句:
Work
decorator = new Decorator(squarePeg);
forum.getFilterManager()是从数据库中获取当前配置的所有过滤器类。每个Forum都有一套自己的过滤器类,这是通过下列语句实现的:
FilterManager
filterManager = new DbFilterManager();
在DbFilterManager 的类变量ForumMessageFilter [] filters中保存着所有的过滤器,applyFilters方法实行过滤如下:
public
ForumMessage applyFilters(ForumMessage message) {
for (int i=0; i < filters.length; i++) {
if (filters[i] != null) {
message = filters[i].clone(message);
}
}
return message;
}
而ForumMessageFilter是ForumMessage的另外一个子类,被油漆者DbForumMessage通过油漆工ForumMessageFilter增加了一些新的行为和功能(过滤),如图3-6所示。
图3-6 装饰模式
这就组成了一个稍微复杂一点的装饰模式。HTMLFilter实现了HTML代码过滤功能,而JavaCodeHighLighter实现了Java代码过滤功能,HTMLFilter代码如下:
public
class HTMLFilter extends ForumMessageFilter {
public ForumMessageFilter clone(ForumMessage message){
HTMLFilter filter = new HTMLFilter();
filter.message = message;
return filter;
}
public boolean isCacheable() {
return true;
}
public String getSubject() {
return StringUtils.escapeHTMLTags(message.getSubject());
}
public String getBody() {
return StringUtils.escapeHTMLTags(message.getBody());
}
}
HTMLFilter中重载了ForumMessage的getSubject()、getBody()方法,实际是改变了这两个原来的行为,这类似前面举例的方法:
public
void insert(){
otherMethod();
work.insert();
}
这两者都改变了被油漆者的行为。
在HTMLFilter中还使用了原型(Prototype)模式,原型模式定义是:用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。按照这种定义,Java的clone技术应该是原型模式的一个实现。
HTMLFilter的clone方法实际就是在当前HTMLFilter实例中再生成一个同样的实例。这样在处理多个并发请求时,不用通过同一个过滤器实例进行处理,提高了性能。但是HTMLFilter的clone方法是采取new方法来实现,不如直接使用Object的native方法速度快。
因为在DbFilterManager中是根据配置使用类反射机制动态分别生成包括HTMLFilter在内的过滤器实例。但是每种过滤器实例只有一个,为了使得大量用户不必争夺一个过滤器实例来实现过滤,就采取了克隆方式,这种实战手法可以借鉴在自己的应用系统中。
2.7 主题监测与观察者模式
观察者(Observer)模式是定义对象之间一对多的依赖关系,当一个被观察的对象发生改变时,所有依赖于它的对象都会得到通知并采取相应行为。
使用观察者模式的优点是将被观察者和观察者解耦,从而可以不影响被观察者继续自己的行为动作。观察者模式适合应用于一些“事件触发”场合。
在Jive中,用户也许会对某个主题感兴趣,希望关于此主题发生的任何新的讨论能通过电子邮件通知他,因此他订阅监视了这个主题。因为这个功能的实现会引入电子邮件的发送。在前面章节已经讨论了电子邮件发送有可能因为网络原因延迟,如果在有人回复这个主题时,立即进行电子邮件发送,通知所有订阅该主题的用户。那么该用户可能等待很长时间得不到正常回应。
使用观察者模式,可以通过触发一个观察者,由观察者通过另外线程来实施邮件发送,而被观察者发出触发通知后,可以继续自己原来的逻辑行为。
看看Jive的WatchManager类:
public
interface WatchManager {
//正常监察类型,用户在这个主题更新后再次访问时,会明显地发现
public static final int NORMAL_WATCH = 0;
// 当主题变化时,通过电子邮件通知用户
public static final int EMAIL_NOTIFY_WATCH = 1;
//设置一个主题被观察的时间,默认为30天
public void setDeleteDays(int deleteDays) throws UnauthorizedException;
public int getDeleteDays();
//是否激活了E-mail提醒
public boolean isEmailNotifyEnabled() throws UnauthorizedException;
public void setEmailNotifyEnabled(boolean enabled) throws
UnauthorizedException;
//保存E-mail的内容
public String getEmailBody() throws UnauthorizedException;
public void setEmailBody(String body) throws UnauthorizedException;
//保存E-mail的主题
public String getEmailSubject() throws UnauthorizedException;
public
void setEmailSubject(String subject) throws UnauthorizedException;
…
//为某个主题创建一个观察者
public void createWatch(User user, ForumThread thread, int watchType)
throws UnauthorizedException;
//删除某个主题的观察者
public void deleteWatch(User user, ForumThread thread, int watchType)
//得到一个主题的所有观察者
public Iterator getWatchedForumThreads(User user, int watchType)
throws UnauthorizedException;
//判断一个用户是否在观察监视该主题
public boolean isWatchedThread(User user, ForumThread thread, int watchType)
throws UnauthorizedException;
…
}
DbWatchManager是WatchManager的一个子类,通过数据库保存着有关某个主题被哪些用户监视等数据资料。WatchManager对象是随同DbForumFactory()一起生成的。
在DbWatchManager中有一个WatchManager没有的很重要的方法——通知方法:
protected
void notifyWatches(ForumThread thread) {
//If watches are turned on.
if (!emailNotifyEnabled) {
return;
}
//通知所有观察这个主题的用户
EmailWatchUpdateTask task = new EmailWatchUpdateTask(this, factory, thread);
TaskEngine.addTask(task);
}
这个方法用来触发所有有关这个主题的监视或订阅用户,以E-mail发送提醒他们。那么这个通知方法本身又是如何被触发的?从功能上分析,应该是在发表新帖子时触发。
在DbForumThread的addMessage的最后一行有一句:
factory.watchManager.notifyWatches(this);
这其实是调用了DbWatchManager的notifyWatches方法,因此确实是在增加新帖子时触发了该帖子的所有观察者。
notifyWatches方法中在执行E-mail通知用户时,使用了TaskEngine来执行E-mail发送。E-mailWatchUpdateTask是一个线程类,而TaskEngine是线程任务管理器,专门按要求启动如E-mailWatchUpdateTask这样的任务线程。其实TaskEngine是一个简单的线程池,它不断通过查询Queue是否有可运行的线程,如果有就直接运行线程。
public
class TaskEngine {
//任务列表
private static LinkedList taskList = null;
//工作数组
private static Thread[] workers = null;
private static Timer taskTimer = null;
private static Object lock = new Object();
static {
//根据配置文件初始化任务启动时间
taskTimer = new Timer(true);
// 默认使用7个线程来装载启动任务
workers = new Thread[7];
taskList = new LinkedList();
for (int i=0; i<workers.length; i++) {
// TaskEngineWorker是个简单的线程类
TaskEngineWorker worker = new TaskEngineWorker();
workers[i] = new Thread(worker);
workers[i].setDaemon(true);
workers[i].start(); //启动TaskEngineWorker这个线程
}
}
//TaskEngineWorker内部类
private static class TaskEngineWorker implements Runnable {
private boolean done = false;
public void run() {
while (!done) {
//运行nextTask方法
nextTask().run();
}
}
}
// nextTask()返回的是一个可运行线程,是任务列表Queue的一个读取者
private static Runnable nextTask() {
synchronized(lock) {
// 如果没有任务,就锁定在这里
while (taskList.isEmpty()) {
try {
lock.wait(); //等待解锁
} catch (InterruptedException ie) { }
}
//从任务列表中取出第一个任务线程
return (Runnable)taskList.removeLast();
}
}
public static void addTask(Runnable r) {
addTask(r, Thread.NORM_PRIORITY);
}
//这是任务列表Queue的生产者
public static void addTask(Runnable task, int priority) {
synchronized(lock) {
taskList.addFirst(task);
//提醒所有锁在lock这里的线程可以运行了
//这是线程的互相通知机制,可参考线程参考资料
lock.notifyAll();
}
}
…
}
在TaskEngine中启动设置了一个消息管道Queue和两个线程。一个线程是负责向Queue里放入Object,可谓是消息的生产者;而另外一个线程负责从Queue中取出Object,如果Queue中没有Object,那它就锁定(Block)在那里,直到Queue中有Object,因为这些Object本身也是线程,因此它取出后就直接运行它们。
这个TaskEngine建立的模型非常类似JMS(Java消息系统),虽然它们功能类似,但不同的是:JMS是一个分布式消息发布机制,可以在多台服务器上运行,处理能力要强大得多。而TaskEngine由于基于线程基础,因此不能跨JVM实现。可以说TaskEngine是一个微观组件,而JMS则是一个宏观架构系统。JMS相关讨论将在后面章节进行。
以上讨论了Jive系统中观察者模式的实现,Jive使用线程比较基础的概念实现了观察者模式,当然有助于了解J2EE很多底层的基础知识,整个Web容器的技术实现就是基于线程池原理建立的。
Java的JDK则提供了比较方便的观察者模式API——java.util.Observable和java.util.Observer,它们的用户非常简单,只要被观察者继承Observable,然后使用下列语句设置观察点:
setChanged();
notifyObservers(name);
//一旦执行本代码,就触发观察者了
而观察者只要实现Observer接口,并实现update方法,在update方法中将被观察者触发后传来的object进行处理。举例如下:
网上商店中商品价格可能发生变化,如果需要在价格变化时,首页能够自动显示这些降价产品,那么使用观察者模式将方便得多。首先,商品是一个被观察者:
public
class product extends Observable{
private
float price;
public
float getPrice(){ return price;}
public
void setPrice(){
this.price=price;
//商品价格发生变化,触发观察者
setChanged();
notifyObservers(new Float(price));
}
...
}
价格观察者实现observer接口:
public
class PriceObserver implements Observer{
private
float price=0;
public
void update(Observable obj,Object arg){
if
(arg instanceof Float){
price=((Float)arg).floatValue();
System.out.println("PriceObserver :price changet to "+price);
}
}
}
这样,一个简单的观察者模式就很容易地实现了。
3 Jive安全管理机制
在Jive中除了前面介绍的有关设计模式实现组件外,还有其他有一定特点的组件功能,分析研究这些组件功能可以更加完整透彻地理解Jive论坛系统。
Jive安全管理机制基本是由下列部分组成:
·
安全验证机制。主要是验证用户名和密码组合是否与数据库中注册时的数据一致,以确认该用户身份为注册用户。这是对所有的JSP访问都进行拦截访问。
·
访问权限控制(ACL)。对不同的数据不同用户拥有不同的访问权限,例如,一个帖子普通用户可以浏览,但是不能更该;但是管理员却可以编辑删除。这部分功能是通过代理模式实现,为每个关键数据都建立一个代理类用来实现访问权限检查,这在前面讨论过。
·
用户资料管理系统。主要是管理用户的资料数据,进行用户组和用户关系的建立等。
1 安全验证机制
Jive的安全验证机制是按照比较通用的思路设计的。类似前面“简单的用户注册管理系统”中的介绍,Jive也是在所有的JSP页面中include一个安全检验功能的global.jsp。由于global.jsp是在每个JSP一开始必须执行的功能,因此通过拦截global.jsp拦截发往各个JSP页面的请求(request)。如果这个请求是合法的,将被允许通过;如果不是,将注明请求者身份是Anonymous(匿名者)。
global.jsp代码如下:
boolean
isGuest = false;
Authorization
authToken = SkinUtils.getUserAuthorization(request, response);
if
(authToken == null) {//未被验证通过
authToken = AuthorizationFactory.getAnonymousAuthorization();
isGuest=true;
}
在Jive中,以Authorization对象作为验证通过的标志,它的接口代码如下:
public
interface Authorization {
public long getUserID();
public boolean isAnonymous();
}
具体实现是DbAuthorization,代码如下:
public
final class DbAuthorization implements Authorization, Serializable {
private long userID;
protected DbAuthorization(long userID) {
this.userID = userID;
}
public long getUserID() {
return userID;
}
public boolean isAnonymous() {
return userID == -1;
}
}
此类只是一个userID,因此只是一个象征性的标志。
SkinUtils是一个为JSP服务的类,它的getUserAuthorization代码如下:
public
static Authorization getUserAuthorization
(HttpServletRequest request, HttpServletResponse response)
{
HttpSession session = request.getSession();
// 从HttpSession中获取Authorization实例
Authorization authToken =
(Authorization)session.getAttribute(JIVE_AUTH_TOKEN);
if (authToken != null) { return authToken; }
// 如果HttpSession中没有,检查用户浏览器cookie
Cookie cookie = getCookie(request, JIVE_AUTOLOGIN_COOKIE);
if (cookie != null) {
try {
String[] values = decodePasswordCookie(cookie.getValue());
String username = values[0];
String password = values[1];
//从cookie中获得用户名和密码后,进行安全验证
authToken = AuthorizationFactory.getAuthorization(username,password);
}catch (Exception e) {}
// put that token in the user's session:
if (authToken != null) {//如果通过验证,保存authToken在http Session
session.setAttribute(JIVE_AUTH_TOKEN, authToken);
}
// return the authorization token
return authToken;
}
return null;
}
用户验证预先通过两个步骤。首先检查HttpSession中是否保存了该用户的验证信息,如果用户第一次验证通过,反复访问,这道关口检查就可以通过。
如果HttpSession中没有验证信息,那么从该用户的浏览器cookie中寻找用户名和密码。如果该用户激活了cookie保存这些登录信息,那么应该可以找到用户名和密码,这样就省却了用户再次从键盘输入用户名和密码,将用户名和密码通过下列语句进行数据库验证:
authToken
= AuthorizationFactory.getAuthorization(username,password);
这一举是验证关键。AuthorizationFactory是一个抽象类,定义了Jive安全验证机制所需的所有方法,AuthorizationFactory的实现类似前面讨论的ForumFactory实现,是使用工厂模式加动态类反射机制完成的,代码如下:
public
abstract class AuthorizationFactory {
//定义一个数据库具体实现
private static String className =
" com.Yasna.forum.database.DbAuthorizationFactory";
private static AuthorizationFactory factory = null;
//验证方法如果没有UnauthorizedException抛出,表示验证通过
public static Authorization getAuthorization(String username,
String password) throws UnauthorizedException
{
loadAuthorizationFactory();
return factory.createAuthorization(username, password);
}
//匿名者处理方法
public static Authorization getAnonymousAuthorization() {
loadAuthorizationFactory();
return factory.createAnonymousAuthorization();
}
//需要具体实现的抽象方法
protected abstract Authorization createAuthorization(String username,
String password) throws UnauthorizedException;
protected abstract Authorization createAnonymousAuthorization();
//动态配置AuthorizationFactory的具体实现,可以在配置文件中定义一个
//基于LDAP的实现。类似ForumFactory的getInstance方法
private static void loadAuthorizationFactory() {
…
}
}
AuthorizationFactory看上去很复杂,实际只有一个核心方法getAuthorization。实现用户名和密码的验证。如果无法通过验证,有两个信息实现显示:一个是抛出UnauthorizedException,另外一个是返回空的Authorization对象。
那么,子类DbAuthorizationFactory毫无疑问就是查询数据库,将输入的用户名和密码与数据库保存的用户名和密码进行校验。
Jive的安全验证机制比较简单易懂,值得在实践中学习借鉴。但是注意到这套安全验证机制只是Web层的“手工”验证,资源访问权限(ACL)也是自己“手工”来实现的。如果使用EJB技术,因为EJB容器本身有一定的资源访问控制体系,因此在Web层验证通过后,需要将这些登录信息传递到EJB层。当然如果直接使用Web容器的安全验证机制,那么Web层与EJB层之间的登录信息传递将由容器实现,这样就更加简单方便。
Jive这种的安全验证并不是使用Web容器的安全验证机制,如何使用Web容器的安全验证机制将在以后章节介绍。尽管如此,Jive这套安全验证机制对付小型系统的应用也是足够的。
2 用户资料管理
在Jive中,用户User对象的操作访问类似于论坛Forum对象的访问,与User对象有关的操作都封装在一个类中操作,这是外观(Facade)模式的应用。
在Jive中,用户资料管理属于大系统中的一个子系统,在这个子系统中,用户子系统和其他系统又有一定的关系,涉及的类不少,通过建立一个UserManager类来统一对外接口,使得整个子系统条目结构清晰。
UserManager中无外乎用户数据的管理,如用户的创建、修改、查询和删除。DbUserManager是UserManager的一个数据库实现,可是看看DbUserManager中除了删除功能是直接通过SQL语句进行数据库删除操作外,其他都委托给User的具体实现DbUser实现的。这种实现非常类似于EJB中Session Bean和实体Bean之间的关系。以创建用户资料为例,代码如下:
public
User createUser(String username, String password, String email)
throws UserAlreadyExistsException
{
User newUser = null;
try {
//以username查询改用户是否存在
User existingUser = getUser(username);
//如果没有抛出UserNotFoundException异常,表示该用户存在
//The user already exists since now exception, so:
throw new UserAlreadyExistsException();
} catch (UserNotFoundException unfe) {
//该用户不存在,创建一个新用户
newUser = new DbUser(username, password, email, factory);
}
return newUser;
}
DbUser的构造方法实际是用户资料的新增创建:
protected
DbUser(String username, String password, String email,
DbForumFactory factory)
{
this.id = SequenceManager.nextID(JiveGlobals.USER); //获得自增ID
this.username = username;
// Compute hash of password.
this.passwordHash = StringUtils.hash(password);
//获得加密的密码
this.email = email;
this.factory = factory;
long now = System.currentTimeMillis();
creationDate = new java.util.Date(now);
modifiedDate = new java.util.Date(now);
properties = new Hashtable();
insertIntoDb();
//数据库插入数据
}
在Jive中,数据修改的保存是由DbUser的saveToDb方法实现的,而saveToDb方法调用是在每个setXXXX方法中。即每当外界调用DbUser的setXXXX,则表示需要改变某些字段属性值,在这个方法中直接进行数据库存储,这也类似EJB中CMP实体Bean的数据字段修改保存。
Jive中组Group与用户User处理几乎差不多,只是在Group中整合了权限方面的信息,这种做法是有一定的局限性,不是很值得借鉴,要想设计一个动态扩展灵活的权限系统,必须在用户或组与权限之间引入角色概念,也就是比较先进的基于角色的权限系统(RBAC Roled-Based Access Control,相关网址:http://csrc.nist.gov/rbac/)。
在RBAC中,用户组只是用户的一个集合,应该是通过角色和权限发生联系。所以RBAC认为,如果给用户组赋予权限,那么用户组也接近角色的概念。
4 Jive的缓存机制
Jive论坛的一个主要特点就是其性能速度快,因此很多巨大访问量的网站都采用了Jive论坛。这些都是由于Jive采取了高速缓存机制。
缓存(Cache)机制是提高系统运行性能必不可少的技术。缓存机制从原理上讲比较简单,就是在原始数据第一次读取后保存在内存中,下次读取时,就直接从内存中读取。原始数据有可能保存在持久化介质或网络上。缓存机制也是代理模式的一种实现。
4.1 缓存原理和实现
Jive的Cache总体来说实现得不是非常精简和有效。它是针对每个具体数据对象逐个实现缓冲,这种“穷尽”的办法不是实践所推荐的用法。通过使用动态代理模式,可以根据具体方法的不同来实现缓存是值得推荐的做法。Jive的缓存实现得比较简单,可以用来学习和研究缓存机制。
Jive中的Cache实现了缓存机制的大部分行为,它是将对象用惟一的关键字Key作标识保存在HashMap或Hashtable中。当然,必须知道这些对象的大小,这个前提条件的设定可以保证缓存增长时不会超过规定的最大值。
如果缓存增长得太大,一些不经常被访问的对象将首先从缓存中删除。如果设置了对象的最大生命周期时间,即使这个对象被反复频繁访问,也将从缓存中删除。这个特性可以适用于一些周期性需要刷新的数据,如来自数据库的数据。
在Cach中除了getObject()方法的性能依据缓存大小,其他方法的性能都是比较快的。一个HashMap用来实现快速寻找,两个LinkedList中一个以一定的访问顺序来保存对象,叫accessed LinkedList;另外一个以它们加入缓存的顺序保存这些对象,这种保存对象只是保存对象的引用,叫 age LinkedList。注意,这里的LinkedList不是JDK中的LinkedList,而是Jive自己定义的LinkedList。
当对象被加入缓存时,首先被CacheObject封装。封装有以下信息:对象大小(以字节计算),一个指向accessed LinkedList的引用,一个指向age
LinkedList的引用。
当从缓存中获取一个对象如ObjectA时,首先,HashMap寻找到指向封装ObjectA等信息的CacheObject对象。然后,这个对象将被移动到accessed LinkedList的前面,还有其他一些动作如缓存清理、删除、过期失效等都是在这个动作中一起触发实现的。
public
class Cache implements Cacheable {
/**
* 因为System.currentTimeMillis()执行非常耗费性能,因此如果get操作都执行
* 这条语句将会形成性能瓶颈,通过一个全局时间戳来实现每秒更新
* 当然,这意味着在缓存过期时间计算上有一到几秒的误差
*/
protected static long currentTime = CacheTimer.currentTime;
//CacheObject对象
protected HashMap cachedObjectsHash;
//accessed LinkedList 最经常访问的排列在最前面
protected LinkedList lastAccessedList;
//以缓存加入顺序排列,最后加入排在最前面;越早加入的排在最后面
protected LinkedList ageList;
//缓存最大限制默认是128k 可根据内存设定,越大性能越高
protected int maxSize = 128 * 1024;
//当前缓存的大小
protected int size = 0;
//最大生命周期时间,默认是没有
protected long maxLifetime = -1;
//缓存的击中率,用于评测缓存效率
protected long cacheHits, cacheMisses = 0L;
public Cache() {
// 构造HashMap. 默认capacity 是11
// 如果实际大小超过11,HashMap将自动扩充,但是每次扩充都
// 是性能开销,因此期初要设置大一点
cachedObjectsHash = new HashMap(103);
lastAccessedList = new LinkedList();
ageList = new LinkedList();
}
public Cache(int maxSize) {
this();
this.maxSize = maxSize;
}
public Cache(long maxLifetime) {
this();
this.maxLifetime = maxLifetime;
}
public Cache(int maxSize, long maxLifetime) {
this();
this.maxSize = maxSize;
this.maxLifetime = maxLifetime;
}
public int getSize() { return
size; }
public int getMaxSize() { return
maxSize; }
public void setMaxSize(int maxSize) {
this.maxSize = maxSize;
// 有可能缓存大小超过最大值,需要激活删除清理动作
cullCache();
}
public synchronized int getNumElements() {
return cachedObjectsHash.size();
}
/**
* 增加一个Cacheable对象
* 因为HashMap不是线程安全的,所以操作方法要使用同步
* 如果使用Hashtable就不必同步
*/
public synchronized void add(Object key, Cacheable object) {
// 删除已经存在的key
remove(key);
int objectSize = object.getSize();
// 如果被缓存对象的大小超过最大值,就放弃
if (objectSize > maxSize * .90)
{
return; }
size += objectSize;
//创建一个CacheObject对象
CacheObject cacheObject = new CacheObject(object, objectSize);
cachedObjectsHash.put(key, cacheObject); //保存这个CacheObject
// 加入accessed LinkedList,Jive自己的LinkedList在加入时可以返回值
LinkedListNode lastAccessedNode = lastAccessedList.addFirst(key);
// 保存引用
cacheObject.lastAccessedListNode = lastAccessedNode;
// 加入到age LinkedList
LinkedListNode ageNode = ageList.addFirst(key);
// 这里直接调用System.currentTimeMillis();用法值得讨论
ageNode.timestamp = System.currentTimeMillis();
// 保存引用
cacheObject.ageListNode = ageNode;
// 做一些清理工作
cullCache();
}
/**
* 从缓存中获得一个被缓存的对象,这个方法在下面两种情况返回空
* <li>该对象引用从来没有被加入缓存中
* <li>对象引用因为过期被清除</ul>
*/
public synchronized Cacheable get(Object key) {
// 清除过期缓存
deleteExpiredEntries();
//以Key从缓存中获取一个对象引用
CacheObject cacheObject = (CacheObject)cachedObjectsHash.get(key);
if (cacheObject == null) {
// 不存在,增加未命中率
cacheMisses++;
return null;
}
// 存在,增加命中率
cacheHits++;
// 从accessed LinkedList中将对象从当前位置删除
// 重新插入在第一个
cacheObject.lastAccessedListNode.remove();
lastAccessedList.addFirst(cacheObject.lastAccessedListNode);
return cacheObject.object;
}
…
}
在Cache中,关键字Key是一个对象,为了再次提高性能,可以进一步将Key确定为一个long类型的整数。
4.2 缓存使用
建立LongCache只是为了提高原来的Cache性能,本身无多大意义,可以将LongCache看成与Cache一样的类。
LongCache的关键字Key是Forum、ForumThread以及 ForumMessage等long类型的ID,值Value是Forum、ForumThread以及ForumMessage等的对象。这些基本是通过DatabaseCacheManager实现完成,在主要类DbForumFactory的初始化构造时,同时构造了DatabaseCacheManager的实例cacheManager。
前面过滤器功能分析中,Message对象获得方法的第一句如下:
protected
ForumMessage getMessage(long messageID, long threadID, long forumID) throws
ForumMessageNotFoundException {
DbForumMessage message = cacheManager.messageCache.get(messageID);
…
}
其中,cacheManager是DatabaseCacheManager的实例,DatabaseCacheManager是一个缓存Facade类。在其中包含了5种类型的缓存,都是针对 Jive的5个主要对象,DatabaseCacheManager主要代码如下:
public
class DatabaseCacheManager {
…
public UserCache userCache;
//用户资料缓存
public GroupCache groupCache;
//组资料缓存
public ForumCache forumCache;
//Forum论坛缓存
public ForumThreadCache threadCache;
//Thread主题缓存
public ForumMessageCache
messageCache; //Message缓存
public UserPermissionsCache userPermsCache; //用户权限缓存
public DatabaseCacheManager(DbForumFactory factory) {
…
forumCache =
new ForumCache(new LongCache(forumCacheSize, 6*HOUR), factory);
threadCache =
new ForumThreadCache(
new LongCache(threadCacheSize, 6*HOUR), factory);
messageCache = new ForumMessageCache(
new LongCache(messageCacheSize, 6*HOUR), factory);
userCache = new UserCache(
new LongCache(userCacheSize, 6*HOUR), factory);
groupCache = new GroupCache(
new LongCache(groupCacheSize, 6*HOUR), factory);
userPermsCache = new UserPermissionsCache(
new UserPermsCache(userPermCacheSize, 24*HOUR), factory
);
}
…
}
从以上代码看出,ForumCache等对象生成都是以LongCache为基础构建的,以ForumCache为例,代码如下:
public
class ForumCache extends DatabaseCache {
//以Cache构建ID缓存
protected Cache forumIDCache = new Cache(128*1024, 6*JiveGlobals.HOUR);
//以LongCache构建整个对象缓存
public ForumCache(LongCache cache, DbForumFactory forumFactory) {
super(cache, forumFactory);
}
public DbForum get(long forumID) throws ForumNotFoundException {
…
DbForum forum = (DbForum)cache.get(forumID);
if (forum == null) { //如果缓存没有从数据库中获取
forum = new DbForum(forumID, factory);
cache.add(forumID, forum);
}
return forum;
}
public
Forum get(String name) throws ForumNotFoundException {
//以name为key,从forumIDCache中获取ID
CacheableLong
forumIDLong = (CacheableLong)forumIDCache.get(name);
if (forumIDLong == null) { //如果缓存没有从数据库获得
long forumID = factory.getForumID(name);
forumIDLong = new CacheableLong(forumID); //生成一个缓存对象
forumIDCache.add(name, forumIDLong);
}
return get(forumIDLong.getLong());
}
…
}
由此可以看到,LongCache封装了Cache的核心功能,而ForumCache等类则是在LongCache核心外又包装了与应用系统相关的操作,这有点类似装饰(Decorator)模式。
从中也可以看到Cache和LongCache两种缓存的用法。
使用Cache时的关键字Key是任何字段。如上面代码中的String name,如果用户大量帖子主题查询中,Key是query + blockID,见DbForum中的getThreadBlock方法;而值Value则是Long类型的ID,如ForumID或ThreadID等。
LongCache的关键字Key是Long类型的ID,如ForumID或ThreadID等;而值Value则是Forum、ForumThread或ForumMessage等主要具体对象。
在实际使用中,大多数是根据ID获得对象。但有时并不是这样,因此根据应用区分了两种Cache,这其实类似数据库的数据表,除了主关键字外还有其他关键字。
4.3 小结
缓存中对象是原对象的映射,如何确保缓存中对象和原对象的一致性?即当原对象发生变化时,缓存中的对象也必须立即更新。这是缓存机制需要解决的另外一个基本技术问题。
Jive中是在原对象发生变化时,立即进行清除缓存中对象,如ForumMessage对象的创建。在DbForumThread的AddMessage方法中有下列语句:
factory.cacheManager.threadCache.remove(this.id);
factory.cacheManager.forumCache.remove(this.forumID);
即当有新的帖子加入时,将ForumThreadCache和ForumCache相关缓冲全部清除。这样,当有相关对象读取时,将直接从数据库中读取,这是一种非常简单的缓存更新方式。
在复杂的系统,例如有一台以上的服务器运行着Jive系统。如果一个用户登陆一台服务器后,通过这台服务器增加新帖。那么按照上述原理,只能更新本服务器JVM中的缓存数据,而其他服务器则无从得知这种改变,这就需要一种分布式的缓存机制。
到目前可以发现,整个Jive系统其实是围绕Forum、ForumThread和ForumMessage等这些主要对象展开的读取、修改或创建等操作。由于这些对象原先持久化保存在数据库中,为了提高性能和加强安全性,Jive在这些对象外面分别实现两层包装,如图3-7所示。
客户端如果需要访问这些对象,首先要经过它们的代理对象。进行访问权限的检查,然后再从缓存中获取该对象。只有缓存不存在时,才会从数据库中获取。
这套机制是大多数应用系统都面临的必须解决的基本功能,因此完全可以做成一个通用的可重复使用的框架。这样在具体应用时,不必每个应用系统都架设开发这样的机制。其实EJB就是这样一套框架,实体Bean都由缓存机制支持,而通过设定ejb-jar.xml可以实现访问权限控制,这些工作都直接由EJB容器实现了,不必在代码中自己来实现。剩余的工作是调整EJB容器的参数,使之适合应用系统的具体要求,这些将在以后章节中讨论。
在Jive中,图3-7的机制是通过不同方式实现的。基本上是一配二模式:一个对象有一个缓冲对象和一个代理对象,这样做的一个缺点是导致对象太多,系统变得复杂。这点在阅读Jive源码时可能已经发现。
如果建立一个对象工厂,工厂内部封装了图3-7机制实现过程,客户端可以根据不同的工厂输入参数获得具体不同的对象。这样也许代码结构要更加抽象和紧凑,Java的动态代理API也许是实现这个工厂的主要技术基础。有兴趣者可以进一步研究提炼。
5 Jive的其他组件技术
Jive是一个比较丰富的知识宝藏,从中可以学习到很多新的实战技巧和具体功能实现方式。前面基本介绍了Jive中的一些主要架构技术,通过这些技术可以基本上掌握Jive论坛系统。
Jive中还有很多非常实用的组件技术和工具库,分析学习可重用技术,可以在自己具体的项目重复使用,大大提高了新系统的开发速度和效率。
5.1 Jive的树形结构
Jive的管理功能中提供了将Jive数据库数据导出到XML文件的管理工具,在这个工具功能实现中,使用了树形结构的遍历技术。
Jive将ForumThread中的第一个ForumMessage作为root ForumMessage,以这个ForumMessage为根节点,每个ForumThread中包含了一套树形结构。
TreeWalker是树形结构的一个抽象接口,代码如下:
public
interface TreeWalker {
//根节点
public ForumMessage getRoot();
//获得父节点
public ForumMessage getParent(ForumMessage child)
throws ForumMessageNotFoundException;
//获得子节点
public ForumMessage getChild(ForumMessage parent, int index)
throws ForumMessageNotFoundException;
//获得所有子节点
public Iterator children(ForumMessage parent);
//获得所有的子节点,包括子节点的子节点…
public Iterator recursiveChildren(ForumMessage parent);
//获得一个节点的深度,相对根节点而言
public int getMessageDepth(ForumMessage message);
public int getChildCount(ForumMessage parent);
public int getRecursiveChildCount(ForumMessage parent);
/**
* 返回相对父节点的子节点索引。例如
* <pre>
* 4
* |-- 2
* |-- |-- 1
* |-- |-- 6
* |-- |-- 8
* |-- 5
* </pre>
* getIndexOfChild(4, 2) 将返回0
* getIndexOfChild(4, 5) 将返回1
* getIndexOfChild(2, 1) 将返回0
* getIndexOfChild(2, 6) 将返回1
* getIndexOfChild(2, 8) 将返回2
*/
public int getIndexOfChild(ForumMessage parent, ForumMessage child);
//一个节点是否是叶,叶相对枝来说,叶没有子节点了
public boolean isLeaf(ForumMessage node);
}
DbTreeWalker 是TreeWalker的一个实现,它是将一个ForumThread下所有帖子的ID从数据库中装入LongTree中。一句LongTree的树形结构遍历核心技术实现ForumThread中的帖子结构的遍历。
LongTree类似之前的Cache类,封装了树形结构遍历的核心算法,在LongTree中建立了3个数组long
[] keys、char [] leftChildren和char [] rightSiblings。
一个节点有两个特性:它有子节点;它有兄弟节点。leftChildren保存的是这个节点的子节点的索引;而rightSiblings保存的是这个节点兄弟节点的索引。例如:
1000
|-- 3000
|-- |--4000
|-- |--6000
|-- |--7000
|-- 5000
1000是个根节点,1000下有两个子节点3000和5000,而3000则有3个子节点4000、6000和7000,3000还有一个兄弟节点5000,使用上述3个数组是这样保持信息的:
keys[0]
= 1000
keys[1]
= 3000
keys[2]
= 4000
keys[3]
= 5000
keys[4]
= 6000
keys[5]
= 7000
keys数组中保存的是各个节点的数值,而leftChildren和rightSiblings数组保存的是keys数组的index,即0、1、2、3、4等数字。
1000节点有两个子节点,那么其对应的leftChildren和rightSiblings分别是:
leftChildren[0] = 1
leftChildren[0]中的索引0表示当前索引,keus[0]是1000,说明现在节点是1000;1也表示keys数组的索引,keys[1]的值是3000,所以上一句表示1000的子节点是3000。
1000节点没有兄弟节点:
rightSiblings[0] = -1
再看看3000节点,其keys的索引Index是1,其子节点是4000、6000和7000,取最近一个4000的索引index放入数组:
leftChildren[1] = 2
这表示1000节点的子节点是4000,那么另外一个6000节点如何表示?这是以4000节点的兄弟节点表现出来的。4000节点的keys的索引index是2,通过下列表示:
rightSiblings[2] = 4
其中,4表示6000在keys中的索引Index。同样,第3个子节点7000表示如下:
rightSiblings[4] = 5
这样,3000节点有3个子节点4000、6000和7000(4000、6000和7000是兄弟节点)通过上述简单两句就表现出来了。
总结一个父子关系树的表示方法:在父节点中,使用leftChildren保存最靠近父节点的一个子节点(父节点的第一个儿子,叫长子)的索引,其他子节点则是通过rightSiblings表明与长子是兄弟关系。
看看LongTress的初始化构造方法,keys数组的值保存的是ForumMessage的ID,如下:
public
LongTree(long rootKey, int size) {
keys = new long[size+1];
//初始化
leftChildren = new char[size+1];
//初始化
rightSiblings = new char[size+1];
//初始化
// 在keys[1]中保存的是rootMessage 的ID
keys[1] = rootKey;
leftChildren[1] = 0;
//无子节点
rightSiblings[1] = 0;
//无兄弟姐妹
}
当加入一个节点时,其方法如下:
public
void addChild(long parentKey, long newKey) {
// 根据parentKey找出其对应的keys索引index
char parentIndex = findKey(parentKey, (char)1);
if (parentIndex == 0) {
throw new IllegalArgumentException("Parent key " + parentKey +
" not found when adding child " + newKey + ".");
}
// 为newKey创建节点
keys[nextIndex] = newKey;
leftChildren[nextIndex] = 0;
rightSiblings[nextIndex] = 0;
//将新建节点标志为父节点的子节点
if (leftChildren[parentIndex] == 0) {
// 如果父节点原来没有子节点,那就将新建节点作为其子节点
leftChildren[parentIndex] = nextIndex;
}else {
// 如果父节点有子节点,寻找最后一个子节点
long siblingIndex = leftChildren[parentIndex];
//在siblingIndex中循环查找,直至值为0
while (rightSiblings[new Long(siblingIndex).intValue()] != 0) {
siblingIndex = rightSiblings[new Long(siblingIndex).intValue()];
}
// 将新建节点作为最后一个字节点加入
rightSiblings[new Long(siblingIndex).intValue()] = nextIndex;
}
// 最后,自动增加nextIndex 以便下一个等待插入
nextIndex++;
}
Jive将数据导出到XML文件时,就是根据某个ForumMessage的ID,通过TreeWalker找出它的所有子节点ForumMessage的ID,然后将其内容导出。
5.2 XML和JDOM
XML 称为可扩充标记语言,是类似HTML定义文档标记语言的一个框架。XML以结构严谨著称,因此用来保存数据是非常适合的,这样在数据库之外,又多了一个持久化保存数据的方式。
在Java中,XML更多时是作为配置文件数据存储体形式出现,在之前一般是使用properties来保存系统的配置文件,如下:
cache.maxsize=1024
cache.minsize=2
这两句分别设置cache的最大值和最小值,那么在Java中通过下列语句读取:
Properties
p = new Properties();
InputStream
fin = new FileInputStream("Config.properties");
p.load(fin);
String
maxSize = p.getProperty("cache.maxsize ");
String
minSize = p.getProperty("cache.minsize ");
这样就可以获得配置文件中的两个值。
这种配置文件使用方法简单直接,但是只适合配置文件不很复杂的情况。在复杂的配置情况下,properties就不是很合适,例如设置系统的可选属性,一个系统安装在不同应用场合,客户的要求总有些不一样,有些功能是可选的,那么需要在配置文件中配置一些可选的功能,以Tomcat的server.xml为例,如下:
<Context
path="/register"
docBase="D:/javasource/SimpleRegister/defaultroot"
debug="1"
reloadable="true" crossContext="true">
…
</Context>
<Context
path="/examples" docBase="examples" debug="0"
reloadable="true"
crossContext="true">
<Logger className="org.apache.catalina.logger.FileLogger"
prefix="localhost_examples_log." suffix=".txt"
timestamp="true"/>
…
</Context>
在一个配置中有很多Context,每个Contexr都包含Logger等具体配置,XML格式本身是一种树形结构的数据格式。在实际应用中,很多复杂的表示都可以使用树形结构来分解代表。因此,使用XML来表示这种树形结构的数据无疑是非常合适的。
在Jive中,jive_config.xml是Jive系统的配置文件。这个配置文件是在Jive系统安装时,按照用户的选择动态生成的,其中包含数据库连接参数、界面显示颜色、电子邮件配置以及缓冲配置、搜索配置和文件或图片上传配置。
分析读取XML数据有很多工具,如DOM(http://www.worg/DOM/)和SAX(http://www.saxproject.org/)。这两种是标准的XML分析器,可以使用任何语言来实现,DOM分析XML数据时,是将整个文档一下子读入内存,如果文档很大,性能就发生影响,而SAX则是动态地对每一行分析,无需全部读入,因此在分析大文档时速度比较快。
但是这两种分析方法都是围绕XML树形结构展开的,在编制这两种分析器时,会涉及到大量XML概念的API,需要一定的XML基础和知识,使用起来有一定难度。
JDOM(http://www.jdom.org)封装了DOM/SAX的具体使用技术,以非常符合Java编程方式的形式来分析XML,因此使用起来非常方便。
在分析速度方面,JDOM比DOM要快,比SAX慢一点。但用在分析配置文件上,速度不是主要的,因为可以使用lazy initialization。这类似缓存机制,在第一次读取后就保存在内存中,以后每次直接从内存中获取。
在Jive中,JDOM操作基本是由JiveGlobals完成的。
public
class JiveGlobals {
private
static final String JIVE_CONFIG_FILENAME = "jive_config.xml";
private static XMLProperties properties = null;
...
//从配置文件获取配置
public
static String getJiveProperty(String name) {
loadProperties();
return
properties.getProperty(name);
}
//用JDOM载入配置文件
private
synchronized static void loadProperties() {
if
(properties == null) {
properties
= new XMLProperties(jiveHome + File.separator +
JIVE_CONFIG_FILENAME);
}
}
//将配置保存到配置文件中
public
static void setJiveProperty(String name, String value) {
loadProperties();
properties.setProperty(name, value);
}
}
从上面代码看出,对XML文件读写非常方便,使用properties.getProperty(name)就可以获得name的配置值,而properties.setProperty(name, value)一句就可以将name和其值value保存到XML文件中,非常类似Hashtable的读取和存入。
XMLProperties是JDOM的一个属性文件辅助包,它主要是对属性名进行分解和合成,例如XML如下:
<jive>
<email>
<fromName>Jive_Administrator</fromName>
<fromEmail>webmaster@example.com</fromEmail>
<subject>Your
thread was updated!</subject>
<body>Hello
{name}! The thread {threadName} was updated!</body>
</email>
<jive>
jive/email/fromName的值是Jive_Administrator,那么如何读取Jive_Administrator?使用properties.getProperty("email.fromName")就可以。注意到,这里Key的名字组合是 email.fromName,这种特定的写法就是XMLProperties可以支持的,在对XML文件保存细节中,由XMLProperties将这种属性名称写法具体转换成XML文档操作。具体内部代码如下:
public String getProperty(String name) {
if (propertyCache.containsKey(name)) { //从缓存中获取
return (String)propertyCache.get(name);
}
//将email.fromName转变为String数组
//例如propName[0] = jive; propName[1] = email
…
String[] propName = parsePropertyName(name);
// 通过propName数组循环,遍历XML的树形结构层次,寻找出对应的属性值
Element element = doc.getRootElement();
for (int i = 0; i < propName.length; i++) {
element = element.getChild(propName[i]);
if (element == null) {
return null;
}
}
// 寻找到element后,获得其内容
String value = element.getText();
if ("".equals(value))
{ return
null; }
else {
// 保存到缓存中
value = value.trim();
propertyCache.put(name, value);
return value;
}
}
以上只是分析了JDOM的XMLProperties包是如何做属性配置提取的,正是因为JDOM内部做了很多基础支持性的细节工作,才使得使用JDOM变得非常方便。
总结使用JDOM对配置文件读写操作语法如下:
·
获得配置(查询):getProperty(name)。
·
新增和修改:properties.setProperty(name, value)。
·
删除:properties.deleteProperty(name)。
name的格式是xxx.xxx.xxx,例如:
<jive>
…
<upload>
<dir>/home/jdon/jive/upload/</dir>
<relurl>upload/</relurl>
</upload>
…
</jive>
要获得/home/jdon/jive/upload/,name的格式是upload.dir;要获得upload/,name的格式是upload.relurl。
注意,如果要在系统中支持上述功能,必须下载JDOM包,还要有DataFormatFilter. java、DataUnformatFilter.java、XMLFilterBase.java和XMLProperties.java支持。这几个类不包含在JDOM标准包中,作为一个应用包含在其Sample中。当然也可以直接从Jive中复制出来使用。
5.3 全文检索和Lucene
Jive中支持全文检索,这个功能主要核心依赖另外一个开放源代码项目Lucene(http://jakarta.apache.org/lucene/docs/index.html)。Jakarta Lucene是一个高性能全文搜索引擎,可以跨平台应用于任何搜索应用。
使用Lucene作为搜索引擎,应用系统需要做两件事情:
(1)建立索引文件。将Jive数据库中的数据内容建立索引文件,这是通过SearchManager来完成。SearchManager代码如下:
public
interface SearchManager {
public boolean isSearchEnabled();
public void setSearchEnabled(boolean searchEnabled);
/**
//如果SearchManage正在工作,返回真
public boolean isBusy();
//返回索引完成率
public int getPercentComplete();
//是否自动建立索引
//通过TaskEngine.scheduleTask方法实现定期自动索引
public boolean isAutoIndexEnabled();
public void setAutoIndexEnabled(boolean value);
//自动索引间隔的分钟数
public int getAutoIndexInterval();
public void setAutoIndexInterval(int minutes);
//获得上次建立索引的时间
public Date getLastIndexedDate();
//在实时建立索引时,将当前帖子加入索引
public void addToIndex(ForumMessage message);
public void removeFromIndex(ForumMessage message);
//手动更新自上次建立索引后的新内容
public void updateIndex();
//手动重新建立全部的索引
public void rebuildIndex();
//优化
public void optimize();
}
·
SearchManager定义了建立索引的一些属性,建立索引有两种方式:当有新帖子加入时,通过调用indexMessage()方法实时索引;或者通过TaskEngine.scheduleTask方法每隔一定时间建立索引。
·
DbSearchManager作为SearchManager的子类实现,又是一个线程类,它是建立索引的主要功能类。在DbSearchManager中主要使用了Lucene的IndexWriter、
Analyzer、 Document和 Field等功能类来建立索引。
·
IndexWriter用户建立新的索引,当然也可以将文档加入已经存在的索引。
在文本被索引之前,它必须通过一个分析器Analyzer。分析器Analyzer 负责从文本中分离出索引关键字。Lucene有几种不同类型的分析器:
·
SimpleAnalyzer是将英文转换为小写字母,按空格和标点符号切分出英文单词,
如I am Java这一句,使用SimpleAnalyzer切词就会切分出下列词语:
token1=I
token2=am
token3=Java
·
StandardAnalyzer是对英文进行了较为复杂的处理。除了按词语建立索引关键字(token)外,还能够为特殊名称、邮件地址、缩写格式等建立索引单元,而且对“and”、“ the”等词语做了过滤。
·
ChineseAnalyzer是专门用来分析中文的索引的。关于中文分析器,有很多尝试,如车东的http://sourceforge.net/projects/weblucene/;zhoujun的http://www.
jdon.com/jive/thread.jsp? forum=61&thread=8400等,该问题将在后面章节继续讨论。
一个索引是由一系列Document组成,每个Document是由一个或多个Field组成,每个Field都有一个名字和值,可以把Document作为关系数据库中一条记录,而Field则是记录中某列字段。一般建立索引如下:
//指定将在哪个目录建立索引
String
indexDir = "/home/jdon/jive/WEB-INF/jiveHome";
//指定将要建立索引的文本
String
text = "welcom here, I am Java,";
Analyzer
analyzer = new StandardAnalyzer(); //使用StandardAnalyzer
//建立一个IndexWriter
IndexWriter
writer = new IndexWriter(indexDir, analyzer, true);
//建立Document
Document
document = new Document();
//进行切词、索引
document.add(Field.Text("fieldname",
text));
//加入索引中
writer.addDocument(document);
writer.close();
其中,Field根据具体要求有不同用法,Lucene提供4种类型的Field: Keyword、 UnIndexed、 UnStored和 Text。
·
Keyword 不实现切词,逐字地保存在索引中,这种类型适合一些如URL、日期、个人姓名、社会安全号码、电话号码等需要原封不动保留的词语。
·
UnIndexed既不实现切词也不索引,但是其值是一个词一个词地保存在索引中,这不适合很大很长的词语,适合于显示一些不经过直接搜索的结果值。
·
UnStored与UnIndexed正好相反,将被切词和索引,但是不保存在索引中,这适合巨大文本,如帖子内容、页面内容等。
·
Text是实现切词、索引,并且保存在索引中。
在Jive中,索引的建立以DbSearchManager中加入帖子索引方法为例:
protected
final void addMessageToIndex(long messageID, long userID,
long threadID, long forumID, String subject, String body,
java.util.Date creationDate, IndexWriter writer) throws IOException
{
//建立一个 Document
Document doc = new Document();
doc.add(Field.Keyword("messageID",Long.toString(messageID)));
doc.add(new Field("userID", Long.toString(userID), false, true,
false));
doc.add(new Field("threadID", Long.toString(threadID), false, true,
false));
doc.add(new Field("forumID", Long.toString(forumID), false, true,
false));
doc.add(Field.UnStored("subject", subject));
doc.add(Field.UnStored("body", body));
doc.add(new Field("creationDate",
DateField.dateToString(creationDate),
false, true, false));
//将该Document加入当前索引中
writer.addDocument(doc);
}
在DbSearchManager中同时也实现了自动建立索引的过程,通过在构造方法中生成TimeTask实例:
timerTask
= TaskEngine.scheduleTask(
this,autoIndexInterval*JiveGlobals.MINUTE,
autoIndexInterval*JiveGlobals.MINUTE);
因为DbSearchManager是线程类,它在run方法中实现索引任务自动运行:
TaskEngine.addTask(new IndexTask(false));
(2)建立完成后,就可以直接搜索特定的词语了。搜索语句一般代码如下:
Searcher
searcher = new IndexSearcher((indexDir); //创建一个搜索器
//使用和索引同样的语言分析器
Query
query = QueryParser.parse(queryString, "body", new
StandardAnalyzer());
//搜索结果使用Hits存储
Hits
hits = searcher.search(query);
//通过hits得到相应字段的数据和查询的匹配度
for
(int i=0; i<hits.length(); i++) {
System.out.println(hits.doc(i).get("fieldname "));
};
Jive实现搜索就复杂得多,它为搜索专门建立了一个Query接口:
public
interface Query {
//需要搜索的字符串
public String getQueryString();
public void setQueryString(String queryString);
public Date getBeforeDate();
public void setBeforeDate(Date beforeDate);
public Date getAfterDate();
public void setAfterDate(Date afterDate);
public User getFilteredUser();
public void filterOnUser(User user);
public ForumThread getFilteredThread();
public void filterOnThread(ForumThread thread);
public int resultCount();
public Iterator results();
public Iterator results(int startIndex, int numResults);
}
Query接口中主要定义了和搜索相关的一些参数,可以根据具体要求定制,直接使用Query就可以达到搜索的目的,如需要搜索Java is cool,那么使用下列代码:
ForumFactory
forumFactory = ForumFactory.getInstance();
Query
query = forumFactory.createQuery(forums);
query.setQueryString("Jive
is cool");
Iterator
iter = query.results();
while
(iter.hasNext()) {
ForumMessage message = (ForumMessage)iter.nextElement();
//输出结果
}
追查代码会发现,上面forumFactory.createQuery(forums)方法实际内容是new DbQuery(forums, this)。DbQuery作为Query的一个子类,它的搜索语句通过executeQuery()方法中下列语句实现:
private
void executeQuery() {
try {
Searcher searcher = getSearcher(); //创建一个搜索器
…
//使用分析器获得Query对象
org.apache.lucene.search.Query bodyQuery =
QueryParser.parse(queryString, "body", DbSearchManager.analyzer);
org.apache.lucene.search.Query subjectQuery =
QueryParser.parse(queryString, "subject", DbSearchManager.analyzer);
//将两个Query对象加入BooleanQuery
BooleanQuery comboQuery = new BooleanQuery();
comboQuery.add(subjectQuery,false,false);
comboQuery.add(bodyQuery,false,false);
//Jive自己的搜索结果过滤器
MultiFilter multiFilter = new MultiFilter(3);
int filterCount = 0;
if (factory.getForumCount() != forums.length) {
//将其他论坛内容搜索结果过滤掉
String[] forumIDs = new String[forums.length];
for (int i=0; i<forumIDs.length; i++) {
forumIDs[i] = Long.toString(forums[i].getID());
}
multiFilter.add(new FieldFilter("forumID", forumIDs));
filterCount++;
}
//日期过滤器 如只查询某日期以后的内容
if (beforeDate != null || afterDate != null) {
if (beforeDate != null && afterDate != null) {
multiFilter.add(new DateFilter("creationDate", beforeDate,
afterDate));
filterCount++;
}else if
(beforeDate == null) {
multiFilter.add(DateFilter.After("creationDate", afterDate));
filterCount++;
}else {
multiFilter.add(DateFilter.Before("creationDate", beforeDate));
filterCount++;
}
}
// 过滤用户
if (user != null) {
String userID = Long.toString(user.getID());
multiFilter.add(new FieldFilter("userID", userID));
filterCount++;
}
// 主题过滤
if (thread != null) {
String threadID = Long.toString(thread.getID());
multiFilter.add(new FieldFilter("threadID",
threadID));
filterCount++;
}
if (filterCount > 0) {//实现搜索
hits = searcher.search(comboQuery, multiFilter);
} else {
hits = searcher.search(comboQuery);
}
//搜索结果不要超过最大大小
int numResults = hits.length() < MAX_RESULTS_SIZE ?
hits.length() : MAX_RESULTS_SIZE;
long [] messages = new long[numResults];
for (int i=0; i<numResults; i++) {
messages[i]= Long.parseLong( ((Document)hits.doc(i)).get("messageID")
);
}
results = messages;
} catch (Exception e) {
e.printStackTrace();
results = new long[0];
}
}
Jive的搜索使用了过滤器,以便过滤掉不想出现的结果,然后还对搜索结果进行了限制转换,这些在实际使用中都是必需的。
5.4 Jive的中文问题
Jive默认的字符集编码方式是ISO8859_1,即Latin-1字符集,这是国际标准化组织用来表示Latin等西方语言使用的字符集。
ISO8859_1字符集非常类似常见的ASCII字符集。由于ISO8859_1是使用单字节来表示,而汉字是采取双字节来表示一个汉字,我国制定了一套专门用来表示汉字GB2312和GBK编码字符集。
在Java内部运算中,涉及到的所有字符串都会被转化为UTF-8编码来进行运算。那么,在被Java转化之前,字符串是什么样的字符集? Java总是根据操作系统的默认编码字符集来决定字符串的初始编码,而且Java系统的输入和输出的都是采取操作系统的默认编码。
因此,如果能统一Java系统的输入、输出和操作系统3者的编码字符集合,将能够使Java系统正确处理和显示汉字。这是处理Java系统汉字的一个原则,但是在实际项目中,能够正确抓住和控制住Java系统的输入和输出部分是比较难的。
Jive是运行在Web容器中的一个Servlet/JSP系统。在这个系统中,输入途径有很多种:一种是通过页面表单打包成请求(request)发往服务器的;第二种是通过数据库读入;还有第3种输入比较复杂,JSP在第一次运行时总是被编译成Servlet,JSP中常常包含中文字符,那么编译使用javac时,Java将根据默认的操作系统编码作为初始编码。除非特别指定,如在Jbuilder中可以指定默认的字符集。
输出途径也有几种:第一种是JSP页面的输出。由于JSP页面已经被编译成Servlet,那么在输出时,也将根据操作系统的默认编码来选择输出编码,除非指定输出编码方式;还有输出途径是数据库,将字符串输出到数据库。
由此看来,一个J2EE系统的输入输出是非常复杂,而且是动态变化的,而Java是跨平台运行的,在实际编译和运行中,都可能涉及到不同的操作系统,如果任由Java自由根据操作系统来决定输入输出的编码字符集,这将不可控制地出现乱码。
正是由于Java的跨平台特性,使得字符集问题必须由具体系统来统一解决,所以在一个Java应用系统中,解决中文乱码的根本办法是明确指定整个应用系统统一字符集。
在Jive中如果指定默认字符集为某个字符集,那么就要在所有的输入输出环节都要标识为这个字符集。但是,前面已经提到,要完全在编码时做到还是有一定难度,必须对Web程序有相当地掌握和理解,而且步骤较繁琐。
有一种相对省事的做法,例如统一指定为ISO8859_1,因为目前大多数软件都是西方人编制的,他们默认的字符集就是ISO8859_1,包括操作系统Linux和数据库MySQL等。这样,如果指定Jive统一编码为ISO8859_1,那么就有下面3个环节必须把握:
·
开发和编译代码时指定字符集为ISO8859_1。
·
运行操作系统的默认编码必须是ISO8859_1,如Linux。
·
在JSP头部声明:<%@ page contentType="text/html;charset=ISO8859_1" %>。
如果统一指定为GBK中文字符集,上述3个环节同样需要做到,不同的是只能运行在默认编码为GBK的操作系统,如中文Windows。
所以统一编码为ISO8859_1和GBK虽然带来编制代码的方便,但是也破坏了Java跨平台运行的优越性,只在一定范围内行得通。
很多情况下,程序员大都是在中文Windows下开发调试Java系统,然后直接部署到Linux等系统上真正运行。而且其中可能涉及到XML文件读写。XML是对编码方式要求严格的数据存储体,XML又可以随着代码移动。因此,在进行真正大规模Java系统开发运行时,上述临时简单的变通方式就没有效果了。
要从根本上解决Java的中文问题,只要将Java系统的统一编码定义为UTF-8。UTF-8编码是一种兼容所有语言的编码方式,惟一比较麻烦的就是要找到应用系统的所有出入口,然后使用UTF-8去“结扎”它。
Jive默认的字符集编码方式是ISO8859_1,如果都统一为UTF-8,那么也需要做下列几步工作:
·
开发和编译代码时指定字符集为UTF-8。
·
使用过滤器,将所有请求(request)转换为UTF-8;针对不同应用过滤器有两种。
·
如果所有请求都经过一个Servlet控制分配器,那么使用Servlet的filter执行语句。
·
request.setCharacterEncoding("UTF-8")。
·
如果不经过Servlet,而直接是JSP,那么每个JSP头部设置上述语句。
·
在JSP头部声明:<%@ page contentType="text/html;charset= UTF-8" %>。
·
设定数据库连接方式是UTF-8。
以上讨论了Jive以及通用Java的中文问题。如果整个应用系统是从开始进行开发,那么统一指定编码为UTF-8就非常容易做到。如果是在英文源代码基础上二次开发,那么首先要将原来的源代码转换为统一编码UTF-8,那么这种转换工作会带来一定的麻烦。
6 Jive图形处理
Jive提供了强大的论坛功能,但是有些功能离实际需求还是有一定的距离,例如论坛是用于信息交流讨论的场所。而信息不只是文字,有时还包括图片或一些PDF等类型的文件,那么如何在Jive中实现这样的功能呢?
6.1 图片上传处理
在HTML中,使用表单Form主要是用来向服务器提交数据,格式如下:
<FORM
ACTION="URL"
METHOD="GET|POST"
ENCTYPE="…" TARGET="...">
. . .
</FORM>
enctype指定了表单提交给服务器时的内容形式(Content-Type),默认值是"application/x-www-form-urlencoded",这时,表单信息一般采用URL编码制。
但是,当向服务器传送图片或文件这样包含非ASCII字符或二进制数的数据时,根据RFC1867规定,就必须使用“multipart/form-data”内容类型。
其实无论是默认表单信息,还是图片文件,这些内容都是装载在HTTP协议的正文内容部分,都可以看成HTTP协议携带的对象,只是两种正文内容形式不一样。前者是String字符串类型,而后者则是一个通用的数据对象类型(Object)。在以后章节中将专门讨论HTTP协议装载数据对象的底层细节。
使用“multipart/form-data”上传文件的格式写法如下:
<FORM
ACTION="URL" METHOD="GET|POST" ENCTYPE="
multipart/form-data ">
<INPUT TYPE=FILE NAME=file1>
</FORM>
文件通过HTTP协议传送到服务器端后,需要在服务器端对该文件进行专门的接受。HttpServletRequest没有提供直接获取文件数据的方法,因此需要开发专门的服务器文件处理组件。
目前有两种上传文件处理组件:一种是基于完全JSP结构的,使用JSP来处理上传的文件,以SmartUpload(http://www.jspsmart.com)最常用;还有一种是完全的JavaBeans组件:Cos文件上传组件包(http://www.servlets.com/cos/index.html),Cos可以使用在JSP中,也可以使用在Servlet或Servlet Filter中。
由于在实际应用中,文件上传功能往往和其他正常表单参数一起混合使用,而不是独立使用的。因此,可以设定一个Servlet专门用来处理这类混合表单的请求,在将文件接受处理后,自动导向到处理表单正常参数的JSP/Servlet去处理。
表单调用示例如下:
<form
action="<%=request.getContextPath()%>/multipartformserv"
method="post" enctype="multipart/form-data">
<input type="hidden" name="FORWARDNAME"
value="login.jsp" >
<input type="file" name="file1" >
<input type="hidden" name="maxwidth"
value="120" >
<input type="hidden" name="maxheight"
value="60" >
<input type="text" name="username" >
<input type="text" name="password" >
</form>
在这个表单中,既有文件提交,也有username这样正常的参数需要提交,提交的Servlet名为multipartformserv。由multipartformserv来处理上传的文件,然后再自动转交到FORWARDNAME的值login.jsp进行username等正常参数的处理。
在表单中,如果设定maxwidth和maxiheight,那么表示如果上传的图片超过这个尺寸,服务器将缩小图片到这个尺寸。
编制一个专门用来统一处理Jive系统中所有文件处理的Servlet,这样有利于简化系统。编制MultipartFormServ如下:
public
class MultipartFormServ extends HttpServlet {
static final private String CONTENT_TYPE = "text/html";
//文件上传处理
private static MultipartFormHandle mf = MultipartFormHandle.getInstance();
//文件上传后存放的临时目录
private String dirName;
private ServletContext context;
public void init() throws ServletException {
// 从web.xml中读取上传目录参数
dirName = mf.getUploaddir();
if (dirName == null) {
throw new ServletException("Please supply uploadDir parameter");
}
}
//处理带有文件内容的请求
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try {
mf.init(dirName,request); //调用文件上传处理器处理
//得到FORWARDNAME参数值
String forward=mf.getForwardProgram();
if (forward.equals(""))
{
errorMessage("no forward program", response);
return;
}
String param=mf.getForwardProgramParam();
mf.clear();
//引导分流到forward参数值进行其他文本参数处理
getServletConfig().getServletContext().
getRequestDispatcher("/"+forward+"?"+param).forward(request,
response);
}catch (Exception Ex) {
throw new ServletException(Ex.getMessage());
}
}
}
MultipartFormHandler主要调用Cos组件处理上传文件,对上传文件图形大小进行处理,然后将图形搬迁到指定的目录;同时也将请求信号中的正常参数提取出来,以作为转发使用。
这样一个图形或文件上传系统已经形成了一个框架结构,可以重复使用在任何需要图片或文件上传处理的系统中,如图3-8所示。
图3-8中上传文件处理额Servlet相当于请求信号Request的一个过滤器,既然正常的Request处理机制不能从Request提取携带的文件,那么,使用一个过滤器先将文件提取出来,剩余的再通过正常Request处理机制去处理。
6.2 服务器端图形处理
Java最初是以Applet等客户端图形处理为技术起点的,而本节讨论的是如何在Servlet/JSP中实现图形处理。
在Jive中,图片可以用来显示用户的头像,用户在上传自己头像图片时,该图片的大小可能不一,但是由于版面原因,显示的头像图片又有大小限制,那么就需要在用户上传图片时对图片大小做一个检查。如果超过规定大小,就进行一定的缩放处理。
缩放处理有两种方式:是在HTML显示时,使用image 语法的width和height来限制大小,但是这样做只是解决了表面问题,无法解决大字节图片传送到客户端带来的性能影响,这个图片因为是用户发言的头像,将会在每个帖子里面显示。如果头像都是巨大图片,对帖子显示速度的影响是很大,因此必须在服务器端进行缩小后,再传送到客户端,这样提高了论坛系统性能。
服务器端的图形处理需要使用到Java的图形处理技术,而且图形处理是在服务器端的Web容器中进行的。和以往Java在客户端进行图形处理稍微有所不同,相同的是都要使用计算机的底层图形支持资源。
J2SE 1.4提供新的增强的图形处理功能,JDK1.4中最新的javax.imageio.ImageIO对图片进行读写操作,而使用java.awt.geom.AffineTransform对图片进行尺寸缩放处理。
import
java.io.File;
import
java.awt.image.BufferedImage;
import
java.awt.Image;
import
java.awt.image.AffineTransformOp;
import
javax.imageio.ImageIO;
import
java.awt.geom.AffineTransform;
public
class UploadImg{
/**
* 参数设置
* @param fromdir 图片的原始目录
* @param todir 处理后的图片存放目录
* @param imgfile 原始图片
* @param sysimgfile 处理后的图片文件名前缀
*/
public void init(String fromdir,String todir,String imgfile,String
sysimgfile)
{
this.fromdir=fromdir;
this.todir=todir;
this.imgfile=imgfile;
this.sysimgfile=sysimgfile;
}
…
public boolean CreateThumbnail() throws Exception
{
//ext是图片的格式 gif、JPG
或png
String ext=""
double Ratio=0.0;
File oldFile = new File(fromdir,imgfile);
if (!F.isFile()) //检查是否存在此图片文件
throw new Exception(F+" is not image file error in
CreateThumbnail!");
//首先判断上传的图片是gif还是JPG ImageIO,只能将gif转换为png
if (isJpg(imgfile)){
ext="jpg";
}else{
ext="png";
}
File newFile = new File(todir,sysimgfile+"."+ext);
BufferedImage Bi = ImageIO.read(oldFile); //读取原始图片
if ((Bi.getHeight()>120) || (Bi.getWidth()>120)){
if (Bi.getHeight()>Bi.getWidth())
Ratio = 120.0/Bi.getHeight();
else
Ratio = 120.0/Bi.getWidth();
}
//进行图片转换
AffineTransformOp op =
new AffineTransformOp(AffineTransform.getScaleInstance(Ratio, Ratio), null);
Image itemp = op.filter(Bi, null);
try { //写入转换后的图片
ImageIO.write((BufferedImage) itemp, ext, newFile);
}catch (Exception ex) {
throw new Exception(ex.getMessage());
}
return (true);
}
}
该类中由于使用到了Java 的AWT,虽然没有实际显示,但Linux系统下需要X11 Windows的支持(安装Linux时需安装 XFree86,Linux完全安装方式包括安装XFree86)。
该缩放功能是在图片上传到服务器后再进行的处理,可以对JPG进行缩小放大;对上传是GIF的图片,缩放后变成PNG图片格式文件。
7 Jive安装调试运行
Jive默认编码方式是ISO8859_1。下面以安装在英文Linux操作系统上为例:
(1)安装数据库。在database目录中选择对应数据库,如果数据库是MySQL,则使用jive_mysql.sql,然后通过数据库的管理工具将jive_mysql.sql导入相应数据库中。Jive所需要的数据库都已经准备完成。
(2)由于Jive是Servlet/JSP系统,必须在Web容器支持下才能运行。因此,必须安装Web服务器软件,例如Tomcat等。
最后需要将Jive部署到Tomcat中。有两种办法,其中直接复制的办法比较简单,修改server.xml办法可以见其他章节介绍。
将Jive的application目录下所有文件复制到Tomcat应用目录webapps的指定目录下,如Tomcat/webapps/jive下。
(3)在运行设置Jive之前,还需要设定Jive的初始值,在上述应用目录的WEB-INF/classes/下,创建或编辑jive_init.properties,加入行:
jiveHome=C:""javasource""jive""WEB-INF""jiveHome
这是指定jivHome的目录,jiveHome主要放置Jive的配置文件和搜索等内部工作文件,一般是建立在WEB-INF目录下,这样,jiveHome的内容就不会被外界通过浏览器直接访问到,要注意是绝对路径。
(4)可以安装设置Jive系统。通过浏览输入网址:http://localhost:8080/jive/admin/setup/,Jive的安装导航程序会自动进行安装检查。由于Jive编码是 ISO8859_1,如果涉及需要设置Java编码方式的提问,都设置为英文。
管理员密码最好更改一下,默认用户名和密码是admin 和admin。
全部设置完成后,进入http://localhost:8080/jive/可以浏览。
进入http://localhost:8080/jive/admin可以实现论坛管理,可以设置全局过滤器,一般设置如下过滤器:
·
TextStyle 文本格式。
·
Newline 新一行。
·
Profanity 过渡亵渎或不恰当词语。
·
URLConverter URLConverter。
·
ImageFilter ImageFilter 支持上传图片。
·
CodeHighlighter CodeHighlighter。
另外,缓存设置也很重要,可根据访问量、缓存击中率以及实际内存大小调整,可以提高论坛的性能。
图片上传路径的设置需要通过手工修改jiveHome下的jive_config.xml。打开jive_config.xml会发现,所有在管理配置中配置的信息都保存在这里,找到下列配置:
<upload>
<dir>/home/bqlr/jive/upload/</dir>
<relurl>upload/</relurl>
</upload>
其中,dir是上传图片存放的绝对路径;relurl是网址的相对路径,比如upload/,那就表示输入下列网址:“http://localhost:8080/jive/upload/xxxx.jpg”,就可以看到上传的图片。因为上传图片有自动缩小功能,因此需要Linux安装时是带图形X86的完全安装。用户如为中文名,将影响图片文件名。
以上是Jive论坛的安装,安装完成后的效果可以访问http://www.jdon.com/jive/。
7 Jive安装调试运行
Jive默认编码方式是ISO8859_1。下面以安装在英文Linux操作系统上为例:
(1)安装数据库。在database目录中选择对应数据库,如果数据库是MySQL,则使用jive_mysql.sql,然后通过数据库的管理工具将jive_mysql.sql导入相应数据库中。Jive所需要的数据库都已经准备完成。
(2)由于Jive是Servlet/JSP系统,必须在Web容器支持下才能运行。因此,必须安装Web服务器软件,例如Tomcat等。
最后需要将Jive部署到Tomcat中。有两种办法,其中直接复制的办法比较简单,修改server.xml办法可以见其他章节介绍。
将Jive的application目录下所有文件复制到Tomcat应用目录webapps的指定目录下,如Tomcat/webapps/jive下。
(3)在运行设置Jive之前,还需要设定Jive的初始值,在上述应用目录的WEB-INF/classes/下,创建或编辑jive_init.properties,加入行:
jiveHome=C:""javasource""jive""WEB-INF""jiveHome
这是指定jivHome的目录,jiveHome主要放置Jive的配置文件和搜索等内部工作文件,一般是建立在WEB-INF目录下,这样,jiveHome的内容就不会被外界通过浏览器直接访问到,要注意是绝对路径。
(4)可以安装设置Jive系统。通过浏览输入网址:http://localhost:8080/jive/admin/setup/,Jive的安装导航程序会自动进行安装检查。由于Jive编码是 ISO8859_1,如果涉及需要设置Java编码方式的提问,都设置为英文。
管理员密码最好更改一下,默认用户名和密码是admin 和admin。
全部设置完成后,进入http://localhost:8080/jive/可以浏览。
进入http://localhost:8080/jive/admin可以实现论坛管理,可以设置全局过滤器,一般设置如下过滤器:
·
TextStyle 文本格式。
·
Newline 新一行。
·
Profanity 过渡亵渎或不恰当词语。
·
URLConverter URLConverter。
·
ImageFilter ImageFilter 支持上传图片。
·
CodeHighlighter CodeHighlighter。
另外,缓存设置也很重要,可根据访问量、缓存击中率以及实际内存大小调整,可以提高论坛的性能。
图片上传路径的设置需要通过手工修改jiveHome下的jive_config.xml。打开jive_config.xml会发现,所有在管理配置中配置的信息都保存在这里,找到下列配置:
<upload>
<dir>/home/bqlr/jive/upload/</dir>
<relurl>upload/</relurl>
</upload>
其中,dir是上传图片存放的绝对路径;relurl是网址的相对路径,比如upload/,那就表示输入下列网址:“http://localhost:8080/jive/upload/xxxx.jpg”,就可以看到上传的图片。因为上传图片有自动缩小功能,因此需要Linux安装时是带图形X86的完全安装。用户如为中文名,将影响图片文件名。
以上是Jive论坛的安装,安装完成后的效果可以访问http://www.jdon.com/jive/。
8 小结
本章主要介绍了一个基于J2EE Web技术进行设计开发的论坛系统,通过这个系统的剖析,能够了解和掌握GOF设计模式,学会Java实战中一些处理技巧和技术。
使用GOF设计模式的主要优点:使得复杂系统的架构变得更加清晰而且有条理,而这一点正是许多程序员在开发实用系统中所缺乏的,可能导致的结果是大大降低Java系统可维护性以及可拓展性,重新回到了传统编程语言的陷阱中。
因此,GOF设计模式对于Java设计编程的重要性是无论怎么强调也不过分,它能够帮助程序员更加深入地理解Java完全面向对象特性,从而以真正的面向对象设计概念进行实用系统的设计和开发。
Jive系统是一个完全的Web系统,整个系统的最大特点是自我定制实现,它为了提高数据库的访问性能,使用了自己开发的数据库连接池;为了提高系统的数据处理系统,它使用了缓存机制;为了实现用户安全管理机制,它使用Proxy模式实现了角色权限的定位和检查等。这些模块功能在很多系统中都是需要的,但是如果想从Jive系统提炼出这些模块功能以达到重用,又是非常困难的。
因此,开发者需要一种具有一定高度的框架技术。在这个框架技术中,所有这些通用技术都能够自动实现,无需再自行设计和开发,能够将更多精力投入到与业务有关的特定功能开发中。J2EE的EJB技术实际就是这种框架技术。
学习和研究Jive论坛系统也非常有助于程序员学习和理解EJB和J2EE完整的框架技术,因为它们的目的都是一样,只不过实现的途径不一样而已