JiveJdon业务核心规则之过滤器设计

上篇

  过滤显示是论坛动态功能的一项重要功能,原来Jive论坛提供很多显示动态的过滤功能,如Html过滤等,这样可以动态地扩展指定帖子显示格式,如果有广告商看中帖子中的几个关键字,我们可以通过增加过滤器讲这些关键字连接指向广告商的网址,等等。总之,对内容显示样式的多样需求变化要求我们的设计也是可以动态可扩展的。

过滤器设计

我们设计通过后台管理来动态安装显示过滤器,如下图显示:

过滤器种类主要区分:当前正在使用的过滤器;没有在使用适合的过滤器。
过滤器的管理都通过com.jdon.jivejdon.service.filter. MessageFilterManager实现,MessageFilterManager是接口FilterManager一个实现。
MessageFilterManager通过其构造参数中initProperties()方法初始化过滤器设置,过滤器设置数据是保存在数据库setup数据表中,setup是JiveJdon3.0新增的一个数据表,主要用来保存论坛的通用设置数据,原来Jive论坛是通过XML文件来保存设置数据,需要在服务器的绝对路径下写文件操作,这破坏了系统的可移植性和可伸缩性。在JiveJdon3.0中我们使用一个数据表来保存,setup结构如下:
CREATE TABLE setup (
  name varchar(50) NOT NULL default '',
  value text NOT NULL,
  PRIMARY KEY name (name) 
)
其中value字段是一个XML格式的字符串,例如显示过滤器的设置setup中name字段值是FilterManager 接口中定义的“FILTERS”,setup表value字段值是如下:

<?xml version="1.0" encoding="UTF-8"?>
<jiveFilters>
<filterClasses>
<filter0>com.jdon.jivejdon.service.filter.html.HTMLFilter</filter0>
<filter1>com.jdon.jivejdon.service.filter.html.Newline</filter1>
<filter2>com.jdon.jivejdon.service.filter.html.TextStyle</filter2>
<filter3>com.jdon.jivejdon.service.filter.html.URLConverter</filter3>
<filter4>com.jdon.jivejdon.service.filter.shield.Profanity</filter4>
<filter5>com.jdon.jivejdon.service.filter.codeviewer.JavaCodeHighlighter</filter5>
<filter6>com.jdon.jivejdon.service.filter.html.WordBreak</filter6>
<filter7>com.jdon.jivejdon.service.filter.html.QuoteFilter</filter7>
<filter8>com.jdon.jivejdon.service.filter.emot.Emoticon</filter8>
<filter9>com.jdon.jivejdon.service.filter.html.ImageFilter</filter9>
</filterClasses>
<global>
<filterCount>3</filterCount>
<filter0>
<className>com.jdon.jivejdon.service.filter.html.HTMLFilter</className>
</filter0>
<filter1>
<className>com.jdon.jivejdon.service.filter.html.TextStyle</className>
</filter1>
<filter2>
<className>com.jdon.jivejdon.service.filter.html.Newline</className>
</filter2>
</global>
</jiveFilters>
这个值是第一次执行时由MessageFilterManager的createProperties方法实现的。

显示过滤器

显示过滤器都已经在XML配置文件中安装准备就绪,下面是在帖子显示要插入显示过滤器,这只要调用MessageFilterManager的applyFilters方法即可。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;
}
通过遍历过滤器集合中的每个过滤器,调用过滤器的clone方法,实现该过滤器中的特殊过滤功能。
下面问题的关键是:在哪个点调用applyFilters方法介入过滤器显示?
在我们的JiveJdon3设计中,ForumMessage对象通常是保存在缓存中的,因此,第一选择是在ForumMessage加入缓存时介入过滤器显示,很显然,这时为时过早,缓存中的ForumMessage可能被用于修改或作其他目的,同时,因为缓存,过滤器是实时更新无法立即得到反映的,对功能有所影响。
所以,将缓存中的ForumMessage对象直接应用显示过滤器规则进行修改,并不是一个非常理想的方案,换句话:介入过滤器显示的切入点过于宽泛,那么能否在某点直接介入呢?
我们发现面向表现层的服务接口ForumMessageService的getMessage方法是用来获得ForumMessage对象的,因此,在这个方法点中切入是比较合适的,但是又有一个问题:getMessage方法因为方法名称包括get,而且方法返回结果是一个Model类型,符合Jdon框架中缓存拦截器com.jdon.aop.interceptor.CacheInterceptor拦截原则,因此,在表现层真正激活ForumMessageService的getMessage之前,CacheInterceptor将首先被激活,所以,getMessage方法实体并没有真正被执行。
这样,我们就不希望CacheInterceptor激活,那么比较方便的方式是更改ForumMessageService的getMessage方法名,我们改为findFilteredMessage,这样在ForumMessageService的子类ForumMessageShell中findFilteredMessage的方法如下:
public ForumMessage findFilteredMessage(Long messageId){   
logger.debug("enter findFilteredMessage");
return filterManager.applyFilters(super.getMessage(messageId));
}
至此,为了保证表现层每次都能够通过ForumMessageService的findFilteredMessage获得过滤后的ForumMessage,我们希望缓存对整个表现层都是封闭的,只有业务层能够访问缓存,为了做到这点,除了上述失效CacheInterceptor激活方法外,我们考察帖子显示的执行环节:
messageListAction.do ---> messageList.jsp
MessageListAction是Jdon框架ModelListAction的子类,ModelListAction主要是根据ForumMessage的ID集合,调用业务层的findFilteredMessage获得一个个ForumMessage实例,进而封装成集合,供messageList.jsp遍历。
在ModelListAction中调用业务层获得ForumMessage之前,会访问缓存,检查缓存中是否有已经保存进去的ForumMessage,因此,我们必须关闭这个缓存访问,这时只要覆盖下列方法就可以了:
protected boolean isEnableCache() {
return false;
}

调试问题

以上过滤器设计编程基本完成,调试时碰到一个非常令人头疼的问题:
在messageList中对ForumMessage集合进行遍历时,如下语句:
<logic:iterate id="forumMessage" name="messageListForm" property="list" indexId="i">
<bean:write name="forumMessage" property="messageId"/>
</logic:iterate>
总是报错:no getter method for the property messageId of the bean forumMessage,也是不是messageListForm集合list中安装的不是ForumMessage对象呢?
通过forumMessage.getClass().getName()打印是com.jdon.jivejdon.service.filter.html.Newline,而Newline是继承ForumMessageDecorator子类,而ForumMessageDecorator又是ForumMessage的子类,现在怎么会不认识ForumMessage呢?
再次检测确认:通过java语句forumMessage.getMessageId()输出没有问题,这说明我们前面逻辑设计正确,为什么Struts的标签<bean:write name="forumMessage" property="messageId"/>会报错呢?
经过研究发现,struts的标签是通过JavaBean反射机制检查某个Bean是否有某个方法,而com.jdon.jivejdon.service.filter.html.Newline等过滤器,我们已经通过NewlineBeanInfo指定其属性,这些属性中没有指定messageId,因而struts的标签会报错。
解决办法有两个:
更改NewlineBeanInfo代码,因为过滤器是可变的,而且数量多,修改量大,而且有这种规定,对于拓展新的过滤器很不方便。
让messageListForm中list包装的是真正ForumMessage集合。
第2方案比较方便,该方案比较方便,由于过滤的是内容ForumMessage的body字段,因此,我们将最终过滤后的bodu内容再装回ForumMessage当中,因此,更改ForumMessageService的子类ForumMessageShell中findFilteredMessage的方法如下:
public ForumMessage findFilteredMessage(Long messageId){   
logger.debug("enter findFilteredMessage2");
ForumMessage forumMessage = super.getMessage(messageId);

ForumMessageDecorator forumMessageDecorator = null;
ForumMessageDecorator[] filters = filterManager.getFilters();
for (int i = 0; i < filters.length; i++) {
if (filters[i] != null) {
logger.debug("enter applyFilter: " +  filters[i].getClass().getName());
forumMessageDecorator = filters[i].clone(forumMessage);
//将forumMessageDecorator过滤后的body装回forumMessage中
forumMessage.setBody(forumMessageDecorator.getBody());
}
}
return forumMessage; //返回的是货真价实的ForumMessage
}

 

下篇