乐者为王

Do one thing, and do it well.

猎狗与猎人的故事

目标

一条猎狗将兔子赶出了窝,一直追赶他,追了很久仍没有捉到。牧羊看到此种情景,讥笑猎狗说,“你们两个当中小的反而跑得快得多。”猎狗回答说:“你不知道我们两个的跑是完全不同的!我仅仅为了一顿饭而跑,他却是为了性命而跑呀!”

动力

这话被猎人听到了,猎人想:猎狗说的对啊,那我要想得到更多的猎物,得想个好法子。于是,猎人又买来几条猎狗,凡是能够在打猎中捉到兔子的,就可以得到几根骨头,捉不到的就没有饭吃。这一招果然有用,猎狗们纷纷去努力追兔子,因为谁都不愿意看着别人有骨头吃,自己没吃的。就这样过了一段时间,问题又出现了。大兔子非常难捉到,小兔子好捉。但捉到大兔子得到的奖赏和捉到小兔子得到的骨头差不多,猎狗们善于观察,发现了这个窍门,专门去捉小兔子。慢慢地,大家都发现了这个窍门。猎人对猎狗说:最近你们捉的兔子越来越小了,为什么?猎狗们说:反正结果没有什么大的区别,为什么费那么大的劲去捉那些大的呢?

保障

猎人经过思考后,决定不将分得骨头的数量与是否捉到兔子挂钩,而是每过一段时间就统计一次猎狗捉到兔子的总重量,按照重量来评价猎狗,从而决定它们一段时间内的待遇。于是,猎狗们捉到兔子的数量和重量都增加了。猎人很开心。但是,过了一段时间,猎人发现猎狗们捉兔子的数量又少了,而且越有经验的猎狗捉兔子的数量下降得越利害。于是,猎人又去问猎狗是怎么回事。猎狗说,“我们把最好的时间都奉献给了您,主人,随着时间的推移我们会慢慢变老,当我们捉不到兔子的时候,您还会给我们骨头吃吗?”

回报

猎人做出了论功行赏的决定,分析与汇总了所有猎狗捉到兔子的数量与重量,规定如果捉到的兔子超过一定的数量后,即使捉不到兔子,每顿饭也可以得到一定数量的骨头。猎狗们都很高兴,大家都努力去争取达到猎人规定的数量。一段时间过后,终于有一些猎狗达到了猎人规定的数量。这时,其中一只猎狗说:“我们这么努力,只得到几根骨头,而我们捉的猎物远远超过了这几根骨头。我们为什么不能给自己捉兔子呢?”于是,有些猎狗离开了猎人,自己捉兔子去了。

归宿

猎人意识到猎狗正在流失,并且那些流失的猎狗像野狗一般与自己的猎狗抢兔子。情况变得越来越糟,猎人不得已引诱了一条野狗,问他到底野狗比猎狗强在那里。野狗说:“猎狗吃的是骨头,吐出来的是肉啊!”接着又道:“也不是所有的野狗都顿顿有肉吃,大部分最后骨头都没的舔!不然也不至于被你诱惑。”于是,猎人又进行了改革,使得每条猎狗除基本骨头外,可获得其所猎兔肉总量的n%,而且随着服务时间加长,贡献加大,该比例还可递增,并有权分享猎人所拥有兔肉总量的m%。就这样,猎狗们与猎人一起努力,将野狗们逼得叫苦连天,纷纷要求重归猎狗队伍。

故事还在继续……

只有永远的利益,没有永远的朋友

日子一天一天地过去,冬天到了,兔子越来越少,猎人们的收成也一天不如一天。那些服务时间长的老猎狗们已老得捉不到兔子,但却仍然无忧无虑地享受着他们自以为应得的大份食物。终于有一天,猎人再也不能忍受,把他们扫地出门了,因为猎人更需要身强力壮的猎狗……

Birth of MicroBone Co.

被扫地出门的老猎狗们得到一笔不菲的赔偿金,于是,他们成立了MicroBone公司。他们采用连锁加盟的方式招募野狗,向野狗们传授猎兔的技巧,他们从猎得的兔子中抽取一部分作为管理费。当赔偿金几乎全部用于广告后,他们终于有了足够多的野狗加盟,公司开始盈利了。一年后,他们收购了猎人的家当……

Development of MicroBone Co.

MicroBone公司许诺,加盟的野狗可以得到公司n%的股份。这实在是太有诱惑力了。那些自认为怀才不遇的野狗们都以为找到了知音:终于可以做公司的主人了,不用再忍受猎人呼来唤去的不快,不用再为捉到足够多的兔子而累死累活,也不用眼巴巴地乞求猎人多给两根骨头。这一切对野狗们来说,比多得到两根骨头更加受用。于是,野狗们拖家带口地加入了MicroBone。一些在猎人门下服务的年轻猎狗也蠢蠢欲动,甚至很多自以为聪明、实际上愚蠢的猎人也想加入。好多同类型的公司雨后春笋般地成立了,BoneEase、Bone.com、ChinaBone……一时间,森林里热闹非凡。

干活的总是拿得少的,拿得多的都是不干活的

猎人凭借出售公司的钱走上了老猎狗走过的路,历经千辛万苦重新发达起来,最后又来与MicroBone公司谈判收购事宜,老猎狗出人意料地把MicroBone公司卖给了猎人。老猎狗们从此不再经营公司,转而开始靠写作赚钱,写完自传《老猎狗的一生》,又写了《如何成为出色的猎狗》、《成功猎狗500条法则》、《猎狗成功秘诀》、《如何成为一只进入管理层的猎狗》、《穷猎狗,富猎狗》等一系列成功自助读物,并且将老猎狗的故事搬上了屏幕,取名《猎狗花园》——从此,老猎狗成为家喻户晓的名人,坐收版权费,既没有风险,利润又可观,轻轻松松地过上了富人闲人的日子。

无我编程的十诫

英文原文:https://blog.codinghorror.com/the-ten-commandments-of-egoless-programming/

无我编程的十诫最早出现在杰拉尔德·温伯格(Gerald M. Weinberg)的《计算机编程心理学》这本书中:

  • 理解并接受自己会犯错误。关键是在错误进入到生产环境之前尽早地找到它们。幸运的是,除了我们几个在JPL开发的火箭制导软件外,我们行业中的错误很少是灾难性的,所以我们可以而且应该,学习、大笑、然后继续前行。
  • 你不是你的代码。记住,代码审查的整个出发点是找到问题,而且终归会找到问题。所以当问题被发现时不要太在意。
  • 不管你知道多少“karate”,总有人比你知道得更多。如果你问的话,这样的人可以教你一些新的动作。寻求并接受他人的意见,尤其是当你认为不需要的时候。
  • 不要没有商讨就重写代码。 “修复代码”和“重写代码”之间有着明显的区别。弄清两者的区别,并在代码审查的框架内追求风格变化,而不是孤独的执行者。
  • 尊重比你懂得少的人,并抱以耐心。与开发者交涉的非技术人员几乎普遍认为我们是妄自尊大的讨厌鬼。不要用愤怒和不耐烦来加深这种刻板印象。
  • 世界上唯一不变的就是变化。敞开胸怀,面带微笑地去接受变化。将需求、平台或工具的每个变化视为新的挑战,而不是一些严重地需要抗争的不便。
  • 真正的权威源自知识,而不是职位。知识产生权威,权威就会产生尊重——所以如果你想在无我的环境中得到尊重,请充实知识。
  • 为信念而战,但优雅地接受失败。明白有时你的想法将被推翻。即使你的确是正确的,也不要报复或者嚷嚷说“我早就说过”,不要把被否定的想法当作是牺牲品或者口号。
  • 不要成为“宅男”。不要成为在黑暗的办公室里编码的家伙,就算偶尔露个面,也只是为了买杯可乐。“宅男”会与其他人失去联系,淡出他们的视野,失去控制,在开放协作的环境中将没有任何位置。
  • 批评代码而不是人——对程序员好点,而不是代码。尽可能地使你的所有评论都是积极的,并且旨在改进代码。将评论与局部标准、程式规格、性能提升等相关联。

软件的人性化原则是永恒的。此书早在1971年我1岁时就写成。虽然已经过去几十年,但这些原则并没有过时,仍然值得所有程序员拜读。

Ajax:Web应用的一种新方法

英文原文:http://adaptivepath.org/ideas/ajax-new-approach-web-applications/

如果有任何关于当前的交互设计可以被称为“迷人”的话,那就是创建Web应用。毕竟,你最后一次听到有人吹嘘不在Web上的产品的交互设计是什么时候?(好吧,除了iPod。)一切都很酷,富有创意的新设计都是在线的。

尽管如此,Web交互设计师仍不能不对创建桌面软件的同事们感到一丝羡慕。桌面应用有着在Web上似乎遥不可及的丰富和响应能力。让Web迅速扩散的同样简单也造成了在我们可以提供的体验和用户能够从桌面应用得到的体验之间差距。

这一差距正在缩小。看一看Google Suggest。观察随着你输入建议项更新的方式,几乎立即。现在看看Google Maps。放大。使用鼠标抓取地图并稍稍滚动。再次滚动,一切几乎立即发生,无需等待页面重新加载。

Google Suggest和Google Maps是Web应用的一种新方法的两个例子,该方法在Adaptive Path被我们称为Ajax。这个名字是异步JavaScript + XML的简写,它代表了Web上可能的根本性转变。

定义Ajax

Ajax不是一种技术。它事实上是几种技术,每种技术都在它自身正确的地方蓬勃发展,以强大的新方式聚合在一起。Ajax包括:

经典的Web应用模型是这么工作的:在界面上的大多数用户操作触发一个HTTP请求回Web服务器。服务器做一些处理——检索数据,精密计算,与各种遗留系统对话——然后返回一个HTML页面到客户端。它是改编自Web作为超文本媒介的原定用途的一个模型,但是如同用户体验的要素的爱好者所知,该模型让Web很好地适合超文本不一定能让它很好地适合软件应用。

ajax-fig1

图1:Web应用的传统模型(左)与Ajax模型(右)的比较。

这种方法很有技术上的感觉,但它无助于很好的用户体验。当服务器在做它的事情的时候,用户在做什么?没错,等待。在任务中的每一步,用户都在等待。

显然,如果我们从头开始为应用设计Web,我们不会让用户呆呆地等待。一旦界面被加载后,为什么每次应用需要从服务器获得什么时用户交互就将陷入停顿?事实上,为什么用户应该看到所有进入服务器的应用?

Ajax有何不同

Ajax应用消除了在Web上交互的开始-停止-开始-停止的本质,通过引入中介——Ajax引擎——在用户和服务器之间。它看起来像是给应用添加一个层而让应用更少地作出响应,但事实恰好相反。

在会话开始而不是加载页面的时候,浏览器加载Ajax引擎——用JavaScript编写并且通常藏在一个隐藏的Frame中。在用户方面引擎同时负责渲染用户看到的界面和与服务器通讯。Ajax引擎允许用户和应用的交互异步发生——独立于与服务器的通讯。因此用户永远不会开始于一个空白浏览器窗口和一个沙漏图标,呆呆地等待服务器做某些事情。

ajax-fig2

图2:传统Web应用的同步交互模式(上)与Ajax应用的异步模式(下)的比较

通常会生成一个HTTP请求的每个用户操作采取JavaScript调用Ajax引擎的形式替代。任何对用户操作的响应不需要回到服务器——例如简单的数据验证,编辑内存中的数据,甚至一些导航——引擎自己处理。如果为了响应引擎需要从服务器获得什么——如果这是提交数据以进行处理,加载额外的界面代码,或者检索新的数据——引擎异步发出这些请求,通常使用XML,没有拖延用户和应用的交互。

谁在使用Ajax

Google在开发Ajax方法上做了巨大的投资。在过去一年里Google推出的所有主要产品——OrkutGmailGoogle Groups的最新beta版本、Google Suggest,以及Google Maps——都是Ajax应用。(想了解更多这些Ajax实现的技术细节,可以查看GmailGoogle Suggest,和Google Maps的这些优秀分析)别人也在模仿:Flickr中人们喜欢的许多特性依靠Ajax,而Amazon的A9.com搜索引擎已经运用了类似的技术。

这些项目表明Ajax并不只是技术层面的论调,而且对实际应用也是实用的。这不是另一种只在实验室工作的技术。Ajax应用可以是任意大小,从非常简单、功能单一的Google Suggest到非常复杂和精密的Google Maps。

在Adaptive Path,在过去的几个月中我们已经使用Ajax完成了我们自己的工作,并且我们认识到我们仅仅触及到Ajax应用可以提供的丰富交互和响应能力的一点皮毛。Ajax是Web应用的一个重要的发展,并且它的重要性只会继续增长。因为外面有那么多的开发者已经知道如何使用这些技术,我们期望看到更多的组织继Google之后率先获得Ajax提供的竞争优势。

继续前行

创建Ajax应用最大的挑战不是技术。核心Ajax技术是成熟的、稳定的、并且容易理解的。相反,挑战来自于这些应用的设计者:忘记我们所认为我们知道的关于Web的限制,并且开始想象一个更广泛、更丰富的可能性的范围。

那一定会很有趣。

Ajax Q&A

2005年3月13日: 自我们第一次发布Jesse的论文以来,我们收到了大量读者关于Ajax问题的信件。在这个Q&A中,Jesse回答了一些最常见的问题。

Q. 是Adaptive Path发明的Ajax吗?还是Google?是不是Adaptive Path帮助构建了Google的Ajax应用?

A. 既不是Adaptive Path也不是Google发明的Ajax。Google近期的产品仅仅是级别最高的Ajax应用例子。Adaptive Path没有参与Google的Ajax应用的开发,但我们一直在为我们其它的一些客户做Ajax的工作。

Q. Adaptive Path正在出售Ajax组件或注册这个名字的商标吗?我从哪里可以下载它?

A. Ajax不是你能下载的东西。它是一种方法——思考使用某种技术的Web应用架构的一种方式。Ajax这个名字和方法均不是Adaptive Path专有的。

Q. Ajax仅仅是XMLHttpRequest的另一个名字吗?

A. 不是。XMLHttpRequest只是Ajax方程式的一部分。XMLHttpRequest是使异步服务器通讯成为可能的技术组件;Ajax是我们给文章中描述的整体方法的名字,它不仅依赖于XMLHttpRequest,还依赖于CSS、DOM和其它技术。

Q. 为什么你觉得有必要给出这个名字?

A. 当和客户讨论这个方法时,我需要使用比“异步JavaScript + CSS + DOM + XMLHttpRequest”更短的东西。

Q. 异步服务器通讯技术已经存在好多年了。是什么让Ajax成为一种“新”方法的?

A. 新情况是这些技术在实际应用中的突出使用改变了Web的基本交互模型。Ajax现在大行其道是因为这些技术和行业对如何更有效地部署它们的理解已经经过了时间的发展。

Q. Ajax是一个技术平台或一种架构风格吗?

A. 都是。Ajax是以某种特殊方式同时使用的一组技术。

Q. Ajax最适合什么类型的应用?

A. 目前我们还不知道。因为它是一种相对较新的方法,我们对Ajax最适用在哪些地方的理解仍处于初级阶段。有时候传统的Web应用模型是某个问题的最合适的解决方案。

Q. 这意味着Adaptive Path是反Flash的吗?

A. 一点也不。Macromedia是Adaptive Path的一个客户,而且我们是Flash技术长期以来的支持者。随着Ajax的成熟,我们希望有时候Ajax是特定问题的最好的解决方案,而有时候Flash 是最好的解决方案。我们也有兴趣探索技术可以混合的方式(正如Flickr的案例,它使用了两者)。

Q. Ajax有显著的可访问性或浏览器兼容性限制吗?Ajax应用会破坏后退按钮吗?Ajax兼容REST吗?Ajax开发时是否有安全考虑?Ajax应用可以为那些已经关闭JavaScript的用户工作吗?

A. 所有这些问题的答案是“可能”。许多开发者已经在致力于解决这些问题。要确定Ajax所有的局限性,我们认为有更多的工作要做,并且我们期望Ajax开发社区一起来发现更多这类问题。

Q. 一些你举出的Google例子根本没有使用XML。我必须在Ajax应用中使用XML和/或XSLT吗?

A. 不。XML是最全面发展的在Ajax客户端内外获取数据的手段,但没有理由使用JavaScript Object Notation之类的技术或任何类似的结构化数据的手段为交换不能达到同样的效果。

Q. Ajax应用比传统Web应用更易于开发吗?

A. 不一定。Ajax应用必然涉及到在客户端运行复杂的JavaScript代码。让复杂的代码高效并且无缺陷不是一个掉以轻心的任务,需要更好的开发工具和框架来帮助我们面对挑战。

Q. Ajax应用经常交付比传统Web应用更好的体验吗?

A. 不一定。Ajax给交互设计师更多的灵活性。然而,我们拥有的能力越大,在运用它的时候我们必须使用的更谨慎。我们必须小心地使用Ajax来增强我们应用的用户体验,而不是削弱它。

这篇文章被来自Webhostinggeeks.com的Jovana Milutinovich翻译成了Serbo-Croatian语言。

在iBatis中如何将JavaBean的boolean值映射成数据库的CHAR(Y或N)值

Spring 1.2.8 + iBatis 2.1.5

JavaBean的boolean值映射到数据库的默认值是0和1,不是非常直观,现在希望能将boolean值映射成为Y和N。iBatis 2.0.5以后的版本提供了TypeHandlerCallback,可以用来解决这个问题。

首先定义数据表:

1
2
3
4
5
6
CREATE TABLE users (
    username varchar(40),
    password varchar(255),
    enabled char(1),
    PRIMARY KEY (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf-8;

YesNoTypeHandlerCallback实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class YesNoTypeHandlerCallback implements TypeHandlerCallback {

    public void setParameter(ParameterSetter setter, Object parameter) throws SQLException {
        if (parameter == null) {
            setter.setString("");
        } else {
            Boolean b = (Boolean)parameter;
            if (b.booleanValue()) {
                setter.setString("Y");
            } else {
                setter.setString("N");
            }
        }
    }

    public Object getResult(ResultGetter getter) throws SQLException {
        String string = getter.getString();
        if (string == null) {
            return null;
        } else if ("Y".equalsIgnoreCase(string)) {
            return new Boolean(true);
        } else {
            return new Boolean(false);
        }
    }

    public Object valueOf(String s) {
        return s;
    }
}

配置文件sql-map-config.xml中的内容如下:

1
2
3
4
5
6
<sqlMapConfig>
    <typeHandler jdbcType="CHAR" javaType="boolean"
                 callback="com.codemany.netlink.dao.support.YesNoTypeHandlerCallback" />

    <sqlMap resource="com/codemany/netlink/dao/impl/User.xml" />
</sqlMapConfig>

jdbcType="CHAR" javaType="boolean"必须有,否则YesNoTypeHandlerCallback.setParameter()不会被调用。

SqlMap映射文件User.xml部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<typeAlias alias="user" type="com.codemany.netlink.model.User" />

<resultMap id="userResult" class="user">
    <result property="username" column="username" />
    <result property="password" column="password" />
    <result property="enabled" column="enabled" jdbcType="CHAR" javaType="boolean" />
</resultMap>

<select id="getUser" parameterClass="string" resultMap="userResult">
<![CDATA[
    SELECT username, password, enabled
    FROM user WHERE username = #username#
]]>
</select>

<insert id="addUser" parameterClass="user">
<![CDATA[
    INSERT INTO user (username, password, enabled)
    VALUES (#username#, #password#, #enabled,jdbcType=CHAR,javaType=boolean#)
]]>
</insert>

jdbcType="CHAR" javaType="boolean"必须有,否则YesNoTypeHandlerCallback.getResult()不会被调用;#enabled,jdbcType=CHAR,javaType=boolean#也必须这样写,否则YesNoTypeHandlerCallback.setParameter()也不会被调用,写成#enabled:CHAR#或#enabled#都不起作用。

DbUnit结合Spring进行SqlMap的单元测试

使用DbUnit,开发者可以控制测试数据库的状态。进行一个DAO单元测试之前,DbUnit为数据库准备好初始化数据;而在测试结束时,DbUnit会把数据库状态恢复到测试前的状态。下面的例子使用DbUnit为SqlMap编写单元测试。

SqlMap映射文件User.xml的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<select id="getUser" parameterClass="String" resultClass="User">
    SELECT username, password, first_name, last_name
    FROM user WHERE username = #username#
</select>

<delete id="removeUser" parameterClass="String">
    DELETE FROM user WHERE username = #username#
</delete>

<update id="updateUser" parameterClass="User">
    UPDATE user SET
        password = #password#,
        first_name = #firstName#,
        last_name = #lastName#
    WHERE username =#username#
</update>

<insert id="insertUser" parameterClass="User">
    INSERT INTO user (username, password, first_name, last_name)
    VALUES (#username#, #password#, #firstName#, #lastName#)
</insert>

配置文件sql-map-config.xml中的内容如下:

1
2
3
4
5
6
7
8
9
10
<sqlMapConfig>
    <settings lazyLoadingEnabled="true"
              cacheModelsEnabled="true"
              enhancementEnabled="true"
              maxSessions="64"
              maxTransactions="8"
              maxRequests="128" />

    <sqlMap resource="com/codemany/netlink/dao/impl/User.xml" />
</sqlMapConfig>

首先,要为单元测试准备测试数据。我们可以用DbUnit的Flat XML格式来准备测试数据集。下面的XML文件称为目标数据库的Seed File,它为测试准备了两条数据。其中元素名user对应数据库的表名,属性username,password,first_name和last_name是表user的列名,属性值就是记录值。

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<dataset>
    <user username="ford"
        password="ford"
        first_name="Henry"
        last_name="Ford" />

    <user username="twain"
        password="twain"
        first_name="Mark"
        last_name="twain" />
</dataset>

缺省情况下,DbUnit在单元测试开始之前执行CLEAN_INSERT操作,删除Seed File中所有表的数据,并导入Seed File的测试数据。我们可以通过覆盖getSetUpOperation()和getTearDownOperation()方法来控制单元测试前和测试后的数据库状态。一种高效的实施方案就是让getSetUpOperation()方法执行REFRESH操作,通过执行这个操作,我们可以用Seed File中的数据去更新目标数据库里的数据。接下来,就是getTearDownOperation(),让他执行一个NONE操作,也就是什么也不执行。

1
2
3
4
5
6
7
protected DatabaseOperation getSetUpOperation() throws Exception {
    return DatabaseOperation.REFRESH;
}

protected DatabaseOperation getTearDownOperation() throws Exception {
    return DatabaseOperation.NONE;
}

为了方便测试,我们为SqlMap的单元测试编写一个抽象的测试基类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public abstract class SqlMapTestCase extends DatabaseTestCase {
    protected SqlMapClient sqlMapClient = null;
    protected Properties props = new Properties();

    protected IDatabaseConnection getConnection() throws Exception {
        props.load(Resources.getResourceAsStream("properties/database.properties"));

        Class.forName(props.getProperty("driver"));
        Connection conn = DriverManager.getConnection(props.getProperty("url"),
                props.getProperty("username"), props.getProperty("password"));
        return new DatabaseConnection(conn);
    }

    protected IDataSet getDataSet() throws Exception {
        String resource = "com/codemany/netlink/dao/impl/dataset.xml";
        return new FlatXmlDataSet(Resources.getResourceAsStream(resource));
    }

    protected void setUp() throws Exception {
        super.setUp();

        // Build the SqlMapClient
        Reader reader = Resources.getResourceAsReader("sql-map-config.xml");
        sqlMapClient = SqlMapClientBuilder.buildSqlMapClient(reader);

        // Tell the SqlMapClient to use the given DataSource
        DataSource dataSource = getDataSource();
        TransactionConfig transactionConfig = getTransactionConfig(dataSource);
        // Apply the given TransactionConfig to the SqlMapClient
        applyTransactionConfig(sqlMapClient, transactionConfig);
    }

    protected void tearDown() throws Exception {
        super.tearDown();

        if (sqlMapClient != null) {
            DataSource ds = sqlMapClient.getDataSource();
            Connection conn = ds.getConnection();
            conn.close();
        }
    }

    private DataSource getDataSource() throws Exception {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(props.getProperty("driver"));
        dataSource.setUrl(props.getProperty("url"));
        dataSource.setUsername(props.getProperty("username"));
        dataSource.setPassword(props.getProperty("password"));
        return dataSource;
    }

    private TransactionConfig getTransactionConfig(DataSource dataSource) throws Exception {
        Properties transactionConfigProperties = new Properties();
        transactionConfigProperties.setProperty("SetAutoCommitAllowed", "false");

        TransactionConfig transactionConfig = (TransactionConfig)ExternalTransactionConfig.class.newInstance();
        transactionConfig.setDataSource(dataSource);
        transactionConfig.initialize(transactionConfigProperties);
        return transactionConfig;
    }

    private void applyTransactionConfig(SqlMapClient sqlMapClient, TransactionConfig transactionConfig) {
        if (!(sqlMapClient instanceof ExtendedSqlMapClient)) {
            throw new IllegalArgumentException("Cannot set TransactionConfig with DataSource"
                    + "for SqlMapClient if not of type ExtendedSqlMapClient: " + sqlMapClient);
        }
        ExtendedSqlMapClient extendedClient = (ExtendedSqlMapClient)sqlMapClient;
        transactionConfig.setMaximumConcurrentTransactions(extendedClient.getDelegate().getMaxTransactions());
        extendedClient.getDelegate().setTxManager(new TransactionManager(transactionConfig));
    }
}

然后为每个SqlMap映射文件编写一个测试类,继承上面的抽象类:

1
2
3
4
5
6
7
8
public class UserSqlMapTest extends SqlMapTestCase {

    public void testGetUser() throws Exception {
        User user = (User)sqlMapClient.queryForObject("getUser", "ford");
        assertNotNull(user);
        assertEquals("ford", user.getName());
    }
}

如此就可以进行单元测试了。

Ant系列之用EMMA测量测试覆盖率的问题

EMMA是一个coverage工具,有on-the-fly和offline两种使用方式,在Ant里通常采用offline方式。

到sourceforge下载了2.0版本的EMMA下来,由于将EMMA的生成文件做了重定向,因此遇到了些问题,比如出现以下错误:

1
nothing to do: no runtime coverage data found in any of the data files

查找EMMA的FAQ 3.7节,只是告诉你可能少了metadata和runtime coverate date(对Ant用户来说一般是少了runtime coveraget date,它的后缀名默认是ec)。

由FAQ 3.15节可以知道runtime coverage data文件默认是生成在user.dir目录下的(即build.xml中的basedir目录下),而由instr指令生成的metadata文件(后缀名默认为em)则已经被重定向到另外的目录(用来生成coverage report的地方),所以必须重新设置coverage.out.file的值,用来重定向生成的runtime coverage data。因为runtime coverage data是在junit任务执行时产生的,所以我们可以在junit任务中添加一行代码:

1
<sysproperty key="emma.coverage.out.file" value="${coverage.dir}/coverage.emma" />

注意:

  1. metadata和runtime文件的后缀名最好设置相同;
  2. 一定要将junit任务的fork属性设置为true,这是因为EMMA 2.0的runtime coverage data是在JVM退出后生成的;
  3. instrumented classes必须是第一个被JVM执行的,所以instrumented classes目录必须处在junit任务的classpath的第一行,具体原因不明,如果谁知道还请告知下。

其实EMMA附带的examples目录下的build-offline.xml里已经说明的很详细了,只不过下载后没有仔细看,才遇到了一些问题。下面是build.xml的部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<path id="classpath">
  <pathelement location="${instrumentedclasses}" />
  <pathelement location="${classes}" />
  <pathelement location="${testclasses}" />
  <fileset dir="${lib}" includes="**/*.jar" />
</path>

<target name="compile" depends="prepare">
  <javac srcdir="${src}" destdir="${classes}" debug="on">
    <classpath refid="classpath" />
  </javac>
  <javac srcdir="${test}" destdir="${testclasses}" debug="on">
    <classpath refid="classpath" />
  </javac>
</target>

<target name="coverage.instrument" depends="compile">
  <emma enabled="yes">
    <instr instrpath="${classes}" destdir="${instrumentedclasses}"
        metadatafile="{coveragereports}/metadata.emma" merge="true">
    </instr>
  </emma>
</target>

<target name="junit" depends="coverage.instrument">
  <junit printsummary="yes" haltonfailure="no" fork="true">
    <sysproperty key="emma.coverage.out.file" value="${coveragereports}/coverage.emma" />
    <classpath refid="classpath" />

    <formatter type="xml" />

    <batchtest fork="yes" todir="${junitreports}">
      <fileset dir="${test}">
        <include name="**/*Test*.java" />
      </fileset>
    </batchtest>
  </junit>
</target>

<target name="coverage.report" depends="test">
  <emma enabled="yes">
    <report sourcepath="${src}">
      <fileset dir="${coveragereports}">
        <include name="*.emma" />
      </fileset>

      <html outfile="${coveragereports}/coverage.html" depth="method" />
    </report>
  </emma>
</target>

修复CheckStyle报告中Summary部分文件数目错误问题

在使用CheckStyle时用了它提供的checkstyle-frames.xsl作为样式文件,不过checkstyle-frames.xsl生成报告时有些问题,主要是统计数据有点问题,Summary中报告的文件数目比真实的多,所以对xsl文件的summary模板作了小小的一些修改,解决了这个问题,且添加了一个统计有错误的文件个数的功能。

原来的xsl内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<xsl:template match="checkstyle" mode="summary">
    <h3>Summary</h3>
    <xsl:variable name="fileCount" select="count(file)"/>
    <xsl:variable name="errorCount" select="count(file/error)"/>
    <table class="log" border="0" cellpadding="5" cellspacing="2" width="100%">
        <tr>
            <th>Files</th>
            <th>Errors</th>
        </tr>
        <tr>
            <xsl:call-template name="alternated-row"/>
            <td><xsl:value-of select="$fileCount"/></td>
            <td><xsl:value-of select="$errorCount"/></td>
        </tr>
    </table>
</xsl:template>

修改后的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<xsl:template match="checkstyle" mode="summary">
    <h3>Summary</h3>
    <xsl:variable name="fileCount">
        <xsl:value-of select="count(file[not(./@name = preceding-sibling::file/@name)])" />
    </xsl:variable>
    <xsl:variable name="fileErrorCount" select="count(file[count(error) > 0])" />
    <xsl:variable name="errorCount" select="count(file/error)" />
    <table class="log" border="0" cellpadding="5" cellspacing="2" width="100%">
        <tr>
            <th>Total Files</th>
            <th>Files With Errors</th>
            <th>Errors</th>
        </tr>
        <tr>
            <xsl:call-template name="alternated-row" />
            <td><xsl:value-of select="$fileCount" /></td>
            <td><xsl:value-of select="$fileErrorCount" /></td>
            <td><xsl:value-of select="$errorCount" /></td>
        </tr>
    </table>
</xsl:template>
  • following-sibling 按文档顺序选择文档中此后出现的当前节点的所有兄弟节点。
  • preceding-silbling 按与文档顺序相反的方向选择文档中此前出现的当前节点的所有兄弟节点。
  • following 除当前节点的所有后代节点外,按顺序选择文档中当前节点之后出现的所有节点,不包括属性节点或名称空间节点。
  • preceding 按与文档方向顺序相反的方向选择文档中在当前节点之前出现的所有节点。

Ant系列之用JDepend生成包依赖性度量的问题

首先获取JDepend的最新jar文件jdepend-2.9.jar,把它添加到ANT_HOME/lib目录里。

在build.xml中添加:

1
2
3
4
5
6
7
<target name="jdepend" description="Generate dependency metrics for each package">
    <jdepend format="xml" outputfile="${jdepend.dir}/jdepend-report.xml">
        <classpath>
            <pathelement path="classes" />
        </classpath>
    </jdepend>
</target>

运行任务后出现如下的错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BUILD FAILED
build.xml:59: Missing classespath required argument
    at org.apache.tools.ant.taskdefs.optional.jdepend.JDependTask.execute(JDependTask.java:397)
    at org.apache.tools.ant.UnknownElement.execute(UnknownElement.java:275)
    at org.apache.tools.ant.Task.perform(Task.java:364)
    at org.apache.tools.ant.Target.execute(Target.java:341)
    at org.apache.tools.ant.Target.performTasks(Target.java:369)
    at org.apache.tools.ant.Project.executeSortedTargets(Project.java:1216)
    at org.apache.tools.ant.Project.executeTarget(Project.java:1185)
    at org.apache.tools.ant.helper.DefaultExecutor.executeTargets(DefaultExecutor.java:40)
    at org.eclipse.ant.internal.ui.antsupport.EclipseDefaultExecutor.executeTargets(EclipseDefaultExecutor.java:32)
    at org.apache.tools.ant.Project.executeTargets(Project.java:1068)
    at org.eclipse.ant.internal.ui.antsupport.InternalAntRunner.run(InternalAntRunner.java:423)
    at org.eclipse.ant.internal.ui.antsupport.InternalAntRunner.main(InternalAntRunner.java:137)

查看JDependTask.java文件的397行:

1
2
3
4
5
6
if (getSourcespath() == null && getClassespath() == null) {
    throw new BuildException("Missing classespath required argument");
} else if (getClassespath() == null) {
    String msg = "sourcespath is deprecated in JDepend >= 2.5 - please convert to classespath";
    log(msg);
}

发现是getClassespath() == null了,跳到221行:

1
2
3
public Path getClassespath() {
    return classesPath;
}

不是写了classpath吗?怎么classesPath的值会等于null呢?结果发现在247行有:

1
2
3
4
5
6
7
public void setClasspath(Path classpath) {
    if (compileClasspath == null) {
        compileClasspath = classpath;
    } else {
        compileClasspath.append(classpath);
    }
}

原来是在build.xml中把classespath错写成classpath了,那样当然有问题啦。改过来,再运行就可以了。

Ant系列之用Checkstyle检查代码规范的问题

首先将checkstyle-all-4.1.jar文件拷贝到项目的lib目录下,并建立CheckStyle配置文件。

然后在build.xml文件中声明CheckStyle任务:

1
<taskdef resource="checkstyletask.properties" classpath="${lib.dir}/checkstyle-all-4.1.jar" />

接着是建立CheckStyle任务:

1
2
3
4
5
6
7
8
9
10
<target name="checkstyle" description="Generates a report of code convention violations.">
    <checkstyle config="${docs.dir}/checkstyle_checks.xml" failOnViolation="false">
        <fileset dir="${src.dir}" includes="**/*.java" />
        <formatter type="xml" toFile="${checkstyle.dir}/checkstyle_report.xml" />
    </checkstyle>
    <xslt basedir="${checkstyle.dir}" destdir="${checkstyle.dir}/html" extension=".html"
            style="${docs.dir}/checkstyle-frames.xsl">
        <param name="output.dir" expression="${checkstyle.dir}/html" />
    </xslt>
</target>

其中output.dir是checkstyle-frames.xsl中的参数:

1
<xsl:param name="output.dir" />

在运行任务时可能会出现如下异常:

1
javax.xml.transform.TransformerException: java.lang.RuntimeException: Unrecognized XSLTC extension 'org.apache.xalan.xslt.extensions.Redirect:write'

可以将checkstyle-frames.xsl中的:

1
xmlns:redirect="org.apache.xalan.xslt.extensions.Redirect"

改成下面的内容:

1
xmlns:redirect="http://xml.apache.org/xalan/redirect"

如果出现这样的异常:

1
Got an exception - java.lang.RuntimeException: Unable to get class information for

可以修改对抛出异常的限制:允许抛出非检查型异常异常和抛出另一个已声明异常的子类。

1
2
3
4
<module name="RedundantThrows">
    <property name="allowUnchecked" value="true" />
    <property name="allowSubclasses" value="true" />
</module>

去除退出中国游戏中心时弹出的IE窗口

这次破解的版本是0.8.011.4。

中国游戏中心退出时弹出的IE窗口很烦人,决定做个补丁把它去除掉。

省略无数次尝试……确定退出中国游戏中心后打开的网页地址是 http://www.chinagames.net/PlazaJump/open 。用W32Dasm反汇编iGame.exe,通过String Data References找到网址字符串,双击该字符串,看到以下程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
:0043397A 90                nop
:0043397B 90                nop
:0043397C 90                nop
:0043397D 90                nop
:0043397E 90                nop
:0043397F 90                nop
:00433980 A1A2034800        mov eax, dword ptr [004803A2]
:00433985 56                push esi
:00433986 85C0              test eax, eax
:00433988 8BF1              mov esi, ecx
:0043398A 7411              je 0043399D               -> 判断是否要打开IE窗口
:0043398C 6A00              push 00000000

* Possible StringData Ref from Data Obj ->"http://www.chinagames.net/PlazaJump/open/"
|
:0043398E 6860D84700        push 0047D860
:00433993 B9D0FC4700        mov ecx, 0047FCD0
:00433998 E8B3D2FFFF        call 00430C50             -> 打开IE窗口

* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0043398A(C)
|
:0043399D 8BCE              mov ecx, esi

从上面可以知道,只要将0043398A处的je指令改成jmp指令就可以避免退出时弹出的IE窗口了。

现在使用CodeFusion来制作一个文件补丁。它有三种制作补丁的方案,这里使用Find&Replace方法。将0043397A到00433998段的数据作为查找匹配数据,并将0043398A处的74(je指令)改成eb(jmp指令),然后按照步骤生成补丁文件就可以了。

2008/3/19更新

应ls的要求制作了一个针对最新版本(0.8.011.10)的补丁,和原来的补丁放在一起提供下载。

补丁文件