2009年8月31日星期一
[心得]WebChat引发的一些做Web程序的问题
现在,我通过在服务器维护一个在线用户列表和客户端得cookies同时来判断用户是否登录。
问题一:如何确保同一账号只有一个登录?
当cookies失效且服务器端仍旧处于登录状态时,此用户再登陆就会失败,但实际上他是非正常的退出的,比如浏览器关闭等等。这就导致了cookies和服务器数据的不一致。
解决方法一:允许单账户多点登录
实现难度一般,但毕竟系统已经接近完成,且强行修改原代码导致了存在的一些问题。现在按照这种思路能得到的结果是:可以单账户多点登录、但任一登录者退出会导致其他人也退出。
解决方法二:在连接时不同向服务器发送消息,证明登录
实现难度非常大,不仅修改客户端逻辑,更要改服务器端。按照这思路没改成功。
解决方案三:允许强行登录、将原登录者挤下
实现难度也比较大,强行登陆容易做但是将原登录者挤下则不容易。要修改的东西很多,主要是无法标识哪个登录者是原先登录的。
一时未能解决头比较大,但是毕竟完成了第一种解决方案,虽然有些隐藏问题!
之后我分析了WebQQ的判断登录的方法,得出的结果是:WebQQ通过禁止关闭标签页、禁止刷新、禁止关闭浏览器等方法(如果要强行刷新或或关闭标签页则会提示对话框并退出登录),这样确保了页面无故关闭或浏览器关闭导致了cookies失效。但是当机器睡眠时,浏览器进程也休眠也会导致cookies失效,不过qq却能显示出你的连接断开了(题外话:拿Webqq挂qq号是不可取的)。WebQQ也使用了反向AJAX技术使得服务器来调用客户端代码(应该用了DWR或其他现有技术),可以看出因为需要反向AJAX所以它不停的向服务器发送请求,但发送的请求里信息奇怪不知道是否起了证明客户端处于登录的状态。个人认为没有,因为发送信息很简单,而且第二种方案服务器开销较大,即使是腾讯也应该会省钱的。
其次WebQQ多点登录是不可能的,当你的账户从另一个地方登录的时候,原登录者会被挤下,也就是说服务器能确定客户端的状态,并发送信息给客户端。但并不是次次都能挤下去,有的时候就挤不下别人。就算能把别人挤下,原登录者也可以做一些操作,我测试过修改签名可以使用。也就是说登录者每进行一个操作不会次次都验证服务器端是否登录,只验证cookies是否正确。这点实现是和我一下,毕竟次次都验证消耗服务器资源太多。
经过这么比较,决定使用第三种方案,并且需要禁止刷新等等动作,设置cookies直至浏览器关闭才失效,至于将原登录者挤下线就比较困难了。现在正在修改当中!
html中input的submit类型与button类型的区别
而button类型只是普通的按钮,一般用onclick属性指定动作。
2009年8月27日星期四
Re: JQuery所支持的是
选择器 描述
E[A=V] 匹配所有元素E,其特性值为V
E[A^=V] 匹配所有元素E、其特性A的值以V开头
E[A$=V] 匹配所有元素E、其特性A的值以V结尾
E[A*=V] 匹配所有元素E、其特性A的值包含V
选择器 描述
* 匹配任何元素
E 匹配标签名称为E的所有元素
E F 匹配标签名称为F、作为E的后代节点的所有元素
E>F 匹配标签名称为F、作为E的直接子节点的所有元素
E+F 匹配前面是邻近兄弟节点E的所有元素F(E和F紧挨着)
E-F 匹配前面是任何兄弟节点E的所有元素F(E和F可以不紧埃着)
E:has(F) 匹配标签名称为E、至少有一个标签名称为F的后代节点的所有元素
E.C 匹配带有类名C的所有元素E。.C等效于*.C
E#I 匹配id特性值为I的元素E。#I等效于*#I
E[A] 匹配带有特性A的所有元素E(不管特性A的值是什么)
JQuery所支持的是
* 匹配任何元素
E 匹配标签名称为E的所有元素
E F 匹配标签名称为F、作为E的后代节点的所有元素
E>F 匹配标签名称为F、作为E的直接子节点的所有元素
E+F 匹配前面是邻近兄弟节点E的所有元素F(E和F紧挨着)
E-F 匹配前面是任何兄弟节点E的所有元素F(E和F可以不紧埃着)
E:has(F) 匹配标签名称为E、至少有一个标签名称为F的后代节点的所有元素
E.C 匹配带有类名C的所有元素E。.C等效于*.C
E#I 匹配id特性值为I的元素E。#I等效于*#I
E[A] 匹配带有特性A的所有元素E(不管特性A的值是什么)
2009年8月25日星期二
Tomoyo Project(LSM)
- PLymouth
- 完全重写了 Netprofile 工具
- Tomoyo-gui
- KDE 4.3
- GNOME 2.27.5
- Kernel 2.6.31 rc6
CSS选择符、声明与注释
>
> 选择符名字
>
> {
>
> 声明;
>
> }
常用的三种选择符
xhtml标签选择符,比如 p标签选择符(代表所有的段落都使用这个CSS样式),比如 p{font-size:12px;
id选择符,唯一性选择符,比如 #dreamdured{color:red;},就是在名字前增加了一个#,id选择符在一个页面中只能出现一次,在整个网站中也最好出现一次(这样有利于程序员控制此元素,有多个一样名称的元素,就无法分开不好控制了).
class选择符,多重选择符,比如.dreamdublue{color:blue;},就是在名字前增加了一个.,class选择符在一个页面中可以出现多次(注意下面的示例中class选择符的用法).
CSS声明语法:
>
> 属性:属性值;
> 选择符名字1,选择符名字2,选择符名字3
>
> {
>
> 声明1;
>
> 声明2;
>
> }
CSS注释语法
> /* 注释内容 */
Javascript location对象
- location,中文"位置"的意思
- 引用网址:http://www.dreamdu.com/javascript/window.location/
- location既是window对象的属性又是document对象的属性
- location包含8个属性,其中7个都是当前窗体的URL的一部分,剩下的也是最重要的一个是href属性,代表当前窗体的URL
- location的8个属性都是可读写的,但是只有href与hash的写才 有意义。例如改变location.href会重新定位到一个URL,而修改location.hash会跳到当前页面中的anchor(<a id="name">或者<div id="id">等)名字的标记(如果有),而且页面不会被重新加载
示例
document
.
write
(
window
.
location
==
document
.
location
)
;
location对象属性图示
location属性
- JavaScript hash 属性 -- 返回URL中#符号后面的内容
- JavaScript host 属性 -- 返回域名
- JavaScript hostname 属性 -- 返回主域名
- JavaScript href 属性 -- 返回当前文档的完整URL或设置当前文档的URL
- JavaScript pathname 属性 -- 返回URL中域名后的部分
- JavaScript port 属性 -- 返回URL中的端口
- JavaScript protocol 属性 -- 返回URL中的协议
- JavaScript search 属性 -- 返回URL中的查询字符串
- JavaScript assign() 函数 -- 设置当前文档的URL
- JavaScript replace() 函数 -- 设置当前文档的URL,并在history对象的地址列表中删除这个URL
- JavaScript reload() 函数 -- 重新载入当前文档(从server服务器端)
- JavaScript toString()函数 -- 返回location对象href属性当前的值
2009年8月24日星期一
[转]MTJS中的元素显示和隐藏的封装
原帖:http://www.css88.com/archives/1793
页面元素的显示和隐藏在前端开发中经常用到的,原来我是这样写的,
hide: function(el) { el.style.display = "none"; } show: function(el) { el.style.display = "block"; }
这里在show函数上就有个问题,如果元素是display:inline话就有问题了,然后我又改成了:
hide: function(el) { el.style.display = "none"; } show: function(el) { el.style.display = " "; }
这也是经常看到的一种,但是当用CSS类设置元素的display时,是computedStyle,而不属于style对象,方法display=”只是取消style对象的display值;最后记过查找在《精通javascript》找到了一下方法,
/** * 隐藏指定的DOM对象 * @id hide * @param {String} el DOM对象 * @return {none} */ hide: function(el) { el = this.get(el);//获取对象 var curDisplay=this.getStyle(el,"display");//获取对象的display属性值, if(curDisplay!="none"){ el._curDisplay=curDisplay;//增加对象的私有属性_curDisplay用来存放元素隐藏前的display属性值; } el.style.display = "none"; }, /** * 显示指定的DOM对象 * @id show * @param {String} el DOM对象 * @return {none} */ show: function(el) { el = this.get(el); el.style.display = el._curDisplay||""; //取对象的display属性值设置成元素隐藏前的display属性值,如果没有就去“” },
[转]DOM标准与IE的html元素事件模型区别
HTML元素事件是浏览器内在自动产生的,当有事件发生时html元素会向外界(这里主要指元素事件的订阅者)发出各种事件,如click,onmouseover,onmouseout等等。
DOM事件流
DOM(文档对象模型)结构是一个树型结构,当一个HTML元素产生一个事件时,该事件会在元素结点与根结点之间的路径传播,路径所经过的结点都会收到该事件,这个传播过程可称为DOM事件流。
主流浏览器的事件模型
早在2004前在HTML元素事件的订阅,发送,传播,处理模型上各浏览器实现并不一致,直到DOM Level3中规定后,多数主流浏览器才陆陆续续支持DOM标准的事件处理模型 — 捕获型与冒泡型。
目前除IE浏览器外,其它主流的Firefox, Opera, Safari都支持标准的DOM事件处理模型。IE仍然使用自己的模型,即冒泡型,它模型的一部份被DOM采用,这点对于开发者来说也是有好处的,只使用DOM标准,IE都共有的事件处理方式才能有效的跨浏览器。
冒泡型事件(Bubbling)
这是IE浏览器对事件模型的实现,也是最容易理解的,至少笔者觉得比较符合实际的。冒泡,顾名思义,事件像个水中的气泡一样一直往上冒,直到顶端。从DOM树型结构上理解,就是事件由叶子结点沿祖先结点一直向上传递直到根结点;从浏览器界面视图HTML元素排列层次上理解就是事件由具有从属关系的最确定的目标元素一直传递到最不确定的目标元素.
捕获型事件(Capturing)
Netscape Navigator的实现,它与冒泡型刚好相反,由DOM树最顶层元素一直到最精确的元素,这个事件模型对于开发者来说(至少是我..)有点费解,因为直观上的理解应该如同冒泡型,事件传递应该由最确定的元素,即事件产生元素开始。
但这个模型在某些情况下也是很有用的,接下来会讲解到。
DOM标准事件模型
因为两个不同的模型都有其优点和解释,DOM标准支持捕获型与冒泡型,可以说是它们两者的结合体。它可以在一个DOM元素上绑定多个事件处理器,并且在处理函数内部,this关键字仍然指向被绑定的DOM元素,另外处理函数参数列表的第一个位置传递事件event对象。
首先是捕获式传递事件,接着是冒泡式传递,所以,如果一个处理函数既注册了捕获型事件的监听,又注册冒泡型事件监听,那么在DOM事件模型中它就会被调用两次。
注册与移除事件监听器
注册事件监听器,或又称订阅事件,当元素事件发生时浏览器回调该监听函数执行事件处理。目前主流浏览器中有两种注册事件的方法,一种是IE浏览器的,另一种是DOM标准的。
1.直接JS或HTML挂载法
移除时将事件属性设为nul即可,这个也是最常用的方法了,优缺点也是显然的:
・ 简单方便,在HTML中直接书写处理函数的代码块,在JS中给元素对应事件属性赋值即可
・ IE与DOM标准都支持的一种方法,它在IE与DOM标准中都是在事件冒泡过程中被调用的。
・ 可以在处理函数块内直接用this引用注册事件的元素
・ 要给元素注册多个监听器,就不能用这方法了
2. IE下注册多个事件监听器与移除监听器方法
IE浏览器中HTML元素有个attachEvent方法允许外界注册该元素多个事件监听器,例如
attachEvent接受两个参数。第一个参数是事件名称,第二个参数observer是回调处理函数。这里得说明一下,有个经常会出错的地方,IE下利用attachEvent注册的处理函数调用时this指向不再是先前注册事件的元素,这时的this为window对象了,笔者很奇怪IE为什么要这么做,完全看不出好处所在。
要移除先前注册的事件的监听器,调用element的detachEvent方法即可,参数相同。
3. DOM标准下注册多个事件监听器与移除监听器方法
实现DOM标准的浏览器与IE浏览器中注册元素事件监听器方式有所不同,它通过元素的addEventListener方法注册,该方法既支持注册冒泡型事件处理,又支持捕获型事件处理。
addEventListener方法接受三个参数。第一个参数是事件名称,值得注意的是,这里事件名称与IE的不同,事件名称是没’on’开头的;第二个参数observer是回调处理函数;第三个参数注明该处理回调函数是在事件传递过程中的捕获阶段被调用还是冒泡阶段被调用
移除已注册的事件监听器调用element的removeEventListener即可,参数不变.
跨浏览器的注册与移除元素事件监听器方案
弄清楚DOM标准与IE的注册元素事件监听器之间的异同后,就可以实现一个跨浏览器的注册与移除元素事件监听器方案:
function addEventHandler(element, evtName, callback, useCapture) {
//DOM标准
if (element.addEventListener) {
element.addEventListener(evtName, callback, useCapture);
} else {
//IE方式,忽略useCapture参数
element.attachEvent('on' + evtName, callback);
}
}
//移除
//注册
function removeEventHandler(element, evtName, callback, useCapture) {
//DOM标准
if (element.removeEventListener) {
element.removeEventListener(evtName, callback, useCapture);
} else {
//IE方式,忽略useCapture参数
element.dettachEvent('on' + evtName, callback);
}
}
如何取消浏览器事件的传递与事件传递后浏览器的默认处理
先说明取消事件传递与浏览器事件传递后的默认处理是两个不同的概念,可能很多同学朋友分不清,或者根本不存在这两个概念。
取消事件传递是指,停止捕获型事件或冒泡型事件的进一步传递。例如上图中的冒泡型事件传递中,在body处理停止事件传递后,位于上层的document的事件监听器就不再收到通知,不再被处理。
事件传递后的默认处理是指,通常浏览器在事件传递并处理完后会执行与该事件关联的默认动作(如果存在这样的动作)。例如,如果表单中input type 属性是 “submit”,点击后在事件传播完浏览器就就自动提交表单。又例如,input 元素的 keydown 事件发生并处理后,浏览器默认会将用户键入的字符自动追加到 input 元素的值中。
要取消浏览器的件传递,IE与DOM标准又有所不同。
在IE下,通过设置event对象的cancelBubble为true即可。
window.event.cancelBubble = true;
}
DOM标准通过调用event对象的stopPropagation()方法即可。
event.stopPropagation();
}
因些,跨浏览器的停止事件传递的方法是:
event = event || window.event;
if(event.stopPropagation)
event.stopPropagation();
else event.cancelBubble = true;
}
取消事件传递后的默认处理,IE与DOM标准又不所不同。
在IE下,通过设置event对象的returnValue为false即可。
window.event.returnValue = false;
}
DOM标准通过调用event对象的preventDefault()方法即可。
event.preventDefault();
}
因些,跨浏览器的取消事件传递后的默认处理方法是:
event = event || window.event;
if(event.preventDefault)
event.preventDefault();
else event.returnValue = false;
}
捕获型事件模型与冒泡型事件模型的应用场合
标准事件模型为我们提供了两种方案,可能很多朋友分不清这两种不同模型有啥好处,为什么不只采取一种模型。
这里抛开IE浏览器讨论(IE只有一种,没法选择)什么情况下适合哪种事件模型。
1. 捕获型应用场合
捕获型事件传递由最不精确的祖先元素一直到最精确的事件源元素,传递方式与操作系统中的全局快捷键与应用程序快捷键相似。当一个系统组合键发生时,如果注册了系统全局快捷键监听器,该事件就先被操作系统层捕获,全局监听器就先于应用程序快捷键监听器得到通知,也就是全局的先获得控制权,它有权阻止事件的进一步传递。所以捕获型事件模型适用于作全局范围内的监听,这里的全局是相对的全局,相对于某个顶层结点与该结点所有子孙结点形成的集合范围。
例如你想作全局的点击事件监听,相对于document结点与document下所有的子结点,在某个条件下要求所有的子结点点击无效,这种情况下冒泡模型就解决不了了,而捕获型却非常适合,可以在最顶层结点添加捕获型事件监听器,伪码如下:
if(canEventPass == false) {
//取消事件进一步向子结点传递和冒泡传递
event.stopPropagation();
//取消浏览器事件后的默认执行
event.preventDefault();
}
}
这样一来,当canEventPass条件为假时,document下所有的子结点click注册事件都不会被浏览器处理。
2. 冒泡型的应用场合
可以说我们平时用的都是冒泡事件模型,因为IE只支持这模型。这里还是说说,在恰当利用该模型可以提高脚本性能。在元素一些频繁触发的事件中,如onmousemove, onmouseover,onmouseout,如果明确事件处理后没必要进一步传递,那么就可以大胆的取消它。此外,对于子结点事件监听器的处理会对父层监听器处理造成负面影响的,也应该在子结点监听器中禁止事件进一步向上传递以消除影响。
综合案例分析
最后结合下面HTML代码作分析:
<div id="div0" onclick="alert('current is '+this.id)">
<div id="div1" onclick="alert('current is '+this.id)">
<div id="div2" onclick="alert('current is '+this.id)">
<div id="event_source"
onclick="alert('current is '+this.id)"
style="height:200px;width:200px;background-color:red;">
</div>
</div>
</div>
</div>
</body>
HTML运行后点击红色区域,这是最里层的DIV,根据上面说明,无论是DOM标准还是IE,直接写在html里的监听处理函数是事件冒泡传递时调用的,由最里层一直往上传递,所以会先后出现
current is event_source
current is div2
current is div1
current is div0
current is body
添加以下片段:
addEventHandler(div2, 'click', function(event){
event = event || window.event;
if(event.stopPropagation)
event.stopPropagation();
else event.cancelBubble = true;
}, false);
当点击红色区域后,根据上面说明,在泡冒泡处理期间,事件传递到div2后被停止传递了,所以div2上层的元素收不到通知,所以会先后出现:
current is event_source
current is div2
在支持DOM标准的浏览器中,添加以下代码:
event.stopPropagation();
}, true);
以上代码中的监听函数由于是捕获型传递时被调用的,所以点击红色区域后,虽然事件源是ID为event_source的元素,但捕获型选传递,从最顶层开始,body结点监听函数先被调用,并且取消了事件进一步向下传递,所以只会出现
current is body
什么是BOM
- BOM是browser object model的缩写,简称浏览器对象模型
- BOM提供了独立于内容而与浏览器窗口进行交互的对象
- 由于BOM主要用于管理窗口与窗口之间的通讯,因此其核心对象是window
- BOM由一系列相关的对象构成,并且每个对象都提供了很多方法与属性
- BOM缺乏标准,JavaScript语法的标准化组织是ECMA,DOM的标准化组织是W3C
- BOM最初是Netscape浏览器标准的一部分
window对象是BOM的顶层(核心)对象,所有对象都是通过它延伸出来的,也可以称为window的子对象。
由于window是顶层对象,因此调用它的子对象时可以不显示的指明window对象,例如下面两行代码是一样的:
示例
document
.
write
(
"
www.dreamdu.com
"
)
;
window
.
document
.
write
(
"
www.dreamdu.com
"
)
;
[转]反对if行动
意大利XP倡导者Francesco Cirillo为他著名的“反对if行动”创建了一个网站,一时吸引了不少支持者。Francesco认为:
if所带来的问题主要在于建立了模块(方法、对象、组件等)之间的依赖,也增加了代码路径的分支(这会降低代码的可读性)。
Francesco举了一个示例,认为如下的代码:
// Bond class double calculateValue() { if(_type == BTP) { return calculateBTPValue(); } else if(_type == BOT) { return calculateBOTValue(); } else { return calculateEUBValue(); } }
应该使用多态将显式的if或switch语句消除:
// Bond class double calculateValue() { _bondProfile.calculate(); } // AbstractBondProfile class abstract double calculate(); // classe BTPBondProfile >> AbstractBondProfile double calculate() { ... } // classe BOTBondProfile >> AbstractBondProfile double calculate() { ... } // classe EUBondProfile >> AbstractBondProfile double calculate() { ... }
Francesco认为这样做的好处在于
当我们需要增加新的bond类型时,只需创建一个新的类型来保存这部分独立逻辑即可。
创建抽象类或接口并非是改进的唯一方式,我们的目的是将程序变得更灵活、更易于交流、更容易测试、并且随时拥抱变化。
对于“反对if行动”,Matteo Vaccari补充了几点做法:
修改前 | 修改后 | |
---|---|---|
直接使用布尔运算结果 | if (foo) { if (bar) { return true; } } if (baz) { return true; } else { return false; } | return (foo && bar || baz); |
使用辅助函数 | if (x > y) return x; return y; | return max(x, y); |
灵活使用0的作用 | int arraySum(int[] array) { if (array.length == 0) { return 0; } int sum = array[0]; for (int i=1; i < array.length; i++) { sum += array[i]; } return sum; } | int arraySum(int[] array) { int sum = 0; for (int i=0; i < array.length; i++) { sum += array[i]; } return sum; } |
不过社区中对此也有不同看法,有人讽刺道:
哦,这里我发现了一个问题。尊敬的客户,您前一个顾问使用了“if”语句,让我把这个函数替换为80个新类,这样显得更加敏捷一些。
Don't Repeat Yourself看上去更敏捷一些。更好的做法是:等到出现第3遍重复代码的时候才去重构(不过也别等太久了)。这样可以避免很多无所谓的代码,也可以让程序员有更灵活的选择余地。
也有人为“反对if行动”进行了补充:
我想这个行动并不是说“删除所有的if代码”,而是将类型判断逻辑使用多态进行替换。如果把它称为“反对类型字段行动”会更合适一些。
“反对if行动”也在征集“if-free”的代码示例,您也可以在那里提交代码片断。事实上,国内社区也提出了不少消除繁琐if语句的案例,如:
- Jeffrey Zhao编写了一个简单的类库,使.NET中的枚举类型可以携带一些信息,以便统一地获取数据或进行运算。
- 木野狐使用了State模式,简化了不同状态下,Web界面中相应组件的样式切换方式。
- Tristan G设计了一组流畅的API,使开发人员能够方便地编写实体验证规则。
您在这方面是否也有独特的经验呢?不妨也一起分享出来吧。
2009年8月23日星期日
Javascript的=、==与===
==是等于运算符,用来判断两个操作数是否相等,并且会返回true或false,比如 a==b
===是恒等于,不仅检查值,还检查类型。
例子:
var a=5,b="5";
document.write(a==b);
document.write(a===b);
得到:
true
false
JavaScript关键词与保留字
JavaScript关键词与保留字
break | case | catch | continue | default |
delete | do | else | finally | for |
function | if | in | instanceof | new |
return | switch | this | throw | try |
typeof | var | void | while | wit |
abstract | boolean | byte | char | class |
const | debugger | double | enum | export |
extends | final | float | goto | implements |
import | int | interface | long | native |
package | private | protected | public | short |
static | super | synchronized | throws | transient |
volatile |
JavaScript变量匈牙利命名类型
JavaScript变量起名 类型 | 变量命名前缀 |
---|---|
Array 数组 | a |
Boolean 布尔 | b |
Float 浮点 | l |
Function 函数 | f |
Integer(int) 整型 | n |
Object 对象 | o |
Regular Expression 正则 | r |
String 字符串 | s |
2009年8月20日星期四
[BugFixed]解决昨天遇到的一个问题
[Bug]今天遇到的一个JAVA问题!
2009年8月14日星期五
DWR错误的解决过程
Hi everyone !
I'm new to DWR. I'm working on a project using DWR. I met a problem,when I call the server-side code.
Server-side code:
public class PasswordService {
public void changePassword(String oldPassword, String newPassword) throws WrongPasswordException{
HttpSession session = (WebContextFactory.get()).getSession();
// Get user's name
String name = ((User)session.getAttribute("user")).getName();
if (name !=null && oldPassword != null && newPassword != null) {
if (!changePassword(name, oldPassword, newPassword)) {
throw new WrongPasswordException();
}
}
}
}
Client-side Javascript code:
function change() {
var name = dwr.util.getValue("name");
var oldpsw = dwr.util.getValue("oldpassword");
var newpsw = dwr.util.getValue("newpassword");
Psw.changePassword(name,oldpsw,newpsw);
}
错误原因:
Javascript调用PasswordService类的函数Psw.changePassword()的参数与原java函数的方法不匹配。
可怜我整一个下午,都是粗心的惹得祸,修改代码是漏改的结果。
2009年8月6日星期四
[转]servlet采用单实例多线程模式开发
问题而. 如何在开发中保证servlet是单实例多线程的方式来工作(也就是说如何开发线程安全的servelt)
一. Servlet容器如何同时来处理多个请求
先说明几个概念:
工作者线程Work Thread:执行代码的一组线程
调度线程Dispatcher Thread:每个线程都具有分配给它的线程优先级,线程是根据优先级调度执行的
Servlet采用多线程来处理多个请求同时访问。servlet依赖于一个线程池来服务请求。线程池实际上是一系列的工作者线程集合。Servlet使用一个调度线程来管理工作者线程.
当容器收到一个Servlet请求,调度线程从线程池中选出一个工作者线程,将请求传递给该工作者线程,然后由该线程来执行Servlet的service方法。当这个线程正在执行的时候,容器收到另外一个请求,调度线程同样从线程池中选出另一个工作者线程来服务新的请求,容器并不关心这个请求是否访问的是同一个Servlet.当容器同时收到对同一个Servlet的多个请求的时候,那么这个Servlet的service()方法将在多线程中并发执行。
Servlet容器默认采用单实例多线程的方式来处理请求,这样减少产生Servlet实例的开销,提升了对请求的响应时间,对于Tomcat可以在server.xml中通过<Connector>元素设置线程池中线程的数目。
就实现来说:
调度者线程类所担负的责任如其名字,该类的责任是调度线程,只需要利用自己的属性完成自己的责任。所以该类是承担了责任的,并且该类的责任又集中到唯一的单体对象中。
而其他对象又依赖于该特定对象所承担的责任,我们就需要得到该特定对象。那该类就是一个单例模式的实现了。
二 如何开发线程安全的Servlet
1,变量的线程安全:这里的变量指字段和共享数据(如表单参数值)。
a,将 参数变量 本地化。多线程并不共享局部变量.所以我们要尽可能的在servlet中使用局部变量。
例如:String user = "";
user = request.getParameter("user");
b,使用同步块Synchronized,防止可能异步调用的代码块。这意味着线程需要排队处理。
在使用同板块的时候要尽可能的缩小同步代码的范围,不要直接在sevice方法和响应方法上使用同步,这样会严重影响性能。
2,属性的线程安全:ServletContext,HttpSession,ServletRequest对象中属性
ServletContext:(线程是不安全的)
ServletContext是可以多线程同时读/写属性的,线程是不安全的。要对属性的读写进行同步处理或者进行深度Clone()。
所以在Servlet上下文中尽可能少量保存会被修改(写)的数据,可以采取其他方式在多个Servlet中共享,比方我们可以使用单例模式来处理共享数据。
HttpSession:(线程是不安全的)
HttpSession对象在用户会话期间存在,只能在处理属于同一个Session的请求的线程中被访问,因此Session对象的属性访问理论上是线程安全的。
当用户打开多个同属于一个进程的浏览器窗口,在这些窗口的访问属于同一个Session,会出现多次请求,需要多个工作线程来处理请求,可能造成同时多线程读写属性。
这时我们需要对属性的读写进行同步处理:使用同步块Synchronized和使用读/写器来解决。
ServletRequest:(线程是安全的)
对于每一个请求,由一个工作线程来执行,都会创建有一个新的ServletRequest对象,所以ServletRequest对象只能在一个线程中被访问。ServletRequest是线程安全的。
注意:ServletRequest对象在service方法的范围内是有效的,不要试图在service方法结束后仍然保存请求对象的引用。
3,使用同步的集合类:
使用Vector代替ArrayList,使用Hashtable代替HashMap。
4,不要在Servlet中创建自己的线程来完成某个功能。
Servlet本身就是多线程的,在Servlet中再创建线程,将导致执行情况复杂化,出现多线程安全问题。
5,在多个servlet中对外部对象(比方文件)进行修改操作一定要加锁,做到互斥的访问。
6,javax.servlet.SingleThreadModel接口是一个标识接口,如果一个Servlet实现了这个接口,那Servlet容器将保证在一个时刻仅有一个线程可以在给定的servlet实例的service方法中执行。将其他所有请求进行排队。
服务器可以使用多个实例来处理请求,代替单个实例的请求排队带来的效益问题。服务器创建一个Servlet类的多个Servlet实例组成的实例池,对于每个请求分配Servlet实例进行响应处理,之后放回到实例池中等待下此请求。这样就造成并发访问的问题。
此时,局部变量(字段)也是安全的,但对于全局变量和共享数据是不安全的,需要进行同步处理。而对于这样多实例的情况SingleThreadModel接口并不能解决并发访问问题。
SingleThreadModel接口在servlet规范中已经被废弃了。
内容转载自http://blog.csdn.net/qfs_v/archive/2008/07/15/2652 097.aspx
[转]JVM内存管理总结
如图所示,JVM主要包括两个子系统和两个组件。两个子系统分别是Class loader子系统和Execution engine(执行引擎) 子系统;两个组件分别是Runtime data area (运行时数据区域)组件和Native interface(本地接口)组件。
Class loader子系统的作用:根据给定的全限定名类名(如 java.lang.Object)来装载class文件的内容到 Runtime data area中的method area(方法区域)。Java程序员可以extends java.lang.ClassLoader类来写自己的Class loader。
Execution engine子系统的作用:执行classes中的指令。任何JVM specification实现(JDK)的核心都是Execution engine,不同的JDK例如Sun 的JDK 和IBM的JDK好坏主要就取决于他们各自实现的Execution engine的好坏。
Native interface组件:与native libraries交互,是其它编程语言交互的接口。当调用native方法的时候,就进入了一个全新的并且不再受虚拟机限制的世界,所以也很容易出现JVM无法控制的native heap OutOfMemory。
Runtime Data Area组件:这就是我们常说的JVM的内存了。它主要分为五个部分——
1、Heap (堆):一个Java虚拟实例中只存在一个堆空间
2、Method Area(方法区域):被装载的class的信息存储在Method area的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件内容并把它传输到虚拟机中。
3、Java Stack(java的栈):虚拟机只会直接对Java stack执行两种操作:以帧为单位的压栈或出栈
4、Program Counter(程序计数器):每一个线程都有它自己的PC寄存器,也是该线程启动时创建的。PC寄存器的内容总是指向下一条将被执行指令的饿地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。
5、Native method stack(本地方法栈):保存native方法进入区域的地址
以上五部分只有Heap 和Method Area是被所有线程的共享使用的;而Java stack, Program counter 和Native method stack是以线程为粒度的,每个线程独自拥有自己的部分。
了解JVM的系统结构,再来看看JVM内存回收问题了——
Sun的JVM Generational Collecting(垃圾回收)原理是这样的:把对象分为年青代(Young)、年老代(Tenured)、持久代(Perm),对不同生命周期的对象使用不同的算法。(基于对对象生命周期分析)
如上图所示,为Java堆中的各代分布。
1. Young(年轻代)
年轻代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制年老区(Tenured。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。
2. Tenured(年老代)
年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。
3. Perm(持久代)
用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。
举个例子:当在程序中生成对象时,正常对象会在年轻代中分配空间,如果是过大的对象也可能会直接在年老代生成(据观测在运行某程序时候每次会生成一个十兆的空间用收发消息,这部分内存就会直接在年老代分配)。年轻代在空间被分配完的时候就会发起内存回收,大部分内存会被回收,一部分幸存的内存会被拷贝至Survivor的from区,经过多次回收以后如果from区内存也分配完毕,就会也发生内存回收然后将剩余的对象拷贝至to区。等到to区也满的时候,就会再次发生内存回收然后把幸存的对象拷贝至年老区。
通常我们说的JVM内存回收总是在指堆内存回收,确实只有堆中的内容是动态申请分配的,所以以上对象的年轻代和年老代都是指的JVM的Heap空间,而持久代则是之前提到的Method Area,不属于Heap。
了解完这些之后,以下的转载一热衷于钻研技术的哥们Richen Wang关于内存管理的一些建议——
1、手动将生成的无用对象,中间对象置为null,加快内存回收。
2、对象池技术 如果生成的对象是可重用的对象,只是其中的属性不同时,可以考虑采用对象池来较少对象的生成。如果有空闲的对象就从对象池中取出使用,没有再生成新的对象,大大提高了对象的复用率。
3、JVM调优 通过配置JVM的参数来提高垃圾回收的速度,如果在没有出现内存泄露且上面两种办法都不能保证内存的回收时,可以考虑采用JVM调优的方式来解决,不过一定要经过实体机的长期测试,因为不同的参数可能引起不同的效果。如-Xnoclassgc参数等。
推荐的两款内存检测工具
1、jconsole JDK自带的内存监测工具,路径jdk bin目录下jconsole.exe,双击可运行。连接方式有两种,第一种是本地方式如调试时运行的进程可以直接连,第二种是远程方式,可以连接以服务形式启动的进程。远程连接方式是:在目标进程的jvm启动参数中添加-Dcom.sun.management.jmxremote.port=1090 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false 1090是监听的端口号具体使用时要进行修改,然后使用IP加端口号连接即可。通过该工具可以监测到当时内存的大小,CPU的使用量以及类的加载,还提供了手动gc的功能。优点是效率高,速度快,在不影响进行运行的情况下监测产品的运行。缺点是无法看到类或者对象之类的具体信息。使用方式很简单点击几下就可以知道功能如何了,确实有不明白之处可以上网查询文档。
2、JProfiler 收费的工具,但是到处都有破解办法。安装好以后按照配置调试的方式配置好一个本地的session即可运行。可以监测当时的内存、CPU、线程等,能具体的列出内存的占用情况,还可以就某个类进行分析。优点很多,缺点太影响速度,而且有的类可能无法被织入方法,例如我使用jprofiler时一直没有备份成功过,总会有一些类的错误。
[转]单例的懒汉模式和饿汉模式
public class Singleton{
private static Singleton singleton = new Singleton ();
private Singleton (){}
public static Singleton getInstance(){return singletion;}
}
懒汉式:
public class Singleton{
private static Singleton singleton = null;
public static synchronized synchronized getInstance(){
if(singleton==null){
singleton = new Singleton();
}
return singleton;
}
}
比较:
饿汉式是线程安全的,在类创建的同时就已经创建好一个静态的对象供系统使用,以后不在改变
懒汉式如果在创建实例对象时不加上synchronized则会导致对对象的访问不是线程安全的
推荐使用第一种
这样确保线程安全的同时, 比上面创建静态实例域的办法还有一个好处就是:
SingletonHolder中可以使用静态方法替换静态域, 实现比较复杂的逻辑, 而不仅仅是new Singleton()这样简单地调用构造方法.
- public class Singleton { private Singleton() {} private static class SingletonHolder { static Singleton instance = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.instance; } }
[转]单例模式和static是两码事情
>那可能对象多了,单例在内存中就占了20M(假如,也许更多),那这样是得不到释放的,
这个现象是针对static而言的。我们可以让一个对象在内存是单例,但是当这样对象多了的时候,也可以释放,被GC清除。
这就涉及容器概念,也就是说,我们将这些对象单例用一个容器包容,不是直接将这些单例放到JVM,而是放到一个容器中,这个容器再放到JVM中;如果我们消灭这个容器对象,那么其中的单例对象就会被GC回收。
这个时候,单例概念已经不是原来JVM单例概念,这个单例是一定范围(时间或空间)内的单例,实际等同于对象的scope。
类似Spring/jdonFramework这样框架,就是引入了这样一个容器,如果我们将这个容器对象scope设置为Web的Application级别,也就是说,在某个Web项目中,这个容器是唯一的,那么容器中的单例对象也就是唯一。如果我们Web项目不再部署,退出,容器对象生命周期结束,其中所有单例对象生命周期也就结束。
注意:虽然我们变通了单例的生命周期,但是在单例有生命时段内,如果多个线程访问(写操作)单例中的状态,那么就有数据不一致问题,好事人如果在这个单例对状态访问操作加上同步锁,那么就可能将多线程的J2EE/JavaEE应用变成单线程。
所以,如果我们的服务或DAO都是无状态的,那么使用单例没有问题,但是谁能保证程序代码不修改,如果来个鲁莽的小伙子,将你的服务改成有状态,那么就出现很多奇怪问题了。
当然,如果你有一个称不上好习惯的习惯,不喜欢在类的字段中保存数据,什么都依赖数据库,那么使用单例也会一直没有问题,但是这种习惯是OO的吗?高负载下性能会好吗?这又涉及另外问题了。
所以,养成用多例Pool来支持你的服务,不失为一个克服上面的两全其美的好办法,但是POOL也有小的性能开销。
其实绕来绕去,我们发现:当初我们使用EJB时的最大困惑:有态和无态的选择,在Spring/jdonFramework中不是被消灭了,而是被转移了,表面简化了,但是这个问题永远回避不了。
2009年8月5日星期三
SQL数据类型和Java数据类型的对应关系
integer、int ---> int
tinyint、smallint ---> short
bigint ---> long
decimal、numeric ---> java.math.BigDecimal
float ---> float
double ---> double
char、varchar ---> String
boolean、bit ---> boolean
date ---> java.sql.Date
time ---> java.sql.Time
timestamp ---> java.sql.Timestamp
blob ---> java.sql.Blob
clob ---> java.sql.Clob
array ---> java.sql.Array
2009年8月4日星期二
[转]接待领导慰问的“专业户”农民(太牛了)
有一个专门接待上级领导慰问的“专业户”,他就是安徽省阜南县王家坝镇农民郑继超。据不完全统计,在短短3个月内,到过郑继超老伯家的领导有(按职务由低到高排列,乡、村领导不算): 阜南县副县长李国庆
阜阳市政府秘书长李子鹏
市政协副主席陶克贵
阜阳市副市长胡明莹
阜阳市副市长刘绍太
阜阳市副市长杜长平
阜阳市人大副主任储金水
阜阳市委常委、副市长王海彦
阜阳市委常委亓龙,阜阳市委常委、原常务副市长(现为市人大常委会副主任)宋家伟
阜阳市委副书记孟庆银
阜阳市委副书记、市长孙云飞
省民政厅厅长李宏塔
安徽省政府副秘书长张韶春
阜阳市委书记、市人大常委会主任宋卫平
安徽省政府秘书长张俊
安徽省人大常委会副主任、原阜阳市委书记(现已不再兼此职)胡连松
安徽省委常委、副省长赵树丛
中央直属机关工委副书记赵凯
安徽省委副书记、代省长王三运
安徽省委书记王金山
中国人民共和国国家主席胡主席
一进屋,总书记就拉着主人的手,深情地说:“去年夏天淮河发大水,乡亲们遭灾了。 现在已是寒冬腊月,你们生产生活上还有哪些困难,我一直放心不下,今天特地来看看。”郑继超告诉总书记,依靠党委和政府的帮助,自己家盖起了新房,生活也 都有了保障。“国家给的蓄洪补偿款发到手了吗?”总书记问。 “发到手了,在这儿呢!”郑继超说着,从胸口的衣兜里掏出一个红色存折。胡锦涛接过存折,一边看一边念着款数,念完后笑着对郑 继超说:“这样,我心里就踏实了。” 离开郑继超家时,看到他家院子里架起了自来水管,胡锦涛便走过去,拧开水龙头,用手捧起冰凉的水,喝了一口。总书记心系群众的这一幕,深深打动了在场的所 有人。来源 新华网 2007年12月22日
更多报道可见:
http://www.ahxf.gov.cn/shownew.asp?ID=53372
http://www.fy.gov.cn/html/20071205/200712051052274638.shtml
http://www.fyradio.com.cn/Html/fynews/20071206192005.html
http://www.hf365.com/html/01/02/20071206/78602.htm
http://www.kpwww.com/readnews.asp?newsid=1892
http://www.glmj.gov.cn/news/xuanjiao/fengcai/2007/12/1802.htm
http://www.ahxf.gov.cn/shownews.asp?ID=55868
http://www.hwcc.com.cn/newsdisplay/NewsDisplay.asp?id=184526
http://www.china.com.cn/chinese/zhuanti/qkjc/797823.htm
http://www.cctv.com/video/xinwen ... 0_20080114_12.shtml
2009年8月3日星期一
[转]spring jar包详细介绍
原文出处:http://blog.csdn.net/Pointer_v/archive/2007/10/30/1857078.aspx
Vista下配置MySQL
以下是转的方法:
1、去官方下载MYSQL 5.0。
2、修改防火墙的设置,在允许例外里添加新的端口,名字为mysql,开放的端口为TCP 3306。
这个大家应该都会,在Vista控制面板的Windows防火墙里面,点击“更改设置”-“例外”,点击“添加端口”就可以。
3、安装MYSQL 5.0,可以自己修改安装路径和组件等。
4、安装完成后,Mysql server instance config wizard应该是启动不了的,下面的工作就是为了解决这个问题的,因为这是配置Mysql的唯一途径。
5、下载Resource Hacker得简体中文版(如果你看的懂英文,那就直接下原版的)。
6、解压rh_chinese_big5.zip ,执行 ResHacker.exe
7、打开MySQLInstanceConfig.exe (在安� MySQL 的bin 目录中) ,可以用鼠标直接把exe等资源文件拖到Resource Hacker窗体上面。
8、左侧展开「24」→「1」→「1033」
9、右侧找到 这行
10、将 level="asAdministrator" 改成 level="requireAdministrator"
11、重新编译
12、保存后关闭 (如果不能保存,另存在 bin 目录外,然后覆盖回�也可以)
13、这时MySQLInstanceConfig.exe,就可以正常的配置了。
2009年8月2日星期日
[转]AJAX和REST 第一部分
服务器端 Web 应用程序因采用富应用程序模型和交付个性化内容而具备了融入式(immersive) 的特点,这种特点越突出,应用程序架构对 Web 架构风格 REST(Representational State Transfer)的违背就越多。这种违背会降低应用程序的可伸缩性,增加系统复杂性。通过与 REST 相互协调,Ajax 架构将使融入式 Web 应用程序消除这些负面影响,尽享 REST 那些出色的特性。
在短短 15 年中,World Wide Web 已经从一项研究实验成长为现代社会的技术支柱。最初发明 Web 的目的是使人们可以轻松发布和链接信息,现在它已经发展为软件应用程序的可行平台。但随着应用程序通过使用富应用程序模型和生成个性化内容而获得了更多的 融入性,它们的架构对 Web 架构风格 REST(Representational State Transfer)的违背也越来越多。这种违背会降低应用程序的可伸缩性,增加系统复杂性。
新兴的 Ajax Web 客户机架构风格让融入式 Web 应用程序与 REST 架构风格协调一致。使它们可以尽享 REST 那些出色的特性,同时又消除了应用程序违背 REST 准则时带来的不良特性。本文将介绍为融入式 Web 应用程序成功结合 Ajax 和 REST 的方法与原因。
|
尽管 World Wide Web 是在数十年的相关研究基础上建立起来的,但它的有效诞生日期是 1990 年 12 月,当时 Tim Berners-Lee 完成了 Web 主要组件的工作原型:统一资源标识符(URI)、HTTP、HTML、浏览器和服务器。Web 被迅猛采用,远远超过了先驱者们的预期。在 Roy Fielding 最出名的系列文章中(请参看 参考资料),他描述了自己当时的心情:
“尽管对其成功感到兴奋不已,但是 Internet 开发者社区逐渐开始担心,Web 使用的这种快速增长,以及早期 HTTP 的一些拙劣的网络特性,会快速压倒 Internet 基础设施所能承担的容量,从而导致突然的崩塌。”
|
Fielding 和其他人对 Web 架构及其是否能够足以支持各种扩展和用法重新进行了审视。这种重新审视的有形结果包括更新诸如 URI 和 HTTP 之类的一些重要标准。这种重新审视还获得了一些无形但却非常有意义的结果:为超级媒体应用程序确定了一种新的架构风格,Fielding 将其命名为 REST(Representational State Transfer)。Fielding 断言,使用且符合 REST 设计约束的 Web 上部署的组件可以充分利用 Web 的有用特性。他还警告说,违背 REST 准则的 Web 组件都将无法利用这些优点。
早期时,大部分 Web 站点和简单的 Web 应用程序实际上都是遵守 REST 准则的。但是随着融入式 Web 应用程序的日益普及,Web 应用程序架构逐渐开始背离 REST 准则了,此后因果循环,情况日益恶化。融入式服务器端 Web 架构的问题很难分析清楚,因为在使用这种架构风格的十年中,已经建立起这样一种信仰:这些问题都是 Web 应用程序架构所固有的。实际上,这并非是 Web 应用程序架构的问题。而是由服务器端 Web 应用程序架构风格所产生的问题。要打破这种偏见,我们来回顾一下整个架构是如何发展到现在这种状态的,这会很有帮助。我们将说明为什么在 Ajax 应用程序创建在商业上可行之后,过去接受的很多假设现在都不再成立了。
|
Berners-Lee 创造了 Web,最初是将 Web 作为研究人员远程共享文档和在文档之间创建简单链接以加速知识和思想传播的一种手段。然而,URI 标准的架构特征很快实现了除静态文件之外更多内容的共享。
Web 上最早的内容由一些静态 HTML 文档组成,其中有很多到其他静态文档的链接,如图 1 所示:
图 1. 提供静态文档的 Web 站点
REST 使静态文档的检索极其高效、可伸缩,这是因为它们可以根据 URI 和最后修改日期来轻松缓存。很快开发人员就超越了静态文档的领域,开始动态文档的提供。
Berners-Lee 和其他人设计了 URI 标准,为资源的统一唯一标识提供支持,同时使其表示(HTML、文本等)根据 Web 客户机(通常是 Web 浏览器)和 Web 服务器之间的协商结果而变化。由于 URI 将资源标识和资源的底层存储机制区分开来,因此 Web 开发人员可以创建一些程序,使之检查 URI 语法,并动态生成文档,将预先定义的 UI 元素和动态检索的数据(通常是从关系数据库中)合并在一起,如图 2 所示。尽管这些文档是生成的,但是它们的缓存特征与静态文件的完全相同。
图 2. 以嵌入 HTML 模板代码形式提供数据库记录的 Web 站点
此类早期应用程序的一个简单例子是统一目录 Web 应用程序。这种应用程序通常以如下方式工作:
- 用户在 Web 表单中输入名字(例如,
Bill Higgins
),并单击提交按钮。 - 表单根据输入的名字创建一个 URI,并从服务器上请求这个 URI 的内容(例如
GET http://psu.edu/Directory/Bill+Higgins
)。 - 服务器检查这个 URI,并使用这个学生的电话号码和地址来生成一个 Web 页面。
- 服务器将所生成的页面发回到用户的浏览器上。
这种交互的一个重要特性是它是幂等的(idempotent),也就是说除非底层资源发生变化(例如 Bill 修改了自己的电话号码),否则同一请求的结果总是相同的。这意味着浏览器或代理服务器都可以在本地对 Bill Higgins 的文档进行缓存,只要底层资源没有发生变化,那就可以从本地缓存中检索资源,而不再需要从远程服务器检索。这种方法能提高用户感受到的响应性,并增加系统 整体效率和可伸缩性。这些早期的动态 Web 应用程序可以很好地工作,将大量的信息送至用户指尖。
下一代 Web 应用程序的目标就是高度融入,提供个性化的内容和富应用程序模型。在过去十年中,Web 开发人员成功创建了这些融入式应用程序。一个非常恰当的例子是 Amazon.com 电子商务站点。当用户与 Amazon Web 应用程序进行交互时,它会创建复杂的客户页面来推荐有针对性的商品,显示浏览历史记录,并显示用户购物车中商品的价格。
|
融入式 Web 应用程序确实非常有用,但服务器端的融入式 Web 应用程序风格从根本上来说是不符合 REST 架构准则的。具体来说,它违背了一项关键的 REST 约束,并且没有利用 REST 最为重要的一些优点,因此又产生了一组新问题。
REST 的 “客户机-无状态-服务器” 约束禁止在服务器上保存会话状态。符合这一约束进行设计可以提高系统的可见性、可靠性和可伸缩性。但是融入式服务器端 Web 应用程序希望能够为单个用户提供大量个性化内容,因此必须在两种设计之间作出选择。第一种设计要在每个客户机请求中都发送大量状态信息,因此每个请求都完 整地保留了上下文的内容,服务器是无状态的。第二种解决方案表面上来看比较简单,应用程序开发人员和中间件供应商都比较倾向于这种方法,它只是简单地发送 一个用户标识,并在服务器端为这个标识关联一个 “用户会话”(如图 3 所示)。第二种设计直接违背了客户机-无状态-服务器约束。尽管它确实可以实现我们想要的用户功能(具体来说就是指个性化),但却对这个架构进行了极大的 改动。
图 3. 融入式服务器端 Web 应用程序,其中包含了大量服务器端会话状态
Java™ Servlet 的 HttpSession API 正是一个此类变动的例子。HttpSession 让我们可以在状态和特定用户之间建立关联。这个 API 看起来对于开发新手非常简单。实际上,它似乎可以将任何对象保存到 HttpSession 中,并且不需要自己实现任何特定的查找逻辑就可以将这些对象取出来。但是当我们开始在 HttpSession 中放入更多对象时,就会开始注意到我们的应用服务器要占用的内存和处理资源越来越多。很快我们就确定自己需要将应用程序部署到集群环境中来应对日益增加的 资源需求。然后就会认识到,要让 HttpSession 在集群环境中工作,每个对象都必须实现 Java 的 Serializable
接口,以使会话数据能够在集群环境中的服务器间传递。然后必须确定应用服务器在关机/重启过程中是否要继续维护会话数据。很快您就会质疑,违背客户机-无状态-服务器约束是否真的是一个好主意。(实际上,很多开发人员都不了解这个约束。)
融入式服务器端 Web 应用程序的第二个严重后果在于:它实际上不能利用 REST 的第一类支持进行资源缓存。引用 Fielding 的话来说,“添加缓存约束的优点是可以部分或完全避免某些交互操作,从而可以通过减少一系列交互的平均延迟来提高效率、可伸缩性以及用户可以感受到的性 能。不过这样做的代价是如果缓存中的陈旧数据与通过将请求直接发送给服务器而获得的数据有很大区别,那么缓存的可靠性就降低了。”
我们可以将融入式 Web 应用程序近似地看作一个活动实体,它会根据用户提供的新输入内容、其他人输入的新内容以及新的后台数据而不断发生变化。由于服务器必须根据多个用户与应用 程序的交互来生成每个页面,因此我们实际上无法两次生成相同的文档。因此,Web 浏览器或代理服务器无法缓存服务器资源。
有几种解决方案可以用来处理资源无法缓存的问题。一种就是创建细粒度的资源在服务器端的缓存,这样服务器就可以通过预先组合好的部分来构建一个粗粒 度的页面,而不是通过基本元素(HTML 和数据)从头开始一步步地构建这种页面了。但是问题依然存在:每个请求都会导致大量的服务器处理,这会损害系统的可伸缩性,还可能会对用户感受到的响应性 造成负面影响。
无法提供可缓存资源的另外一个结果是:动态程度相当高的 Web 应用程序必须显式地禁止搜索引擎和其他类型的 “机器人”作出请求,因为处理这类请求的成本都非常昂贵;而在符合 REST 准则的应用程序中,只需一次性地将某个资源提供给那一类 “机器人”,然后对它们的后续访问发送一条简单的 “Not-modified” 消息即可。
|
但是为什么要在一个负载过重的服务器上集中这么多的资源消耗呢?从理论上来说,我们什么时候可以将处理和内存需求分布到客户机呢?简单的答案是给定传统 Web 浏览器约束,这是不可行的(请参看 不使用 Ajax 的客户端处理)。但是 Ajax 架构风格使开发人员可以将处理和状态需求分布到客户机。请继续阅读,学习为什么选择使用 Ajax 风格的融入式应用程序可以继续遵循 REST 准则,并充分利用它的优势。
正如我们前面看到的一样,传统的服务器端 Web 应用程序将数据的标识和服务器上的动态数据元素合并在了一起,并将所构成的完整 HTML 文档返回给浏览器。Ajax 应用程序在其主要 UI 和浏览器中的主要逻辑方面有所不同;基于浏览器的应用程序代码可以在必要时获取新的服务器数据,并将这些数据织入当前页面(请参看 参考资料 中 Jesse James Garrett 有关 Ajax 的启蒙文章)。呈现和数据绑定的位置看起来可能是一个实现细节,但是这种区别会导致完全不同的架构风格。
人们通常将 Ajax 应用程序描述成无需在每次点击时彻底地刷新整页的 Web 页面。尽管这个描述非常确切,但是根本的动机在于彻底刷新整页会令用户不耐烦,从而无法获得愉快、融入式的用户体验。从架构的角度来看,整个页面全部刷新 的设计甚至非常危险,这种设计使您无法选择在客户机存储应用程序状态,这可能会导致妨碍应用程序充分利用 Web 最强大的架构设计点的设计决策。
Ajax 让我们不需要进行完全刷新就可以与服务器进行交互,这一事实使有状态客户机再次成为可用选择。这一点对于动态融入式 Web 应用程序架构的可能性有深远的影响:由于应用程序资源和数据资源的绑定转换到了客户端,因此这些应用程序都可以享受这两个世界中最好的东西 —— 融入式 Web 应用程序中动态、个性化的用户体验,以及遵守 REST 准则的应用程序中简单、可伸缩的架构。
设想一下,将 Amazon.com 彻底重新实现为一个 Ajax 应用程序 —— 一个 Web 页面可以从服务器上动态获取所有的数据。(出于商业原因,Amazon 可能并不希望这样做,不过那是其他文章讨论的话题了。)由于现在有很多 UI 和应用程序逻辑都可以在客户机而不是在服务器上运行,根据 Garrett 的说法,最初加载页面时需要下载 Amazon 的 Ajax “引擎”。这个引擎包含大量应用程序逻辑(以 JavaScript 代码实现),另外还有此后将使用从服务器上异步获取的数据填充的 UI 框架(见图 4):
图 4. 融入式 Ajax 应用程序
Ajax 引擎一个有趣的特征就是:尽管它包含了很多应用程序逻辑和表示框架元素,但是如果经过恰当的设计,它可以不包含任何业务数据或个性化内容。应用程序和表示 都冻结在部署时。在典型的 Web 环境中,应用程序资源可能 6 个月才会变更一次。这意味着负责隔离应用程序资源和数据资源的 Ajax 引擎是高度可缓存的。
Dojo Toolkit 就是一个很好的例子(请参看 参考资料)。 Dojo 提供了构建时工具来创建一个包含所有应用程序逻辑、表示和风格的压缩 JavaScript 文件。由于它终究只是一个文件,因此 Web 浏览器可以对其进行缓存,这意味着我们第二次访问启用 Dojo 的 Web 应用程序时,很可能就会从浏览器缓存中加载 Ajax,而不是从服务器上加载它。我们可以将这种情况与高度融入化的服务器端 Web 应用程序进行一下对比,后者每次请求都会导致大量的服务器处理,因为浏览器和网络中介不能对缓存不断变化的资源。
由于 Ajax 应用程序引擎只是一个文件,因此它也是可以使用代理缓存的。在大型的企业内部网中,只要有一名员工曾经下载过某个特定版本的应用程序的 Ajax 引擎,其他任何人都可以从内部网网关上上获取一个缓存过的拷贝。
因此对于应用程序资源来说,经过良好定义的 Ajax 应用程序引擎符合 REST 准则,与服务器端 Web 应用程序相比,它具有显著的可伸缩性优势。
用户浏览一个 Ajax Web 站点,加载 Ajax 应用程序引擎,最好是从浏览器缓存中加载的,否则就从本地代理服务器加载。那么对于业务数据来说情况如何呢?由于应用程序逻辑和状态都在浏览器上驻留并执 行,因此应用程序与服务器的交互就与传统 Web 应用程序的方式有很大的不同。不需要获取混合的内容页面,只需要获取业务数据即可。
现在回到 Amazon.com 的例子上来,假设我们点击了一个链接,要查看有关设计模式的一本书籍。在 Amazon.com 目前的应用程序中,链接点击操作会发送很多标识所请求的资源的信息。它还会发送很多会话状态信息,这让服务器可以创建一个新页面,其中可以包括之前的状态 (例如最近查看的内容)、个性化信息(例如您在 1999 年购买的书籍)以及实际的业务资源本身。应用程序是动态且高度个性化的 —— 但是却不能缓存,也无法伸缩(正如 Amazon 所示范的一样,这些架构问题都可以通过投入大量资金构建基础设施来克服)。现在我们考虑一下这个操作在(假想的)Ajax 版本的应用程序中的情况。对于 “最近查看的内容” 并不需要进行处理。当我们点击某个链接时,这些在页面上已经存在的信息并不会消失。有两个请求很可能会与设计模式的书籍有关:
/Books/0201633612
(其中0201633612
是设计模式书的 ISBN 号)/PurchaseHistory/0201633612/bhiggins@us.ibm.com
第一个假定的请求会返回有关书籍的信息(作者、标题、简介等);其中并没有包含特定于用户的数据。特定于用户的数据意味着当更多用户请求相同的资源 时,很可能会从 Internet 上的中间节点上来检索缓存版本,而不是从原始服务器上检索这些资源。这种特性会降低服务器和总体网络负载。另外一方面,第二个请求包含了特定于用户的信息 (Bill Higgins 的购买该书的历史记录)。由于这些数据包括一些个性化信息,因此只有一名用户需要从这个 URI 中获取并缓存数据。尽管这种个性化数据并没有非个性化数据的可伸缩特性,但是重要的问题是这些信息都是直接从 URL 中获取的,因此都具有这样的正面特征:它们都不会妨碍其他可缓存的应用程序和数据资源的缓存。
Ajax 架构风格的另外一个优点是它可以轻松处理服务器的故障。正如我们前面介绍的一样,具有融入式用户体验的服务器端 Web 应用程序通常会在服务器上保存大量的用户会话状态。如果服务器发生了故障,会话状态就丢失了,那么用户就会体验到非常奇怪的浏览器行为(“为什么我又回到 主页上来了?我的购物车中的东西都到哪里去了?”)。在采用有状态客户机和无状态服务的 Ajax 应用程序中,服务器崩溃/重新启动对于用户来说都是完全透明的,因为服务器崩溃不会影响会话状态,这些都保存在用户的浏览器中;无状态服务的行为是幂等 的,可以由用户请求的内容来单独确定。
[转]JUnit4教程
毋庸置疑,程 序员要对自己编写的代码负责,您不仅要保证它能通过编译,正常地运行,而且要满足需求和设计预期的效果。单元测试正是验证代码行为是否满足预期的有效手段 之一。但不可否认,做测试是件很枯燥无趣的事情,而一遍又一遍的测试则更是让人生畏的工作。幸运的是,单元测试工具 JUnit 使这一切变得简单艺术起来。
JUnit 是 Java 社区中知名度最高的单元测试工具。它诞生于 1997 年,由 Erich Gamma 和 Kent Beck 共同开发完成。其中 Erich Gamma 是经典著作《设计模式:可复用面向对象软件的基础》一书的作者之一,并在 Eclipse 中有很大的贡献;Kent Beck 则是一位极限编程(XP)方面的专家和先驱。
麻雀虽小,五脏俱全。JUnit 设计的非常小巧,但是功能却非常强大。Martin Fowler 如此评价 JUnit:在软件开发领域,从来就没有如此少的代码起到了如此重要的作用。它大大简化了开发人员执行单元测试的难度,特别是 JUnit 4 使用 Java 5 中的注解(annotation)使测试变得更加简单。
|
在开始体验 JUnit 4 之前,我们需要以下软件的支持:
- Eclipse:最为流行的 IDE,它全面集成了 JUnit,并从版本 3.2 开始支持 JUnit 4。当然 JUnit 并不依赖于任何 IDE。您可以从 http://www.eclipse.org/ 上下载最新的 Eclipse 版本。
- Ant:基于 Java 的开源构建工具,您可以在 http://ant.apache.org/ 上得到最新的版本和丰富的文档。Eclipse 中已经集成了 Ant,但是在撰写本文时,Eclipse 使用的 Ant 版本较低(必需 1.7 或者以上版本),不能很好的支持 JUnit 4。
- JUnit:它的官方网站是 http://www.junit.org/。您可以从上面获取关于 JUnit 的最新消息。如果您和本文一样在 Eclipse 中使用 JUnit,就不必再下载了。
首先为我们的体验新建一个 Java 工程 ―― coolJUnit。现在需要做的是,打开项目 coolJUnit 的属性页 -> 选择"Java Build Path"子选项 -> 点选"Add Library…"按钮 -> 在弹出的"Add Library"对话框中选择 JUnit(图1),并在下一页中选择版本 4.1 后点击"Finish"按钮。这样便把 JUnit 引入到当前项目库中了。
图1 为项目添加 JUnit 库
|
可 以开始编写单元测试了吗?等等……,您打算把单元测试代码放在什么地方呢?把它和被测试代码混在一起,这显然会照成混乱,因为单元测试代码是不会出现在最 终产品中的。建议您分别为单元测试代码与被测试代码创建单独的目录,并保证测试代码和被测试代码使用相同的包名。这样既保证了代码的分离,同时还保证了查 找的方便。遵照这条原则,我们在项目 coolJUnit 根目录下添加一个新目录 testsrc,并把它加入到项目源代码目录中(加入方式见 图2)。
图2 修改项目源代码目录
现在我们得到了一条 JUnit 的最佳实践:单元测试代码和被测试代码使用一样的包,不同的目录。
一 切准备就绪,一起开始体验如何使用 JUnit 进行单元测试吧。下面的例子来自笔者的开发实践:工具类 WordDealUtil 中的静态方法 wordFormat4DB 是专用于处理 Java 对象名称向数据库表名转换的方法(您可以在代码注释中可以得到更多详细的内容)。下面是第一次编码完成后大致情形:
package com.ai92.cooljunit; |
它是否能按照预期的效果执行呢?尝试为它编写 JUnit 单元测试代码如下:
package com.ai92.cooljunit; |
很普通的一个类嘛!测试类 TestWordDealUtil 之所以使用"Test"开头,完全是为了更好的区分测试类与被测试类。测试方法 wordFormat4DBNormal 调用执行被测试方法 WordDealUtil.wordFormat4DB,以判断运行结果是否达到设计预期的效果。需要注意的是,测试方法 wordFormat4DBNormal 需要按照一定的规范书写:
- 测试方法必须使用注解 org.junit.Test 修饰。
- 测试方法必须使用 public void 修饰,而且不能带有任何参数。
测 试方法中要处理的字符串为"employeeInfo",按照设计目的,处理后的结果应该为"employee_info"。assertEquals 是由 JUnit 提供的一系列判断测试结果是否正确的静态断言方法(位于类 org.junit.Assert 中)之一,我们使用它将执行结果 result 和预期值"employee_info"进行比较,来判断测试是否成功。
看看运行结果如何。在测试类上点击右键,在弹出菜单中选择 Run As JUnit Test。运行结果如下图所示:
图3 JUnit 运行成功界面
绿 色的进度条提示我们,测试运行通过了。但现在就宣布代码通过了单元测试还为时过早。记住:您的单元测试代码不是用来证明您是对的,而是为了证明您没有错。 因此单元测试的范围要全面,比如对边界值、正常值、错误值得测试;对代码可能出现的问题要全面预测,而这也正是需求分析、详细设计环节中要考虑的。显然, 我们的测试才刚刚开始,继续补充一些对特殊情况的测试:
public class TestWordDealUtil { |
再次运行测试。很遗憾,JUnit 运行界面提示我们有两个测试情况未通过测试(图4)――当首字母大写时得到的处理结果与预期的有偏差,造成测试失败(failure);而当测试对 null 的处理结果时,则直接抛出了异常――测试错误(error)。显然,被测试代码中并没有对首字母大写和 null 这两种特殊情况进行处理,修改如下:
//修改后的方法wordFormat4DB |
图4 JUnit 运行失败界面
JUnit 将测试失败的情况分为两种:failure 和 error。Failure 一般由单元测试使用的断言方法判断失败引起,它表示在测试点发现了问题;而 error 则是由代码异常引起,这是测试目的之外的发现,它可能产生于测试代码本身的错误(测试代码也是代码,同样无法保证完全没有缺陷),也可能是被测试代码中的 一个隐藏的bug。
|
啊哈,再次运行测试,绿条又重现眼前。通过对 WordDealUtil.wordFormat4DB 比较全面的单元测试,现在的代码已经比较稳定,可以作为 API 的一部分提供给其它模块使用了。
不知不觉中我们已经使用 JUnit 漂亮的完成了一次单元测试。可以体会到 JUnit 是多么轻量级,多么简单,根本不需要花心思去研究,这就可以把更多的注意力放在更有意义的事情上――编写完整全面的单元测试。
|
当然,JUnit 提供的功能决不仅仅如此简单,在接下来的内容中,我们会看到 JUnit 中很多有用的特性,掌握它们对您灵活的编写单元测试代码非常有帮助。
何 谓 Fixture?它是指在执行一个或者多个测试方法时需要的一系列公共资源或者数据,例如测试环境,测试数据等等。在编写单元测试的过程中,您会发现在大 部分的测试方法在进行真正的测试之前都需要做大量的铺垫――为设计准备 Fixture 而忙碌。这些铺垫过程占据的代码往往比真正测试的代码多得多,而且这个比率随着测试的复杂程度的增加而递增。当多个测试方法都需要做同样的铺垫时,重复代 码的"坏味道"便在测试代码中弥漫开来。这股"坏味道"会弄脏您的代码,还会因为疏忽造成错误,应该使用一些手段来根除它。
JUnit 专门提供了设置公共 Fixture 的方法,同一测试类中的所有测试方法都可以共用它来初始化 Fixture 和注销 Fixture。和编写 JUnit 测试方法一样,公共 Fixture 的设置也很简单,您只需要:
- 使用注解 org,junit.Before 修饰用于初始化 Fixture 的方法。
- 使用注解 org.junit.After 修饰用于注销 Fixture 的方法。
- 保证这两种方法都使用 public void 修饰,而且不能带有任何参数。
遵循上面的三条原则,编写出的代码大体是这个样子:
//初始化Fixture方法 |
这样,在每一个测试方法执行之前,JUnit 会保证 init 方法已经提前初始化测试环境,而当此测试方法执行完毕之后,JUnit 又会调用 destroy 方法注销测试环境。注意是每一个测试方法的执行都会触发对公共 Fixture 的设置,也就是说使用注解 Before 或者 After 修饰的公共 Fixture 设置方法是方法级别的(图5)。这样便可以保证各个独立的测试之间互不干扰,以免其它测试代码修改测试环境或者测试数据影响到其它测试代码的准确性。
图5 方法级别 Fixture 执行示意图
可 是,这种 Fixture 设置方式还是引来了批评,因为它效率低下,特别是在设置 Fixture 非常耗时的情况下(例如设置数据库链接)。而且对于不会发生变化的测试环境或者测试数据来说,是不会影响到测试方法的执行结果的,也就没有必要针对每一个 测试方法重新设置一次 Fixture。因此在 JUnit 4 中引入了类级别的 Fixture 设置方法,编写规范如下:
- 使用注解 org,junit.BeforeClass 修饰用于初始化 Fixture 的方法。
- 使用注解 org.junit.AfterClass 修饰用于注销 Fixture 的方法。
- 保证这两种方法都使用 public static void 修饰,而且不能带有任何参数。
类级别的 Fixture 仅会在测试类中所有测试方法执行之前执行初始化,并在全部测试方法测试完毕之后执行注销方法(图6)。代码范本如下:
//类级别Fixture初始化方法 |
图6 类级别 Fixture 执行示意图
注 解 org.junit.Test 中有两个非常有用的参数:expected 和 timeout。参数 expected 代表测试方法期望抛出指定的异常,如果运行测试并没有抛出这个异常,则 JUnit 会认为这个测试没有通过。这为验证被测试方法在错误的情况下是否会抛出预定的异常提供了便利。举例来说,方法 supportDBChecker 用于检查用户使用的数据库版本是否在系统的支持的范围之内,如果用户使用了不被支持的数据库版本,则会抛出运行时异常 UnsupportedDBVersionException。测试方法 supportDBChecker 在数据库版本不支持时是否会抛出指定异常的单元测试方法大体如下:
@Test(expected=UnsupportedDBVersionException.class) |
注解 org.junit.Test 的另一个参数 timeout,指定被测试方法被允许运行的最长时间应该是多少,如果测试方法运行时间超过了指定的毫秒数,则JUnit认为测试失败。这个参数对于性能 测试有一定的帮助。例如,如果解析一份自定义的 XML 文档花费了多于 1 秒的时间,就需要重新考虑 XML 结构的设计,那单元测试方法可以这样来写:
@Test(timeout=1000) |
JUnit 提供注解 org.junit.Ignore 用于暂时忽略某个测试方法,因为有时候由于测试环境受限,并不能保证每一个测试方法都能正确运行。例如下面的代码便表示由于没有了数据库链接,提示 JUnit 忽略测试方法 unsupportedDBCheck:
@ Ignore("db is down") |
但是一定要小心。注解 org.junit.Ignore 只能用于暂时的忽略测试,如果需要永远忽略这些测试,一定要确认被测试代码不再需要这些测试方法,以免忽略必要的测试点。
又 一个新概念出现了――测试运行器,JUnit 中所有的测试方法都是由它负责执行的。JUnit 为单元测试提供了默认的测试运行器,但 JUnit 并没有限制您必须使用默认的运行器。相反,您不仅可以定制自己的运行器(所有的运行器都继承自 org.junit.runner.Runner),而且还可以为每一个测试类指定使用某个具体的运行器。指定方法也很简单,使用注解 org.junit.runner.RunWith 在测试类上显式的声明要使用的运行器即可:
@RunWith(CustomTestRunner.class) |
显而易见,如果测试类没有显式的声明使用哪一个测试运行 器,JUnit 会启动默认的测试运行器执行测试类(比如上面提及的单元测试代码)。一般情况下,默认测试运行器可以应对绝大多数的单元测试要求;当使用 JUnit 提供的一些高级特性(例如即将介绍的两个特性)或者针对特殊需求定制 JUnit 测试方式时,显式的声明测试运行器就必不可少了。
在 实际项目中,随着项目进度的开展,单元测试类会越来越多,可是直到现在我们还只会一个一个的单独运行测试类,这在实际项目实践中肯定是不可行的。为了解决 这个问题,JUnit 提供了一种批量运行测试类的方法,叫做测试套件。这样,每次需要验证系统功能正确性时,只执行一个或几个测试套件便可以了。测试套件的写法非常简单,您只 需要遵循以下规则:
- 创建一个空类作为测试套件的入口。
- 使用注解 org.junit.runner.RunWith 和 org.junit.runners.Suite.SuiteClasses 修饰这个空类。
- 将 org.junit.runners.Suite 作为参数传入注解 RunWith,以提示 JUnit 为此类使用套件运行器执行。
- 将需要放入此测试套件的测试类组成数组作为注解 SuiteClasses 的参数。
- 保证这个空类使用 public 修饰,而且存在公开的不带有任何参数的构造函数。
package com.ai92.cooljunit; |
上例代码中,我们将前文提到的测试类 TestWordDealUtil 放入了测试套件 RunAllUtilTestsSuite 中,在 Eclipse 中运行测试套件,可以看到测试类 TestWordDealUtil 被调用执行了。测试套件中不仅可以包含基本的测试类,而且可以包含其它的测试套件,这样可以很方便的分层管理不同模块的单元测试代码。但是,您一定要保证 测试套件之间没有循环包含关系,否则无尽的循环就会出现在您的面前……。
回顾一下我们在小节"JUnit 初体验" 中举的实例。为了保证单元测试的严谨性,我们模拟了不同类型的字符串来测试方法的处理能力,为此我们编写大量的单元测试方法。可是这些测试方法都是大同小 异:代码结构都是相同的,不同的仅仅是测试数据和期望值。有没有更好的方法将测试方法中相同的代码结构提取出来,提高代码的重用度,减少复制粘贴代码的烦 恼?在以前的 JUnit 版本上,并没有好的解决方法,而现在您可以使用 JUnit 提供的参数化测试方式应对这个问题。
参数化测试的编写稍微有点麻烦(当然这是相对于 JUnit 中其它特性而言):
- 为准备使用参数化测试的测试类指定特殊的运行器 org.junit.runners.Parameterized。
- 为测试类声明几个变量,分别用于存放期望值和测试所用数据。
- 为测试类声明一个使用注解 org.junit.runners.Parameterized.Parameters 修饰的,返回值为 java.util.Collection 的公共静态方法,并在此方法中初始化所有需要测试的参数对。
- 为测试类声明一个带有参数的公共构造函数,并在其中为第二个环节中声明的几个变量赋值。
- 编写测试方法,使用定义的变量作为参数进行测试。
我们按照这个标准,重新改造一番我们的单元测试代码:
package com.ai92.cooljunit; |
很明显,代码瘦身了。在静态方法 words 中,我们使用二维数组来构建测试所需要的参数列表,其中每个数组中的元素的放置顺序并没有什么要求,只要和构造函数中的顺序保持一致就可以了。现在如果再 增加一种测试情况,只需要在静态方法 words 中添加相应的数组即可,不再需要复制粘贴出一个新的方法出来了。
|
随 着项目的进展,项目的规模在不断的膨胀,为了保证项目的质量,有计划的执行全面的单元测试是非常有必要的。但单靠JUnit提供的测试套件很难胜任这项工 作,因为项目中单元测试类的个数在不停的增加,测试套件却无法动态的识别新加入的单元测试类,需要手动修改测试套件,这是一个很容易遗忘得步骤,稍有疏忽 就会影响全面单元测试的覆盖率。
当然解决的方法有多种多样,其中将 JUnit 与构建利器 Ant 结合使用可以很简单的解决这个问题。Ant ―― 备受赞誉的 Java 构建工具。它凭借出色的易用性、平台无关性以及对项目自动测试和自动部署的支持,成为众多项目构建过程中不可或缺的独立工具,并已经成为事实上的标准。 Ant 内置了对 JUnit 的支持,它提供了两个 Task:junit 和 junitreport,分别用于执行 JUnit 单元测试和生成测试结果报告。使用这两个 Task 编写构建脚本,可以很简单的完成每次全面单元测试的任务。
不过,在使用 Ant 运行 JUnit 之前,您需要稍作一些配置。打开 Eclipse 首选项界面,选择 Ant -> Runtime 首选项(见图7), 将 JUnit 4.1 的 JAR 文件添加到 Classpath Tab 页中的 Global Entries 设置项里。记得检查一下 Ant Home Entries 设置项中的 Ant 版本是否在 1.7.0 之上,如果不是请替换为最新版本的 Ant JAR 文件。
图7 Ant Runtime 首选项
剩下的工作就是要编写 Ant 构建脚本 build.xml。虽然这个过程稍嫌繁琐,但这是一件一劳永逸的事情。现在我们就把前面编写的测试用例都放置到 Ant 构建脚本中执行,为项目 coolJUnit 的构建脚本添加一下内容:
<?xml version="1.0"?> |
Target junit report 是 Ant 构建脚本中的核心内容,其它 target 都是为它的执行提供前期服务。Task junit 会寻找输出目录下所有命名以"Test"开头的 class 文件,并执行它们。紧接着 Task junitreport 会将执行结果生成 HTML 格式的测试报告(图8)放置在"report folder"下。
为整个项目的单元测试类确定一种命名风格。不仅是出于区分类别的考虑,这为 Ant 批量执行单元测试也非常有帮助,比如前面例子中的测试类都已"Test"打头,而测试套件则以"Suite"结尾等等。
图8 junitreport 生成的测试报告
现在执行一次全面的单元测试变得非常简单了,只需要运行一下 Ant 构建脚本,就可以走完所有流程,并能得到一份详尽的测试报告。您可以在 Ant 在线手册 中获得上面提及的每一个 Ant 内置 task 的使用细节。