Sarkuya's Java Garden

Main | Next page »

http://blog.matrix.org.cn/Sarkuya/date/20070712 星期四 2007年07月12日

Building a Simple 2 Columns Equal Height Fullsize CSS Template

What's the purpose?

The way to construct a beautiful and useful web site is NOT necessarilly so complicated that everyone of us should have to spent lots of time on it. Especially when CSS comes into the view. However, the CSS solution is not that simple as we might thought as to equal height of the multi columns. The purpose of this template is to show you how to construct a simple fullsize web page of that kind with CSS. This template is particularly useful for blog for its simplicity, well organized structure, large area for articles, and freedom of changing the font size.

How to use?

Download the html file (CSS style inline) and keep it as your start-up. Add image background. Change the content and theme to what you like. And finally, add up your favorate CSS rules.

The default chartset is set to "iso-8859-1". Please switch to the correspondent charset. For example, to use Chinese charset, change it to "gb2312".

Has it been tested?

The template has been tested under IE6/Win and Firefox.

Why so many repeating Welcome and Useful Links in the sideBar?

For testing the equal heights of the two columns. With so many sidebarBoxes, the height of sideBar will exceed the height of mainContent so you can see the effect in such situation. Then try to resize the browser to a smaller size by dragging at the right edge of it to increase the height of mainContent column, you will confirm the concept of "equal height".

Is there any known issue?

Yes, I have caught one. While resizing the browser to a different size, or sometimes refreshing or maximizing the page, IE6/Win does not render the bottom section of the "mainContent" correctly -- the background color of sideBar streches through. However, scroll up the mouse and then scroll down (to cause the browser render again), you should find the interferring color is gone and the missing texts are back there. It should be the bug of IE6/Win in realtime rendering. Such problem does not exist in Firefox. Solution for this issue is wanted and appreciated.

Is it free?

Yes, it's free under whatever circumstances, including directly usage and freely further modification.

http://blog.matrix.org.cn/Sarkuya/date/20070507 星期一 2007年05月07日

中国不需要军国主义

(文为自己多年前关于军国主义的一个回帖,仅作收录)

多几个军国主义分子并没有什么作用,十多亿人口的大国,多几千个或几万个军国主义分子,于事无济。而像日本一样,皆是军国分子,于中国也无助。

军国主义是唯武治国论,最终通过扩张,通过武力来征服他国或异族,达到奴役他人、掠夺他人资源的目的。纵观日本历史,其军国主义的出现均是有因有缘的。日本的军国主义原因有三:一是国土狭小,资源紧缺,四周环海,交通不便,先天营养不良。在科技、通讯不发达的年代,这些都是致命的障碍;二是日本幕府时代武士精神的影响根深蒂固,武士群体的崛起左右着日本的政治。中国的武者崇尚侠义,救天下之心昭然于世,且武者从于政治,天下为公。日本从中国学到的是以武力来毁没,却未学到以武力来救济天下。三是近代文明的发展始于西方,日本以身处亚洲为耻,立志于成为“东洋”大国,在亚洲诸国都处于弱势时,不免要大打出手,强行出头,向进化到西方文明捞取一张通行证。再看今日之日本,这三个原因尚还存在,军国主义的事件屡出不断,也就不足为奇了。

虽为军国,但日本并非只是中国的宿敌,而是亚洲各国的活火山,不知何时又要死灰复燃,岩浆乱迸,祸害千国,遗臭万年。故中国只应联合亚洲各国,提防日本,而不必一昧认作死敌,置之于死地而后快。因此赵薇之举虽有出格,但恐非本意,疑是受了小人之惑利而致,出了这种事,使其及国人均明白到中日症结所在,裂痕之深,也算是达到了目的,不应再死打不放了。别国尚要连纵,类似于赵这样的人不是汉奸,自然要团结和争取。

天下没有永远的利益,也没有永远的冲突;没有永远的盟友,更没有永远的敌人。美国与日本可谓是天敌了。“珍珠港”事件,是美国建国以来,第一次在本土遭受到外敌袭击,恨不得生嗜其肉,活扒其皮,方能解恨;而广岛、长崎上空的蘑菇炽云,又要导致多少名日本冤鬼寻上门来,休缠不放?但为了抗衡中苏的联盟,不惜撇下昔日情仇,吴蜀联姻,虽是各怀鬼胎,也算是有所补偿。美英铁杆一个,但事关英阿之战,巴以冲突,兄弟暗中较劲,好戏纷呈。法德宿敌,但为争取政治地位和独立空间,相互也不乏走门窜亲之为。中苏(俄)分而又合,合而又分,续而又合,皆不离维护自己利益之宗旨。是而连纵远交的精髓,均是各国要员所不会忘的。

中国并不需要军国主义。中国百年磨难,自是列强所致,也并非军国主义所能救的。日本败了,伊拉克也败了,军国主义救不了他们。苏联这个当年的阵营老大,不顾自己的经济老底,一意发展尖端产业,为最终解体埋下了祸根,军国主义也不能从最根本震动对手。

“不战而屈人之兵”才会永远是战争的女神。陈水扁献媚美国,李登辉、吕秀莲寻根日本,目的就是要当幕后人,达到这种效果。中国也要亲近战争女神,但不应是这种三流水准。因此中俄联盟、上海五国,都是不针对任何一国或一方,更何况这种联盟要弱于对手,只能处于守态。实力、道义与发展是驶向战争女神的快车,也是中国最终强大的唯一捷径。

http://blog.matrix.org.cn/Sarkuya/date/20070420 星期五 2007年04月20日

改进Spring中的分页技术

Spring中有一个PagedListHolder,可以实现分页。但此类有几个缺点:

1. 使用此类的代码比较繁琐
2. 此类存放的数据源是所有的记录集,即对于记录数为1000条的数据,即使我们只需在一个页面中显示10条记录,每次均需要检索1000条记录出来,并且没有内在的缓存机制
3. 如果需将pageSize, maxLinkedPages这些一般为Session级的变量存于Session中,则必须在Session中存放PagedListHolder,从而导致大容量的数据常常撑满了Session
4. 只是实现了Serializable标识接口,且getPage(), setPage(), setPageSize()方法中直接使用newPageSet (private) 的属性,不利于子类覆盖。而且,内部类的各个方法耦合极强。特定方法的使用必须信赖于某个方法或标志变量作为前提条件。

比较理想的情况是,根据每一个HttpServletRequest产生一个PagesListHolder,不管记录总数有多少个,每次只检索页面上所显示的记录,但将pageSize, maxLinkedPages设为Session级的效果。

鉴于上述几点,我从Spring原有的PagedListHolder抽取出一些必需的方法名作为接口,并以一个名为RequestPagedListHolder的类实现之。

下面是抽取出来的PagedListHolder接口。

package com.sarkuya.web.pagination;

import java.io.Serializable;
import java.util.List;

/**
 *
 * @author Sarkuya
 */
public interface PagedListHolder extends Serializable {
    public static final int DEFAULT_PAGE_SIZE = 10;
    public static final int DEFAULT_MAX_LINKED_PAGES = 10;
   
    public void setRecordsSubst(List recordsSubset);
    public void setRealRecordCount(long realRecordCount);
   
    /**
     * 设置每页应有多少条记录。
     */
    public void setPageSize(int pageSize);
   
    /**
     * 返回每页共有多少条记录
     */
    public int getPageSize();
   
    /**
     * 根据pageSize,返回共有多少页
     */
    public int getPageCount();
   
    /**
     * 返回当前页码。
     * 首页为0
     */
    public int getPage();
   
    /**
     * 设置当前页码。
     * 首页为0
     */
    public void setPage(int page);
   
    /**
     * 设置围绕当前页最多可以显示多少链接的页数。
     * 此方法<strong>会</strong>影响getFirstLinkedPage()及getLastLinkedPage()
     */
    public void setMaxLinkedPages(int maxLinkedPages);
   
    /**
     * 返回围绕当前页最多可以显示多少链接的页数
     */
    public int getMaxLinkedPages();
   
    /**
     * 返回首页的页码
     */
    public int getFirstLinkedPage();
   
    /**
     * 返回最后一页的页码
     */
    public int getLastLinkedPage();
   
   
    /**
     * 转至前一页。
     * 如果已经是首页,则停在该页。
     */
    public void previousPage();
   
    /**
     * 转至下一页。
     * 如果已经是最后一页,则停在该页。
     */
    public void nextPage();
   
    /**
     * 转至首页。
     */
    public void firstPage();
   
    /**
     * 转至最后一页
     */
    public void lastPage();
   
    /**
     * 返回总的记录数
     */
    public long getNrOfElements();
   
    /**
     * 返回在当前页面上的第一个记录在所有记录(从0开始)中的编号
     */
    public int getFirstElementOnPage();
   
    /**
     * 返回在当前页面上的最后一个记录在所有记录(从0开始)中的编号
     */
    public int getLastElementOnPage();
   
    /**
     * 返回在当前页面上的所有记录
     */
    public List getPageList();
}

setRecordsSubst()用于存放页面显示的记录源,而setRealRecordCount()用于记录满足条件的记录总数。

下面是此接口的实现:

package com.sarkuya.web.pagination;

import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.bind.ServletRequestDataBinder;

/**
 *
 * @author Sarkuya
 */
public class RequestPagedListHolder implements PagedListHolder {
    private static int pageSize = DEFAULT_PAGE_SIZE;
    private static int maxLinkedPages = DEFAULT_MAX_LINKED_PAGES;
   
    private int page = 0;
    private List recordsSubset;
   
    private long realRecordCount;
   
    /** Creates a new instance of RequestPagedListHolder */
    public RequestPagedListHolder(HttpServletRequest request, long realRecordCount, PagedListProvider pagedListProvider) {
        setRealRecordCount(realRecordCount);
       
        ServletRequestDataBinder binder = new ServletRequestDataBinder(this);
        binder.bind(request);
       
        checkPageNavigation(request);
       
        setRecordsSubst(pagedListProvider.getRecordsSubset(getPageSize() * getPage(), getPageSize()));
    }

    private void checkPageNavigation(final HttpServletRequest request) {
        String pageNavAction = request.getParameter("pageNavAction");
        if (pageNavAction != null) {
            if (pageNavAction.equals("firstPage")) {
                firstPage();
            } else if (pageNavAction.equals("previousPage")) {
                previousPage();
            } else if (pageNavAction.equals("nextPage")) {
                nextPage();
            } else if (pageNavAction.equals("lastPage")) {
                lastPage();
            }
        }
    }
   
    public void setRecordsSubst(List recordsSubset) {
        this.recordsSubset = recordsSubset;
    }
   
    public void setRealRecordCount(long realRecordCount) {
        this.realRecordCount = realRecordCount;
    }
   
    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }
   
    public int getPageSize() {
        return pageSize;
    }
   
    public int getPageCount() {
        float nrOfPages = (float) getNrOfElements() / getPageSize();
        return (int) ((nrOfPages > (int) nrOfPages || nrOfPages == 0.0) ? nrOfPages + 1 : nrOfPages);
    }
   
    public int getPage() {
        if (page >= getPageCount()) {
            page = getPageCount() - 1;
        }
        return page;
    }
   
    public void setPage(int page) {
        this.page = page;
    }
   
    public void setMaxLinkedPages(int maxLinkedPages) {
        this.maxLinkedPages = maxLinkedPages;
    }
   
    public int getMaxLinkedPages() {
        return maxLinkedPages;
    }
   
    public int getFirstLinkedPage() {
        return Math.max(0, getPage() - (getMaxLinkedPages() / 2));
    }
   
    public int getLastLinkedPage() {
        return Math.min(getFirstLinkedPage() + getMaxLinkedPages() - 1, getPageCount() - 1);
    }
   
    public void previousPage() {
        if (!isAtFirstPage()) {
            page--;
        }
    }
   
    public void nextPage() {
        if (!isAtLastPage()) {
            page++;
        }
    }
   
    public void firstPage() {
        setPage(0);
    }
   
    public void lastPage() {
        setPage(getPageCount() - 1);
    }
   
    public long getNrOfElements() {
        return realRecordCount;
    }
   
    public int getFirstElementOnPage() {
        return (getPageSize() * getPage());
    }
   
    public int getLastElementOnPage() {
        int endIndex = getPageSize() * (getPage() + 1);
        return (endIndex > getNrOfElements() ? (int)getNrOfElements() : endIndex) - 1;
    }
   
    public List getPageList() {
        return recordsSubset;
    }
   
    public boolean isAtFirstPage() {
        return getPage() == 0;
    }
   
    public boolean isAtLastPage() {
        return getPage() == getPageCount() - 1;
    }
}

此类有以下特点:

1. pageSize及maxLinkedPages均设为static,这样不因每个Request而改变。因此用户不必每次显示一个不同的页面后都在UI中重新设置它们。
2. 在构造函数中包装了所有的使用过程,既简化了该类的使用,也保证了该类被正确初始化。
3. 摒弃了newPageSet变量,减少了各个方法的耦合强度。
4. 在Spring环境中使用了ServletRequestDataBinder,大大简化了各个参数的读取设置过程。
5. 通过回调机制,每次只检索PagedListProvider所提供的记录子集,节约了内存,提高了程序效率。

不难看出,PagedListProvider是个接口,只有一个方法:

package com.sarkuya.web.pagination;

import java.util.List;

/**
 *
 * @author Sarkuya
 */
public interface PagedListProvider {
    public List getRecordsSubset(int firstResult, int maxResults);
}

熟悉Hibernate的用户知道,Hibernate中就是需要这两个参数来实现分页了。如果不使用Hibernate,也没关系,自己实现此接口就行了。(接口实现起来很简单,但技术细节却非简单,Hibernate用户在此居于明显的优势)

以上的两个接口,一个实现类,便是经过改进后的分页技术了。下面看其使用方法。

当用户需要查看带有分面功能的页面时,都会由下面的方法处理:

    private void setPageListForView(HttpServletRequest request, ModelAndView mav, final String tableName) {
        long totalRecordsCount = adminService.getTotalRecordCount(tableName);

        PagedListProvider listProvider = new PagedListProvider() {
            public List getRecordsSubset(int firstResult, int maxResults) {
                return (List) adminService.listTable(tableName, firstResult, maxResults);
            }
        };
       
        PagedListHolder holder = new RequestPagedListHolder(request, totalRecordsCount, listProvider);
       
        mav.addObject("pagedRecords", holder);
    }

这是一个简单的方法,为RequestPagedListHolder的构造函数准备好实参后,生成一个实例,将其置于页面的一个名为"pagedRecords"的attribute中,以供JSP读取。

adminService.getTotalRecordCount()应不难实现。主要是getRecordsSubset()。Service层的listTable()如下:

    public Collection listTable(String tableName, int firstResult, int maxResults) throws DataAccessException {
        return ((HibernateDao) daoFactory.getDao(tableName)).findByCriteria(firstResult, maxResults);
    }

Dao层代码:

    public Collection findByCriteria(int firstResult, int maxResults) throws DataAccessException {
        DetachedCriteria criteria = DetachedCriteria.forClass(getEntityClass());
        return getHibernateTemplate().findByCriteria(criteria, firstResult, maxResults);
    }

下面看看视图层的使用。

    ......
    <c:forEach items="${pagedRecords.pageList}" var="record">
      ......
    </c:forEach>
    ......

通过JSTL方便地读出pagedRecords变量的pageList属性。重抄一下上面的RequestPagedListHolder代码相应部分:

    public List getPageList() {
        return recordsSubset;
    }

返回的正是Hibernate已经为我们检索出来的记录子集。

下面是图例(参考了NetBeans 5.5 Visual Web Pack的界面设计)。

图1. 数据库中共有8条记录,每页10行,一页全部显示完毕

图2. 设为每页5行,显示第0页。尽管共有8条记录,但实际上只检索了5条记录。

图3. 每页5行时的第1页,这次只检索了3条记录。且pageSize的值在同一个HttpServletSession中自动保存了下来。

经过这样改进后,代码耦合性大大减小了,也利于以后的扩展。

http://blog.matrix.org.cn/Sarkuya/date/20070120 星期六 2007年01月20日

调试Web应用的梦魇终于远去

以前在使用NetBeans开发Web应用时,当修改了某些类而增量部署,经常由于出现"8080端口已被占用"的原因而失败,只好重启计算机,猖獗时一天居然重启不下5、6趟,这还是开发程序吗?疑犯可能是:NetBeans, Tomcat, Ant,以及JDK。曾有一段时间,认定是NetBeans的问题而弃之不用,改为使用纯文本编辑器编写Ant脚本来开发。但运行后,还是会出现这个问题。

与NetBeans别离后,虽然获得了较大的自我满足感 -- 逐行编写代码,与Ant亲密交往,自制开发环境,但发现开发效率显著下降,不得已重返NetBeans,哼着小曲,"快乐"地重启着电脑。

JDK6.0出来后,下载了其与NetBeans 5.5捆绑的版本,安装运行后发现,若有部署不成功情况下,Tomcat的输出窗口中经常有Perm outofspace的字样,然后在NetBeans中停止Tomcat的运行,再次启动Tomcat,故障排除。不敢相信困扰已久的老大难就这样轻易被解决了,在另一台电脑上有意不装JDK6.0,老问题无法避免。对比一段时间后,此机器也装上了JDK6.0,故障也排除了。终于确认,原来的JDK就是罪魁祸首。明白了此道理,如果在NetBeans中重启Tomcat还不能解决问题,也好办,退出NetBeans,调出Windows的任务管理器,结束其中的java.exe进程,宿主在Java中的Tomcat也自动退出并不再占用相应端口。重启NetBeans,一切又恢复了正常。

http://blog.matrix.org.cn/Sarkuya/date/20061224 星期日 2006年12月24日

关于Object的equals()及hashCode()

在某些时候,我们需要判断两个对象是否相等。Java的每个类都继承于Object类。它使用equals()hashCode()这两个方法来判断两个Object是否相等。

 

1.         equals()

对于非null的对象引用,依下面关系进行判断:

l         对于任一非null引用xx.equals(x)应返回true

l         对于任一非null引用xy,仅在y.equals(x)返回true时,x.equals(y)才返回true

l         对于任一非null引用xyz,如果x.equals(y)true,而且y.equals(z)true,则x.equals(z)应返回true

l         对于任一非null引用xy,如果用于比较的信息没有改变,无论多少次调用x.equals(y)都会恒定地返回truefalse

l         对于任一非null引用xx.equals(null)应返回false

 

Object的默认实现是只有在两个Object的引用相等时,才会返回true,即return x == y;

 

如果要覆盖(override)此方法,需要同时覆盖hasCode(),要求是:两个相等的对象必须有相等的hash code

 

2.         hashCode()

其必须遵循的约定是:

l         在一个Java应用的一次运行中,如果用于equals()比较的信息未变,同一个对象的hasCode()必须恒定地返回相同的整数。但在同一个应用的另一个运行中,此值可以改变

l         如果根据equals(),两个对象均相同,则调用这两个对象的hasCode()必须返回相同的int值。

l         如果equals()返回false,这两个对象的hashCode()可以返回相同的int值。但不等的两个对象返回不同的int值可以提高hashtables的运行效率。

 

作为常理,不相等的对象的hasCode()应可能地返回不同的int值。(通常可以将对象的内存地址转换为int值后返回,但这不是Java语言所要求的)

 

3.         小结

l         hashCode()必须配合equals()来实现判定两个对象是否相等

l         由于默认的equals()方法是使用是否同一对象的引用来判断是否相等,由于默认的hashCode()返回对象的内存地址,而不同的对象有不同的地址,因此,如果未覆盖equals(),也不需覆盖hashCode()。但有一点需注意,由于内存地址比Integer的数值要大,因此不同地址所转换成的int值可能会一样。即使出现这种情况也不要紧,仅影响hashtable的运行效率而已。

l         hashCode()的返回值必须根据用于equals()判断的信息来决定。如果equals()返回truehashCode()必须返回相同的int值。如果equals()false,最好返回不同的int值。

 

这样,只要符合上面3点条件,采用何种公式或方式来实现hasCode()均是可行的。

 

4.         实例

尽管道理很浅显,但要实现起来并不容易,尤其是hashCode()。考虑一下两个对象,如果它们的一个String的字段相等就可认为两个对象相等。这种情况下,应如何根据这个String的字段来返回相同或不同的int值?相等时很好办,用以下的语句即可解决:

 

if (strA.equals(strB)) {

  return 1;

}

 

但不等时呢,难点在于如何将不等的String值转换为不同的int值。而且,如果这两个对象均出自同一类,您所编写的代码均适用于这两个对象,同样的代码,如何才能出现不同的结果?如果通过硬编码的方式,先取得另一个对象的hashCode()值,再依此修改本对象的hasCode()值,势必造成每次比较都会导致本身的hasCode()值频繁发生变化。而如果同一类的不同实例使用各自的算法,从而无需提取另一个对象的hashCode()值,但在比较时,又如何实时地了解对方的情况?我们陷入了两难的境地。

 

好消息是,jakarta-commons中一个lang包,其EqualsBuilderHashCodeBuilder类可方便地解决此问题。对于equals()hashCode(),均有硬编码及反射机制的两种解决方法。

 

EqualsBuilder

先看码编码方式:

 

 public boolean equals(Object obj) {

   if (obj instanceof MyClass == false) {

     return false;

   }

   if (this == obj) {

     return true;

   }

   MyClass rhs = (MyClass) obj;

   return new EqualsBuilder()

                 .appendSuper(super.equals(obj))

                 .append(field1, rhs.field1)

                 .append(field2, rhs.field2)

                 .append(field3, rhs.field3)

                 .isEquals();

  }

 

在进行基本的判断后,通过EqualsBulder.append()的方式,将用于区分两个对象的各个业务关键逻辑逐一增加至其实例中就可以了。

 

下面是反射机制:

 

 public boolean equals(Object obj) {

   return EqualsBuilder.reflectionEquals(this, obj);

 }

 

代码更简洁。

 

HashCodeBuilder

先看硬编码方式:

 

 public class Person {

   String name;

   int age;

   boolean smoker;

   ...

 

   public int hashCode() {

     // you pick a hard-coded, randomly chosen, non-zero, odd number

     // ideally different for each class

     return new HashCodeBuilder(17, 37).

       append(name).

       append(age).

       append(smoker).

       toHashCode();

   }

 }

 

这里,name, age, smoker等字段均为在equals()中使用的信息。任选两个随意的非零奇数作为构造函数的参数,并逐一加至HashCodeBuilder的实例中。

 

再看反射机制:

 

 public int hashCode() {

   return HashCodeBuilder.reflectionHashCode(this);

 }

 

同样也很简单。

 

尽管通过反映机制看起来比较简单,但一是速度较慢,二是要受到安全机制的制约,因此,应尽可能地使用硬编码的方式。

 

http://blog.matrix.org.cn/Sarkuya/date/20060913 星期三 2006年09月13日

在NetBeans中使用CVSNT详解

1. 在D盘上新建文件夹,d:\CVSRepository

2. 安装CVSNT,重新启动

3. 所有程序 -> CVSNT -> CVSNT Control Panel -> Repository configuration -> Add。点击Location文本框右边的"..."按钮,选择d:\CVSRepository。CVSNT自动在Name文本框加上/CVSRepository。按"OK"按钮后,提示"D:/CVSRepository exists, but is not a valid CVS repository. Do you want to initialise it?" 按"是"按钮,让CVSNT自动为此目录完成库的初始化工作。

4. 在CVSNT的About标签页中,确保CVSNT及CVSNT Lock都处于"Running"状态

5. 控制面板 -> 系统 -> 高级 -> 环境变量 -> Sarkuya的用户变量 -> 新建
   CVS_HOME: C:\Program Files\CVSNT
   CVSROOT: :pserver:Sarkuya@localhost:/CVSRepository
   在path加上: %CVS_HOME%;

6. 打开命令行窗口。
   "d:"回车,转到d:盘。
   "md work",创建一个名为work的目录。
   "cd work",转入work目录。
  
7. "cvs login",准备登录。因为我们使用pserver的登录方式,默认方式下,以Windows操作系统的用户名与密码来验证用户。如果我们未设置第5步的CVSROOT参数,则必须输入
   "cvs -d :pserver:Sarkuya@localhost/CVSRepository login"
   在CVS Password:的右侧输入登录Windows操作系统的密码
  
8. 输入"cvs co -l -d tmp .",签出/CVSRepository的从根目录开始的所有路径,并放在tmp目录中。"."代表CVSROOT中的当前目录,当前情况下即根目录

9. 输入"cd tmp",回车。输入"md Programming" 回车。输入"cd Programming",回车。输入"md Java"。依此方法按如下方法增加子目录。
   tmp
     |--> Programming
       |--> Java
         |--> Product
           |--> Organization
           |--> Personal
         |--> Test
   Product中可存放产品应用,而Test中可存放实验性的应用。读者可根据自己的需要,自行组织目录结构。
  
10. 输入 "cd d:\work\tmp",转入到tmp目录中。输入"cvs update",可看到在Programming前面有一个"?",说明CVS知道工作目录下有一个Programming的目录,但未加入库中。
    输入"cvs add Programming",将此目录加入库中。
    输入"cvs update",可看到Programming已加入库中,但这时CVS又对Programming下的Java目录打了问号。
    依此步骤,转入适当的子目录,将所有的目录均加入CVS库中。
    最后,可返回到tmp目录下,通过"cvs update"来验证所有已加的目录。
   
11. 由于我们只是增加目录,cvs add xxx已经自动提交变更,因此无需再进行cvs commit了。
    返回到work目录下,输入"cvs release -d tmp", 按"y"确认,释放tmp目录,并删除本地工作区中的tmp目录。

12. "cvs logout",退出本次登录。

13. (可选)由于在CVS中更改目录比较费劲,因此,当我们做完第12步后,马上将d:\CVSRepository复制到一个另一个目录上做备份。以后需要时直接复制回来,并重命名为"CVSRepository"就行了。因此,CVS的库备份是极其简单的。

14. 打开NetBeans 5.0,新建一个项目,HelloWorld,按右键,选"清理项目",从菜单中选"CVS -> 导入到库中"。
    在CVS根的文本框中填写:":pserver:Sarkuya@localhost:/CVSRepository"。
    在口令文本框中填上密码。
    下一步。
    在导入消息文本框中输入"初始导入",可随意填。
    点击库文件夹文本框右边的"浏览"按钮,选/CVSRepository/Programming/Java/Product/Organization。然后,修改库文件夹文本框内容,将"/CVSRepository/Programming/Java/Product/Organization",改为"/CVSRepository/Programming/Java/Product/Organization/HelloWorld"。否则,将把应用的所有内容直接放在Organization之下,导致无法在该目录下存放多个应用。
    确认导入后执行签出打上钩,可以在导入后立即打开该项目。
    在通过CVS菜单提交所有文件后,可退出NetBeans,将HelloWorld所在目录都删除。因为在服务器模式下(只需将CVSROOT中的localhost改为实际的服务器地址就行了),可能有其他成员也使用了库,当你休息时,他们可能还在干活。因此,下次进来时,重新签出代码可保证代码最新,从而加大CVS的提交成功率。如果不想删除HelloWorld目录,在下次进来时可不用签出,但在开始工作前,最好CVS -> 更新所有文件。
   
15. NetBeans中签出很简单,CVS -> 签出,依14步设置CVS根及口令,选择我们所要的模块,并指定工作夹就行了。

16. 需要注意的是,即使我们通过:local:的方式来访问库,在NetBeans中也需要通过CVSNT来提供服务,否则的话,通过指定盘符+路径的形式,如:local:D:\CVSRepository,在命令行可以连接库的语句,放在NetBeans中,它会一直试着连接,但不会成功。

17. 如果我们需要在单位及家里同时使用CVS,一般情况下,我们先采取在单位及家里同时安装CVS库的方式。但由于从CVS库中导出的代码,其各个目录下面会带有CVS的目录,里面忠实记载着CVS库的结构,因此,从两个不同的库分别导出的源码会不一致,容易出错。此时,我们可以采取以下的方法之一:
    a) 如果你确信今晚没人再用此仓库了,可从服务器上将库所在的目录全部复制下来。对于记录代码的CVS库,其容量很小,一个U盘可以搞定。然后先删除家里的库,再贴上从单位复制的库。当然,CVSROOT可能要重新设置。这种情况,必须确保,家里的库没有需要保留的内容。谨记!
       总结其条件如下:
       1) 单位的仓库容量要小
       2) 当天晚上应该没有其他人需要更新单位的仓库
       3) 家里的仓库被单位的仓库替换后,确保没有损失任何代码。
       一般情况下,这种情况比较适合经常加班加点,极少回家的人。因此,女孩们,一旦发现你们的丈夫经常这么做,在读过本文后,你该知道除了要注意提防第三者外,别忘了还要提防这个小小的U盘!要让他们每天都得写一篇保证书:我今天没将单位的仓库往家里倒!
    b) 通过checkout部分单位仓库代码,回家后,同样checkout需要覆盖的代码,手工逐一替换代码,然后update,看看有无冲突,如果没有,则可以提交。完工并提交代码后,再次checkout当晚所修改的代码,带回单位也是如此操作。
       这种方式,要求两者仓库的结构越相近越不容易出错。最好在命令行方式操作,自由度稍大点。

http://blog.matrix.org.cn/Sarkuya/date/20060612 星期一 2006年06月12日

Hibernate 3.1 Reference第7章译注

Hibernate的关联映射存在着多种情况,如果能把这一块吃透了,对付Hibernate就相对轻松多了。近日在看Hibernate 3.1 Reference过程中,发现其第7章写得比较详实,便快速翻译下来,并加入了自己的一些注解。由于只追求翻译速度,不讲究译文措辞,因此有些地方可能比较别扭。现公布出来,希望对Hibernate的粉丝们能有一些帮助。

本译文中将第7章变成了第3章,是因为这段译文在我的一篇学习笔记中正好是第3章,贴过来时未加以改动。

以下是其译文。


3 映射

3.1 简介

关联映射是最难对付的问题。在此部分中我们将逐一地处理各种情况。从单向映射开始,然后是双向映射。我们将在全部例子中使用Person及Address说明。

我们将明确表明,这些关联是否映射向连接表,以及是否多的关系。

在传统的数据建模中,允许空值的外键不被认为是好的实践。因此我们所有的例子都使用非空的外键。这并非Hiberante的要求,并且如果你删除空值约束,映射依旧可以工作。

3.2 单向关联

3.2.1 多对一关联

单向的多对一关联是最普遍的单向关联。

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <many-to-one name="address"
        column="addressId"
        not-null="true"/>
</class>

<class name="Address">
    <id name="id" column="addressId">
        <generator class="native"/>
    </id>
</class>

create table Person ( personId bigint not null primary key, addressId bigint not null )
create table Address ( addressId bigint not null primary key )


注解3.2.1:单向的多对一关联,“一”方可以不用写“one-to-many”,只需要在“多”方写“many-to-one”。从数据库的角度,“一”方没有外键,而“多”方却有一外键与“一”方相联。因此,有外键的表需要写“many-to-one”,并且通过在<many-to-one>中的column属性指向主表的主键,column的属性值可以与主表的主键名不一样。另外,由于“一”方没有外键,因此它对外界一无所知,从所映射的Java代码中不能检索到其他表的信息。但我们可以通过join的方式来与其他表连接。


3.2.2 一对一关联
基于外建上的单向一对一关联(与上面所述的多对一关联)几乎是一样的。唯一的区别是字段上多了一个unique约束。

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <many-to-one name="address"
        column="addressId"
        unique="true"
        not-null="true"/>
</class>

<class name="Address">
    <id name="id" column="addressId">
        <generator class="native"/>
    </id>
</class>

create table Person ( personId bigint not null primary key, addressId bigint not null unique )
create table Address ( addressId bigint not null primary key )

基于主键上的单向的一对一关联通常使用一个特殊的id生成器。(请注意在此例中我们已经反转了关联的方向。)

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
</class>

<class name="Address">
    <id name="id" column="addressId">
        <generator class="foreign">
            <param name="property">person</param>
        </generator>
    </id>
    <one-to-one name="person" constrained="true"/>
</class>

(译注:原文中Address的id的column=”personId”,应为”adderssId”)

create table Person ( personId bigint not null primary key )
create table Address ( personId bigint not null primary key )

注解3.2.2:意味着在Address的主键addressId上有一指向person表的约束,这个约束通过名为person的属性进行关联。


3.2.3 一对多关联
在外键上的单向一对多关系很不常见,因此确实不推荐使用。

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <set name="addresses">
        <key column="personId"
            not-null="true"/>
        <one-to-many class="Address"/>
    </set>
</class>

<class name="Address">
    <id name="id" column="addressId">
        <generator class="native"/>
    </id>
</class>

create table Person ( personId bigint not null primary key )
create table Address ( addressId bigint not null primary key, personId bigint not null )

我们认为最好使用一个连接表来应付这种情况。

注解3.2.3:注意此例与前面的几个例子不同。

前面几个例子都是Person作为多方,Address作为一方,是多对一的关系,含义为多个人拥有同一个地址,如一家人住在一起。每个Person只有一个地址,但Address则拥有多个Person。在Address的Java代码中,可能有Set<Person> Person = new HashSet<Person>()的定义。但由于此节只讨论单向的关联,即从Person到Address的关联,不需要从Address中访问Person,因此,Address的代码可以不含有Set<Person> Person = new HashSet<Person>()的代码,在Hibernate映射文件中也不需要有<set>的定义。

而最后的一个例子则为Person作为一方,Address作为多方,是一对多的关系,含义为一个人有多个地址,如同时拥有别墅和宿舍。作为单向关联,我们需要从Person访问Address,因此,必须有<set>的定义。从含义来讲,这种情况是常见的。因此,在单向关联中,如果存在多对一的关联,而我们又需要从多方关联到一方,如本例,完全可以这样定义。

Hibernate Team不推荐此用法,只在于推荐将此语法转化为下面3.3.1节中谈到的语法,而不在于否定单向关联中从多方到一方的使用。

3.3 与连接表的单向关联

3.3.1 一对多关联

在连接表上的一对多关联更值得推荐。请注意,通过设定unique=”true”,我们将多对多关联变为一对多关联。

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <set name="addresses" table="PersonAddress">
        <key column="personId"/>
        <many-to-many column="addressId"
            unique="true"
            class="Address"/>
    </set>
</class>

<class name="Address">
    <id name="id" column="addressId">
        <generator class="native"/>
    </id>
</class>

create table Person ( personId bigint not null primary key )
create table PersonAddress ( personId not null, addressId bigint not null primary key )
create table Address ( addressId bigint not null primary key )

注解3.3.1: 从表面上看,这种形式是典型的多对多关联,即多对多关系通过一个中间表转换为各自对中间表的一对多关系,即一个人可以同时有多个地址,而一个地址又同时有多个人居住。但通过设定unique=”true”后,确实转化为一对多的关联。此节创建了三个表,设定了unique=”true”之后,PersonAddress表中addressId成为唯一值的主键,即每个地址只对应于一个人,不允许出现两个人同时拥有同一个地址的情况,但一个人可以拥有两个以上的地址。这种关联,是从Person到Address的一对多的关联。因此,与3.2.3的环境相同。

这种方式使得中间表具有仅由一个单一字段构成的主键,与传统多对多关联中的中间表一般由两个以上的字段组成复合主键不同。如果我们有这种情况出现,可以好好地利用这种方式。但应注意,此种方法已经背离了通过多表来表现多对多的关系的初衷,而是一种实实在在的一对多关联。

另外一个值得注意的地方,是此种方法未使用一个代理键作为主键,而是使用了一个在中间表中有实际意义的外键作为主键,尽管此键在主表中只不过是一个代理键。尽管前面的例子中,都出现是外键关联主表主键的情况,但外键均不是主键。而在此例中,外键成为中间表的主键了。

3.3.2 多对一关联

当关联为可选时,在连接表上的单向的多对一关联非常普遍。

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <join table="PersonAddress"
        optional="true">
        <key column="personId" unique="true"/>
        <many-to-one name="address"
            column="addressId"
            not-null="true"/>
    </join>
</class>

<class name="Address">
    <id name="id" column="addressId">
        <generator class="native"/>
    </id>
</class>

create table Person ( personId bigint not null primary key )
create table PersonAddress ( personId bigint not null primary key, addressId bigint not null )
create table Address ( addressId bigint not null primary key )

注解3.3.2: 这种方式更接近于数据库而不是面向对象的视角,是从中间表的角度来定义的,而且中间表PersonAddress的主键已经转移到personId上了。

3.3.3 一对一关联

在连接表上的单向的一对一关联非常少见,但也是可以的。

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <join table="PersonAddress"
        optional="true">
        <key column="personId"
            unique="true"/>
        <many-to-one name="address"
            column="addressId"
            not-null="true"
            unique="true"/>
    </join>
</class>

<class name="Address">
    <id name="id" column="addressId">
        <generator class="native"/>
    </id>
</class>

create table Person ( personId bigint not null primary key )
create table PersonAddress ( personId bigint not null primary key, addressId bigint not null unique )
create table Address ( addressId bigint not null primary key )


3.3.4 多对多关联

最后,是单向的多对多关联。

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <set name="addresses" table="PersonAddress">
        <key column="personId"/>
        <many-to-many column="addressId"
            class="Address"/>
    </set>
</class>

<class name="Address">
    <id name="id" column="addressId">
        <generator class="native"/>
    </id>
</class>

create table Person ( personId bigint not null primary key )
create table PersonAddress ( personId bigint not null, addressId bigint not null, primary key (personId, addressId) )
create table Address ( addressId bigint not null primary key )

注解3.3.4: 这才是真正意义上的多对多关联。


3.4 双向关联

3.4.1 一对多 / 多对一

双向的多对一关联是关联中最常见的。(这是标准的父/子关系。)

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <many-to-one name="address"
        column="addressId"
        not-null="true"/>
</class>

<class name="Address">
    <id name="id" column="addressId">
        <generator class="native"/>
    </id>
    <set name="people" inverse="true">
        <key column="addressId"/>
        <one-to-many class="Person"/>
    </set>
</class>

create table Person ( personId bigint not null primary key, addressId bigint not null )
create table Address ( addressId bigint not null primary key )

如果你使用List(或者其它索引集合),你需要将外键所在的关键字段设为not null,并且让Hibernate从集合一端管理关联,以维护每位元素的索引(通过设定update=”false”及insert=”false”来使另一方反转):

<class name="Person">
   <id name="id"/>
   ...
   <many-to-one name="address"
      column="addressId"
      not-null="true"
      insert="false"
      update="false"/>
</class>

<class name="Address">
   <id name="id"/>
   ...
   <list name="people">
      <key column="addressId" not-null="true"/>
      <list-index column="peopleIdx"/>
      <one-to-many class="Person"/>
   </list>
</class>

如果外键字段是NOT NULL,将位于集合映射中的<key>元素设定为not-null=”true”很重要。不要只在一个可能嵌套的<column>元素上定义not-null=”true”,在<key>元素上也要这样做。


3.4.2 一对一

基于外键的双向的一对一关联很常见。

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <many-to-one name="address"
        column="addressId"
        unique="true"
        not-null="true"/>
</class>

<class name="Address">
    <id name="id" column="addressId">
        <generator class="native"/>
    </id>
   <one-to-one name="person"
        property-ref="address"/>
</class>

create table Person ( personId bigint not null primary key, addressId bigint not null unique )
create table Address ( addressId bigint not null primary key )

注解3.4.2: 注意两表中的“address”,此标识可任意取,作用为将两表通过此相同的标识符联接起来。这样,Address知道是person表与其联接。

基于主键的双向的一对一关联使用特殊的id生成器。

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <one-to-one name="address"/>
</class>

<class name="Address">
    <id name="id" column="personId">
        <generator class="foreign">
            <param name="property">person</param>
        </generator>
    </id>
    <one-to-one name="person"
        constrained="true"/>
</class>

create table Person ( personId bigint not null primary key )
create table Address ( personId bigint not null primary key )


3.5 与连接表的双向关联

3.5.1 一对多 / 多对一

基于连接表的一对多关联。注意,inverse=”true”既可放在集合的一端,也可放在连接表的一端。

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <set name="addresses"
        table="PersonAddress">
        <key column="personId"/>
        <many-to-many column="addressId"
            unique="true"
            class="Address"/>
    </set>
</class>

<class name="Address">
    <id name="id" column="addressId">
        <generator class="native"/>
    </id>
    <join table="PersonAddress"
        inverse="true"
        optional="true">
        <key column="addressId"/>
        <many-to-one name="person"
            column="personId"
            not-null="true"/>
    </join>
</class>

create table Person ( personId bigint not null primary key )
create table PersonAddress ( personId bigint not null, addressId bigint not null primary key )
create table Address ( addressId bigint not null primary key )       


3.5.2 一对一

基于连接表的双向的一对一关联极端少见,但也可以实现。

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <join table="PersonAddress"
        optional="true">
        <key column="personId"
            unique="true"/>
        <many-to-one name="address"
            column="addressId"
            not-null="true"
            unique="true"/>
    </join>
</class>

<class name="Address">
    <id name="id" column="addressId">
        <generator class="native"/>
    </id>
    <join table="PersonAddress"
        optional="true"
        inverse="true">
        <key column="addressId"
            unique="true"/>
        <many-to-one name="person"
            column="personId"
            not-null="true"
            unique="true"/>
    </join>
</class>

create table Person ( personId bigint not null primary key )
create table PersonAddress ( personId bigint not null primary key, addressId bigint not null unique )
create table Address ( addressId bigint not null primary key )

 

3.5.3 多对多

最后,是双向的多对多关联。

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <set name="addresses" table="PersonAddress">
        <key column="personId"/>
        <many-to-many column="addressId"
            class="Address"/>
    </set>
</class>

<class name="Address">
    <id name="id" column="addressId">
        <generator class="native"/>
    </id>
    <set name="people" inverse="true" table="PersonAddress">
        <key column="addressId"/>
        <many-to-many column="personId"
            class="Person"/>
    </set>
</class>

create table Person ( personId bigint not null primary key )
create table PersonAddress ( personId bigint not null, addressId bigint not null, primary key (personId, addressId) )
create table Address ( addressId bigint not null primary key )


3.6 更复杂的关联映射

更复杂的关联极端稀有。Hibernate可以通过在内嵌于映射文件中的SQL片断来处理这种情况。例如,如果一个有历史帐户信息数据的表定义了accountNumber, effectiveEndDate及effectiveStartDate字段,如下:

<properties name="currentAccountKey">
    <property name="accountNumber" type="string" not-null="true"/>
    <property name="currentAccount" type="boolean">
        <formula>case when effectiveEndDate is null then 1 else 0 end</formula>
    </property>
</properties>
<property name="effectiveEndDate" type="date"/>
<property name="effectiveStateDate" type="date" not-null="true"/>

这样我们可以通过下面来将一个关联映射到当前实例(即有null effectiveEndDate的):

<many-to-one name="currentAccountInfo"
        property-ref="currentAccountKey"
        class="AccountInfo">
    <column name="accountNumber"/>
    <formula>'1'</formula>
</many-to-one>

在一个更复杂的例子中,假如Employee与Organization之间的关联由充斥着历史雇用数据的Employment表来维护。此时,对employee最新的employer的关联(即有最近的startDate者)可以这样映射:

<join>
    <key column="employeeId"/>
    <subselect>
        select employeeId, orgId
        from Employments
        group by orgId
        having startDate = max(startDate)
    </subselect>
    <many-to-one name="mostRecentEmployer"
            class="Organization"
            column="orgId"/>
</join>

通过这种方式,你可以创造性对付关联,但更为常见的是使用HQL或criteria query来处理这种情况。


(以上译自Hibernate 3.1 Reference第7章)

http://blog.matrix.org.cn/Sarkuya/date/20060523 星期二 2006年05月23日

活用SiteMesh,一个装饰器就可支撑整个网站结构

在寻求网站结构的高效统一上,SiteMesh通过Decorator的设计模式,十分利索地达到了目的。其设计思想是,用户发送request至服务器,服务器根据此request生成动态数据,生成网页,准备返回给客户端。就在返回前,SiteMesh进行拦截,对此网页进行解析,将title、body等部分拆解出来,套上模板后,再返回给客户端。由于SiteMesh在返回客户端的最后一步工作,此时的网页已经具备了标准的html网页格式,因此SiteMesh只需解析标准的html网页,无需考虑各个Web应用是应用了JSP、ASP,还是Velocity技术,相当灵活。

一般情况下,我们在decorators.xml文件中定义一个模板main.jsp,来自动套用未加装饰的网页:

<decorators defaultdir="/decorators">
    <decorator name="main" page="main_decorator.jsp">
          <pattern>/*</pattern>
    </decorator>
</decorators>


main_decorator.jsp是默认的装饰器,其pattern应用于所有的网页。其典型的代码如下:


1:  <%@page contentType="text/html"%>
2:  <%@page pageEncoding="gb2312"%>
3:  <%@taglib uri="http://www.opensymphony.com/sitemesh/decorator" prefix="decorator"%>
4:  <%@taglib uri="http://www.opensymphony.com/sitemesh/page" prefix="page"%>
5:  <%@taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
6: 
7:  <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
8:   "http://www.w3.org/TR/html4/loose.dtd">
9:    
10:  <fmt:setBundle basename="AppResource" />
11: 
12:  <html>
13:    <head>
14:      <meta http-equiv="Content-Type" content="text/html; charset=gb2312">
15:      <title><fmt:message key="webapp.name" /> - <decorator:title default="Welcome!" /></title>
16:      <link rel="stylesheet" href="<%=request.getContextPath()%>/css/w3c.css" type="text/css" />
17:      <decorator:head />
18:    </head>
19:    <body id="matrix">
20:      <script type="text/javascript">window.status = "正在加载: <decorator:title default="INTRANET" />...";</script>
21:      <div id="container">
22:        <script type="text/javascript">window.status = "正在加载: 页眉";</script>
23:        <div id='pageHeader'>
24:          <%@include file="/includes/header.jsp"%>
25:        </div>
26:        <div id="centralBody">
27:          <div id="mainbg">
28:            <script type="text/javascript">window.status = "正在加载: 页面内容";</script>
29:            <div id="mainContent">
30:              <decorator:body />
31:            </div>
32:            <script type="text/javascript">window.status = "正在加载: 右边导航栏";</script>
33:            <div id="sideBar">
34:              <page:applyDecorator page="/rootsidebar.jsp" name="sidebar" />
35:            </div>
36:          </div>
37:        </div>
38:        <script type="text/javascript">window.status = "正在加载: 页脚";</script>
39:        <div id="footer">
40:          <%@include file="/includes/footer.jsp"%>
41:        </div>
42:      </div>
43:      <script type="text/javascript">window.status = "完毕";</script>
44:    </body>
45:  </html>

假设有一个未加装饰的网页index.jsp如下:

<%@page contentType="text/html; charset=gb2312"%>
<html>
    <head>
        <title>首页</title>
    </head>
    <body>
        <h2>Hello, World!</h2>
    </body>
</html>

当SiteMesh工作时,它会分解出title及body的内容,并将其分别通过<decorator:title>及<decorator:body>套用到默认的main.jsp上。

好,首页工作得很好,我们准备装饰第二个网页downlaod.jsp。此网页的内容如下:

<%@page contentType="text/html; charset=gb2312"%>
<html>
    <head>
        <title>下载页面</title>
    </head>
    <body>
        <h2>欢迎下载!</h2>
    </body>
</html>

我们发现,当main_decorator.jsp这个装饰器装饰此网页上时,title及body均没问题。但在第34行,作为下载页面的sidebar不应该是rootsidebar,而应是downloadsidebar,即第34行的代码应为:

34:  <page:applyDecorator page="/downloadsidebar.jsp" name="sidebar" />

很显然,我们需要另外一个装饰器,于是我们将第34行改后另存为一个名为download_decorator.jsp的装饰器,还是存于/decorators下面。


相应地,在download.jsp的<title>此行上加上:

  <meta name="decorator" content="download_decorator" />

SiteMesh将根据此元数据的指示,自动调用download_decorator.jsp的装饰器。

但观察两个装饰器,只有第34行不同。只因为一行不同而需要创建另一个装饰器,代价较高。而往下,每一个需要不同的sidebar的页面都必须另行创建一个新装饰器。看来这种方式令人无法忍受。我们注意到,title、body与sidebar都是被装饰的部分,但title及body却可以不用修改装饰器实现共享,而sidebar的装饰部分却不行。title及body均是由<decorator>标签来实现,而sidebar由<page:applyDecorator>来实现。看来不同的结果就源自于这两个标签的不同之上。

我们来看SiteMesh后台的实现原理。SiteMesh在装饰网页时,先分解出index.jsp及download.jsp的title及body,然后读取装饰器,遇到<decorator>标签时,换入title及body的内容。遇到<page:applyDecorator>时,读取其page属性所指定的网页,然后应用名为sidebar的另一个装饰器来装饰sidebar,装饰完后再将结果换进main_decorator。看来,两者的区别在于<decorator>所加工的素材是未装饰的网页已经主动提供的,而因为我们不能主动提供素材给<page:applyDecorator>,因此main_decorator必须自己去找。这就是它们的本质区别。

这使我们想起设计思想中的依赖注入模式。<decorator>就是一个setter,而<page:applyDecorator>则是一个getter,因此它必须自己去寻找所需的素材。如果我们能根据依赖注入模式,为其注入所需素材,那么,就像<decorator>一样,它也不需要再变了。

实现的思路是,download.jsp负责传递一个含有具体page文件名的参数给<page:applyDecorator>,然后,<page:applyDecorator>将其值赋于page属性。对于download.jsp来讲,存放于参数的好地方是网页中的<meta>部分。因此,在其<title>之上加上这一行:

  <meta name="sidebar" content="/downloadsidebar.jsp" />

这样,<page:applyDecorator>就有机会读取此参数了。配合使用SiteMesh的另一个标签<decorator:usePage>,我们可以顺利地为page属性设定值。

  <decorator:usePage id="myPage" />
  <page:applyDecorator page='<%= myPage.getProperty("meta.sidebar") %>' name="sidebar" />

通过这种方法,将原来的由main_decorator主动索取sidebar文件名的方式,改为其被动地接收参数,然后由在各个页面中通过设定meta name="sidebar"的方