UML软件工程组织

Jdonjive源代码情景分析——过滤器篇

内容摘要:

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,代码就在DbForuminit()方法中:

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>&lt;/font&gt;&lt;font size=1 color=black&gt;</stringEnd>

                <reservedWordEnd>&lt;/b&gt;</reservedWordEnd>

                <commentStart>&lt;/font&gt;&lt;font size=1 color="#0000aa"&gt;&lt;i&gt;</commentStart>

                <commentEnd>&lt;/font&gt;&lt;/i&gt;&lt;font size=1 color=black&gt;</commentEnd>

                <stringStart>&lt;/font&gt;&lt;font size=1 color="#00bb00"&gt;</stringStart>

                <reservedWordStart>&lt;b&gt;</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>&lt;/font&gt;&lt;font size=1 color=black&gt;</stringEnd>

                <reservedWordEnd>&lt;/b&gt;</reservedWordEnd>

                <commentStart>&lt;/font&gt;&lt;font size=1 color="#0000aa"&gt;&lt;i&gt;</commentStart>

                <commentEnd>&lt;/font&gt;&lt;/i&gt;&lt;font size=1 color=black&gt;</commentEnd>

                <stringStart>&lt;/font&gt;&lt;font size=1 color="#00bb00"&gt;</stringStart>

                <reservedWordStart>&lt;b&gt;</reservedWordStart>

            </properties>

        </filter0>

    </forum1>

</jiveFilters>

1)jiveFilters.filterClasses 为论坛已经安装的过滤器,这些过滤器可以用于全局过滤和单个的版块

2)jiveFilters.global 为全局论坛安装的过滤器。

3)jiveFilters.global.filterCount 为全局论坛已安装过滤器的数目。

4)jiveFilters.global.filterXX为整数) 为安装的全局过滤器。

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的客户端是不知道区别的。为了便于理解,HTMLFilterclone()方法如下:

    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装饰了IMGFilterIMGFilter又被DateFilter装饰,则最后返回的是DateFilter,如果调用DateFilter中的message变量,那么将会产生的效果如下图:

过滤管理器

在这里即DBFilterManager,它实现了接口FilterManager,负责对过滤器进行管理,在DBFactoryDBForum中均有这个这个类的实例,使用它来管理全局和每个论坛的过滤器,例如解析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()方法,得到过滤器过滤完成的结果,并将结果放在原始消息对象的filteredSubjectfilteredBody字段中,这样就完成了缓存,我们来看一下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文件中filterXX越大将越后被加载),这样可以使得更多的可缓存过滤器的结果被缓存,不用和不可缓存的过滤器一起在消息反复调用时对消息过滤,造成不必要的开销。

 

参考资料:


原文出处: http://www.chenshen.com

 

 

版权所有:UML软件工程组织