内容摘要:
Jive作为学习设计模式的好教材,难度自然不低,我们就以Jive(J道版)为例程,带着大家来一次“面向过程式”的逐步阅读源代码,好像一次游览一般。本次浏览过滤器。
作者:蔡永航
(注:James Shen的学弟) 南京工业大学工商管理三年级学生
|
做过论坛的朋友都知道如果论坛的帖子中包含了HTML代码或者JS代码,将会产生安全隐患和破坏版面,并且贴子中的敏感信息也是令人头疼的问题。目前流行的解决方法就是在显示帖子的时候将贴子的内容进行过滤,Jive实现了一套不错的过滤机制,先将其剖析如下。先看一张图片,形象化理解一下。
一.简介
Jive有两处过滤方式,一处为全局过滤,即在此设置的过滤器将会应用于所有的论坛上面,一处为论坛的过滤器,即每个版块除了有全局的过滤器外,还可以应用该版块特有的过滤器,比如Java论坛就需要安装代码过滤器以格式化显示Java代码。过滤器又分为两种,一种为过滤的结果可以缓存的过滤器,一种过滤结果不能进行缓存的过滤器。举个例子,将Java代码格式的过滤器过滤的结果是可以进行缓存的,因为用户任何时候看到的内容随着时间的推移是没有变化的,但是如果过滤器过滤的结果随着时间的变化而变化的话,那么这个过滤的结果是不能缓存的,例如,消息中使用了[date/]标签,表示用户看到消息的时间,这个时候对[date/]标签的过滤结果就不可以缓存,因为这随着时间的推移而变化。我发现Jive在处理两个以上的不能缓存的过滤器时存在着错误缓存的问题,这个下面会讲道,并且给出解决方法。
二.入手点
我们想要了解过滤器的运行机制就需要找到初始化过滤器的地方,我们在DbForumFactory中可以找到初始化DbFilterManager的代码,如下:
filterManager
= new
DbFilterManager(
-1, this);
这里的-1表示该FilterManager为全局过滤器,上文说过不但有全局过滤器,还有每个版块特有的过滤器,大家想想因该在哪儿呢?不错,就在类DbForum中,当每个论坛创建的时候将同时创建该论坛的FilterManager,代码就在DbForum的init()方法中:
filterManager
= new
DbFilterManager(this.id,
factory);
这里的this.id就是论坛的ID。DbFilterManager会根据构造函数中的ID去位于WEB-INF\jiveHome中的jive_filters.xml取得相应的过滤器,并通过此ID保存论坛的全局和单个版块的过滤器。
为了理解Jive过滤器的实现,了解jive_filters.xml的文件结构至关重要。Jive中通过XXX.XXX来表示XML的文件结构,例如:
<jiveFilters>
<filterClasses>
<filterClasses>
</jiveFilters>
中的filterClasses通过jiveFilters.filterClasses获得,我们就按照这种方式分析jive_filters.xml的结构。
jive_filters.xml范例代码:
<?xml
version="1.0" encoding="UTF-8"?>
<jiveFilters>
<filterClasses>
<filter0>com.jivesoftware.forum.filter.HTMLFilter</filter0>
<filter1>com.jivesoftware.forum.filter.Newline</filter1>
<filter2>com.jivesoftware.forum.filter.TextStyle</filter2>
<filter3>com.jivesoftware.forum.filter.URLConverter</filter3>
<filter4>com.jivesoftware.forum.filter.Profanity</filter4>
<filter5>com.jivesoftware.forum.filter.CodeHighlighter</filter5>
<filter6>com.jivesoftware.forum.filter.WordBreak</filter6>
<filter7>com.jivesoftware.forum.filter.ImageFilter</filter7>
</filterClasses>
<global>
<filterCount>5</filterCount>
<filter0>
<className>com.jivesoftware.forum.filter.DateFilter</className>
</filter0>
<filter1>
<className>com.jivesoftware.forum.filter.HTMLFilter</className>
</filter1>
<filter2>
<className>com.jivesoftware.forum.filter.Newline</className>
</filter2>
<filter3>
<className>com.jivesoftware.forum.filter.CodeHighlighter</className>
<properties>
<stringEnd></font><font
size=1 color=black></stringEnd>
<reservedWordEnd></b></reservedWordEnd>
<commentStart></font><font
size=1 color="#0000aa"><i></commentStart>
<commentEnd></font></i><font
size=1 color=black></commentEnd>
<stringStart></font><font
size=1 color="#00bb00"></stringStart>
<reservedWordStart><b></reservedWordStart>
</properties>
</filter3>
<filter4>
<className>com.jivesoftware.forum.filter.DateFilter2</className>
</filter4>
</global>
<forum1>
<filterCount>1</filterCount>
<filter0>
<className>com.jivesoftware.forum.filter.CodeHighlighter</className>
<properties>
<stringEnd></font><font
size=1 color=black></stringEnd>
<reservedWordEnd></b></reservedWordEnd>
<commentStart></font><font
size=1 color="#0000aa"><i></commentStart>
<commentEnd></font></i><font
size=1 color=black></commentEnd>
<stringStart></font><font
size=1 color="#00bb00"></stringStart>
<reservedWordStart><b></reservedWordStart>
</properties>
</filter0>
</forum1>
</jiveFilters>
(1)jiveFilters.filterClasses
为论坛已经安装的过滤器,这些过滤器可以用于全局过滤和单个的版块。
(2)jiveFilters.global
为全局论坛安装的过滤器。
(3)jiveFilters.global.filterCount
为全局论坛已安装过滤器的数目。
(4)jiveFilters.global.filterX(X为整数)
为安装的全局过滤器。
(5)jiveFilters.global.filterX.className
这个就不必说了吧。
(6)jiveFilters.global.filterX.properties
为过滤器的属性。
(7)jiveFilters.forumX
(此处的X为论坛的ID号)
为论坛的过滤器设置。
(8)jiveFilters.forumX.filterCount
为此版块的过滤器数目。
(9)jiveFilters.forumX.filterX
(X为整数)为此版块的过滤器。
(10)jiveFilters.forumX.filterX.className
这个也不必说了吧。
(11)jiveFilters.forumX.filterX.properties
为过滤器的属性。
三.设计模式
在过滤器的设计中Jive采用了“装饰器”设计模式(详见《Java与模式》),类图如下:
这里具体实现类DBForumMessage与抽象类ForumMessageFilter均来自接口ForumMessage。同时ForumMessageFilter作为抽象类,其中规定了继承其的过滤器的行为,在ForumMessageFilter中含有对ForumMessage的引用,这样过滤器就可以对ForumMessage的具体子类进行装饰(或者称为包装),最后返回过滤器类,由于均继承于ForumMessage,所以调用ForumMessage的客户端是不知道区别的。为了便于理解,HTMLFilter的clone()方法如下:
public
ForumMessageFilter clone(ForumMessage
message){
HTMLFilter filter
= new
HTMLFilter();
filter.message =
message;
return
filter;
}
在clone()方法中,程序将传入方法的ForumMessage,放入新生成的自身类的实例的message变量中,同时返回自身类的实例,由于过滤器类继承自ForumMessageFilter,而ForumMessageFilter实现了ForumMessage接口,并且filter.message中的message为ForumMessageFilter中的变量,类型为ForumMessage,所有这一切都是面向对象多态的体现。当存在两个以上的过滤器的时候,通过过滤器的层层装饰,当调用最后一个返回的过滤器的message时将会产生链式反应,例如HTMLFilter装饰了IMGFilter,IMGFilter又被DateFilter装饰,则最后返回的是DateFilter,如果调用DateFilter中的message变量,那么将会产生的效果如下图:
四.过滤管理器
在这里即DBFilterManager,它实现了接口FilterManager,负责对过滤器进行管理,在DBFactory和DBForum中均有这个这个类的实例,使用它来管理全局和每个论坛的过滤器,例如解析jive_filters.xml文件,得到全局过滤器的类名和属性,并使用Class.forName(className).newInstance()将其实例化放入数组中这些工作,下面让我们来分析DBFilterManager中的代码:
DbFilterManager部分代码:
public
class DbFilterManager
implements FilterManager
{
private
static XMLProperties
properties =
null;
private
static ForumMessageFilter[]
availableFilters =
null;
private
ForumMessageFilter[]
filters;
private
int
uncacheableIndex =
-1;
private
DbForumFactory factory;
String context
= null;
/**
* Creates
a new
filter manager.
*
* @param
forumID the
forumID to
manage filters
on, or
-1
to manage
* global
filters.
* @param
factory a
forum factory
to use
for various
tasks.
*/
public
DbFilterManager(long
forumID,
DbForumFactory factory)
{
this.factory
= factory;
String name
= null;
if
(forumID
== -1)
{
name =
"global";
} else
{
name =
"forum" +
forumID;
}
// Make sure properties are loaded.
loadProperties();
// Now load up filters for this
manager.
context =
name +
".";
//context = global. or
context = forum1. or context = forum2.
// See if a record for this context
exists yet. If not, create one.
String fCount
= properties.getProperty(context
+ "filterCount");
//得到该类别过滤器的数目
if
(fCount
== null)
{
fCount =
"0";
}
int
filterCount =
0;
try
{
filterCount =
Integer.parseInt(fCount);
//将文字的fcount解析为整数类型的filterCount
} catch
(NumberFormatException
nfe)
{
}
// Load up all filters
filters =
new ForumMessageFilter[filterCount];
for
(int
i =
0; i
< filterCount;
i++)
{
try
{
String filterContext
= context
+ "filter"
+ i
+ ".";
String className
= properties.getProperty(filterContext
+
"className");
filters[i]
= (ForumMessageFilter)
Class.forName(className)
.newInstance();
(1)
// If this filter isn't cacheable, then
no further filters can
// be cached.
//if (!filters[i].isCacheable()
&& uncacheableIndex == -1) {
if
(!filters[i].isCacheable())
{
uncacheableIndex
= i;
(2)
}
// Load filter properties.
String[]
propNames =
properties.getChildrenProperties(filterContext
+
"properties");
(3)
Map filterProps
= new
HashMap();
for
(int
j =
0; j
< propNames.length;
j++) {
// Get the bean property name, which is
everything after
// the last '.' in the xml property
name.
filterProps.put(propNames[j],
properties.getProperty(filterContext
+ "properties."
+
propNames[j]));
}
// Set properties on the bean
BeanUtils.setProperties(filters[i],
filterProps);
} catch
(Exception e)
{
System.err.println("Error
loading filter " + i
+
" for
context " + context);
e.printStackTrace();
}
}
}
public ForumMessage
applyFilters(ForumMessage
message) {
(4)
// Loop through cacheable filters and
apply them
for
(int
i =
0; i
< filters.length;
i++)
{
if
(filters[i]
!= null)
{
message =
filters[i].clone(message);
}
}
return
message;
}
public ForumMessage
applyCacheableFilters(ForumMessage
message) {
(5)
if
(uncacheableIndex
== -1)
{
return
applyFilters(message);
} else
{
// Loop through cacheable filters and
apply them
for
(int
i =
0; i
< uncacheableIndex;
i++)
{
if
(filters[i]
!= null)
{
message =
filters[i].clone(message);
}
}
return
message;
}
}
public ForumMessage
applyUncacheableFilters(ForumMessage
message) {
(6)
if
(uncacheableIndex
== -1)
{
return
message;
} else
{
// Loop through uncacheable
filters and apply them
for
(int
i =
uncacheableIndex;
i <
filters.length;
i++)
{
if
(filters[i]
!= null)
{
message =
filters[i].clone(message);
}
}
return
message;
}
}
}
(1)通过取得的jiveFilters.global.filterX.className或者jiveFilters.forumX.filterX.className的值,使用Class.forName(className).newInstance()将对象实例化,并放入数组中。
(2)这个uncacheableIndex非常重要。由于存在可以缓存结果和不可缓存结果的过滤器,并且过滤器存放于一个数组中,所以需要得知第一个不可以缓存的过滤器的位置,从这个位置开始(包括那个位置)以后的过滤器结果无论是可以缓存还是不可以缓存,通通不可以缓存,只有这样才可以保证最终结果的正确。举个例子,如果程序中有四个过滤器,其中第三个和第五个为不可缓存的,如:filter1àfilter2àfilter3(不能缓存)àfilter4àfilter5(不能缓存),程序检测到第三个过滤器的结果不可以缓存,那么从第三个过滤器开始结果就不能够缓存了。例如,使用[date/]标签表示服务器当前时间,如果进行缓存的话,当用户调用这个消息的时候将看到的是缓存中的值,而不是当前时刻的值。
(3)读取过滤器额外设置的属性值,将其放置在散列表中,并通过
BeanUtils.setProperties()对bean进行设置。
(4)将过滤器逐个装饰(包装)在用户需要浏览的消息上,此刻不分可缓存的过滤器和不可缓存的过滤器。
(5)应用可缓存的过滤器,直到数组下标小于uncacheableIndex,这就是(1)提到的uncacheableIndex的用处。
(6)应用下标大于等于uncacheableIndex的过滤器,无论其为可缓存的还是不能够缓存的。
五.具体实现
所有的过滤器操作都集中于DbForumFactory中,我们查看getMessage()的代码:
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;
(1)
// See if the filter values are not
already cached.
if (message.filteredSubject
== null)
{
(2)
// Apply
global filters
filterMessage
= filterManager.applyCacheableFilters(message);
(3)
// Apply
forum specific filters if there were no uncacheable
filters
// at the global level.
if
(!filterManager.hasUncacheableFilters())
{
(4)
try
{
FilterManager
fManager =
getForum(forumID).getFilterManager();
filterMessage =
fManager.applyCacheableFilters(filterMessage);
}
catch
(Exception e)
{}
}
// Now, cache those values.
message.filteredSubject
= filterMessage.getSubject();
(5)
message.filteredBody
= filterMessage.getBody();
Hashtable filteredProperties
= new
Hashtable();
for
(Iterator
i =
filterMessage.propertyNames();
i.hasNext();
) {
String name
= (String)
i.next();
filteredProperties.put(name,
filterMessage.getProperty(name));
}
message.filteredProperties
= filteredProperties;
}
// Apply uncacheable
filters.
if (filterManager.hasUncacheableFilters())
{
(6)
// Apply
global uncachable filters and then all
filters for the forum.
filterMessage =
filterManager.applyUncacheableFilters(message);
try
{
Forum forum
= getForum(forumID);
filterMessage =
forum.getFilterManager().applyFilters(filterMessage);
(7)
}
catch
(Exception e)
{}
}
else
{
// Apply
any forum specific uncacheable filters.
try
{
Forum forum
= getForum(forumID);
filterMessage =
forum.getFilterManager().applyUncacheableFilters(
message);(8)
}
catch
(Exception e)
{}
}
return
filterMessage;
}
(1)ForumMessage
filterMessage =
null;申明了一个安装了过滤器的消息,注意这个方法的最后一行,返回的是filterMessage,即已经安装了过滤器的消息,当调用这个实例的程序使用getBody()方法时,将直接作用于安装了过滤器的消息,并触发一连串安装于其上的过滤器对消息的主体内容进行过滤,效果如设计模式章节中的插图所示。
(2)判断从缓存或者从数据库得到的消息对象中的filteredSubject是不是为空,如果为空,则表示该消息还没有经过过滤器的过滤。
(3)对消息应用可以缓存的过滤器。
(4)如果该论坛存在不可缓存的过滤器,那么就先将该论坛特有的不可缓存的过滤器装饰在消息上。
(5)在应用了可缓存装饰器的filterMessage对象上调用getSubject()和getBody()方法,得到过滤器过滤完成的结果,并将结果放在原始消息对象的filteredSubject和filteredBody字段中,这样就完成了缓存,我们来看一下DBFourmMessage中的getSubject()和getMessage()方法就明白了。
getSubject()代码:
public
String getSubject()
{
// Return cached filter value if it
exists.
if
(filteredSubject
!= null)
{
return
filteredSubject;
}
// Otherwise,
return normal value.
else
{
return
subject;
}
}
当filteredSubject不为空的时候就直接返回filteredSubject,并且结合(2)中的代码,当filteredSubject不为空的时候就不必应用可缓存的过滤器了,直接进入不可缓存的过滤器的装饰。
(6)如果含有不能缓存的过滤器就在下面给消息装饰上。我觉得这里代码有问题,因该将第7句和第8句互换位置,您说呢?
六.BUG
大家看一下过滤管理器那节的代码,我认为存在bug,代码如下:
if
(!filters[i].isCacheable())
{
uncacheableIndex =
i;
}
如果存在两个以上的不可缓存的过滤器的话,那么uncacheableIndex将只能定位于最后一个不可缓存的过滤器上,按照可缓存过滤器的调用逻辑,此时的不可缓存的过滤器过滤的结果也将被错误的缓存,因为此时的uncacheableIndex定位在最后一个不可缓存的过滤器上。
因该修改为:
if
(!filters[i].isCacheable()
&& uncacheableIndex!= -1)
{
uncacheableIndex =
i;
}
鉴于Jive过滤器的实现原理,为了提高性能,尽可能将不可缓存的过滤器放在最后加载(即在jive_filters.xm文件中filterX的X越大将越后被加载),这样可以使得更多的可缓存过滤器的结果被缓存,不用和不可缓存的过滤器一起在消息反复调用时对消息过滤,造成不必要的开销。
参考资料:
无
原文出处:
http://www.chenshen.com
|