前言
很久很久没有更新博客了。久到我都不记得上一次更新博客是什么时候,久到我们博客主机都过期了,一度我还想停掉这个博客。好在有simon的坚持才决定博客继续整下去。2013年对我来说是一个比较折腾的一年。一年之类换了两份
工作。找工作的时候才发现理想与现实之间的差距是如此的巨大。期间经历了落差、失望、彷徨……
一、为什么要使用检查点
为什么要使用检查点,那就要说明一下LR如何判断脚本是否执行成功。
LR判断脚本是否执行成功是根据服务器返回的状态来确定的,如果服务器返回的HTTP状态为 200 OK ,那么VuGen 就认为脚本正确地运行了,并且是运行通过的。在绝大多数系统出错时会返回错误页面码? 不会一般系统都会返回一个消息提示框,来提升用户感受。例如,“网站繁忙,请稍后”。其实这个时候网站已经无法正确响应用户请求了,但是VuGen 脚本无法识别,会错误地认为网站还能正确访问,导致分析错误。所以这时需要一种检查点函数帮助验证请求发送出去后,服务器的返回是不是期望的内容,如果不是,那么就说明服务器返回无法提供正常的服务了。
另外,需要特别说明的是,检查点一般跟事务结合起来使用。
二、事务结束的四种状态
前面说到检查点一般跟事务结合使用。这样需要说明一下事务(transaction)。我认为事务是LR中非常非常重要的一个概念。因为完成一个事务所需要的时间是响应时间(Trans Response Time),一秒钟可以完成多少事务是TPS(Trans/Sec)。响应时间和TPS是
性能测试中非常重要的两个指标。可以通过这两个指标来分析系统是否出现瓶颈。
LR中事务结束的时候需要带上一个状态码(Transaction Status)。LR中Transaction Status有四个, 分别为LR_PASS, LR_FAIL, LR_AUTO , LR_STOP。
LR_AUTO:事物的状态被自动设置,如果事务执行成功,状态设置为PASS,如果执行失败,状态设置为FAIL,如果由于异常中断,状态被设置成STOP.
LR_PASS:事务如果执行成功,代码的返回状态就是PASS。
LR_FAIL:事务如果执行失败,代码的返回状态就是FAIL。
一般情况下会选择LR_AUTO,让LR自动判断事务的状态。可是如上面所说LR判断脚本是否执行成功是根据服务器返回的状态来确定的,也就是只要服务器返回的HTTP状态为 200 OK ,事务执行后状态总是被置为LR_PASS。这样不科学的地方在于,LR提示成功的事务你不知道实际上它到底是成功的还是失败的。所以,这就需要检查点了。使用只有事务执行成功才会出现的文本或者图片(PS:虽然图片检查点我从来没用过)来作为事务是否执行成功的标准。
三、如何添加检查点
使用文本检查点(下面简称检查点)需要使用web_reg_find函数。函数的使用详见LR帮助手册,如果不会用可以下面的实战部分。
另外说明一下如果web_reg_find查找中文失败,完全可以查找英文,只要能实现检查点功能就可以了,如果执意要查找英文,请将Record-Options 中勾选support charset中的UTF-8后重新录制。
四、如何选择检查点
选择什么作为检查点,这是困扰了很久了一个问题。就拿登陆来说,并不是所有的系统登陆完成后系统跳转到登陆成功的页面。这里这需要借助运行时查看器(run-time viewer)来帮助我们查找合适的检查点。运行时查看器在选择工具(tools)-常规选项(general Options)-显示(Display) ,选择 打开运行时查看器和自动排列窗口。
运行时查看器的使用我还得感谢我们组成员文武同学。我还老吐槽他老开着这没用的玩意干嘛,我承认我错了。
五、实战
以网站www.huhoo.com(一不小心透露了经纬度)登陆举例,录制登陆脚本后回放,回放的时候打开运行时查看器。可以看到登陆成功后发回这样的信息。
图1 登陆成功后返回信息
然后修改脚本,故意将用户名的密码弄错,重新回放
图2 登陆错误后返回信息
结合两张图可以看出,用”code”:0作为检查点比较合适。因为这样的文本在登陆失败时没有出现。但是如果用“msg”来作为检查点就不合适,因为不管登陆成功和失败都出现了这也的文本,不能作为检查登陆成功的标准。
检查点示例代码如下:
web_reg_find("Text=\"code\":0","SaveCount=Code_Count", LAST ); lr_start_transaction("login"); web_submit_data("login.quick", "Action=http://www.huhoo.com/p/login.quick/", …… "Name=account", "Value=XXXXXX", ENDITEM, "Name=password", "Value=XXXXX", ENDITEM, LAST); if (atoi(lr_eval_string("{Code_Count}")) > 0){ //lr_output_message("Log on successful."); lr_end_transaction("login", LR_PASS); } else{ lr_error_message("Log on failed"); lr_end_transaction("login", LR_FAIL); return -1; } |
完整脚本下载:传送门
说明:
脚本中用户名,密码已经被我用XXX替换,如果想用此脚本做实验,请到虎虎官网www.huhoo.com注册用户,并将用户名写入脚本中。
可能有些读者并不连接什么是Ant以及入可使用它,但只要使用通过
Linux系统得读者,应该知道make这个命令。当编译Linux内核及一些软件的源程序时,经常要用这个命令。Make命令其实就是一个
项目管理工具,而Ant所实现功能与此类似。像make,gnumake和nmake这些编译工具都有一定的缺陷,但是Ant却克服了这些工具的缺陷。最初Ant开发者在开发跨平台的应用时,用样也是基于这些缺陷对Ant做了更好的设计。
Ant 与 makefile
Makefile有一些不足之处,比如很多人都会碰到的烦人的Tab问题。最初的Ant开发者多次强调”只是我在Tab前面加了一个空格,所以我的命令就不能执行”。有一些工具在一定程度上解决了这个问题,但还是有很多其他的问题。Ant则与一般基于命令的工具有所不同,它是
Java类的扩展。Ant运行需要的XML格式的文件不是Shell命令文件。它是由一个Project组成的,而一个Project又可分成可多target,target再细分又分成很多task,每一个task都是通过一个实现特定接口的java类来完成的。
Ant的优点
Ant是Apache软件基金会JAKARTA目录中的一个子项目,它有以下的优点。跨平台性。Ant是存Java语言编写的,所示具有很好的跨平台性。操作简单。Ant是由一个内置任务和可选任务组成的。Ant运行时需要一个XML文件(构建文件)。Ant通过调用target树,就可以执行各种task。每个task实现了特定接口对象。由于Ant构建文件时XML格式的文件,所以和容易维护和书写,而且结构很清晰。Ant可以集成到开发环境中。由于Ant的跨平台性和操作简单的特点,它很容易集成到一些开发环境中去。
Ant 开发Ant的构建文件当开始一个新的项目时,首先应该编写Ant构建文件。构建文件定义了构建过程,并被团队开发中每个人使用。Ant构建文件默认命名为build.xml,也可以取其他的名字。只不过在运行的时候把这个命名当作参数传给Ant。构建文件可以放在任何的位置。一般做法是放在项目顶层目录中,这样可以保持项目的简洁和清晰。下面是一个典型的项目层次结构。
(1) src存放文件。
(2) class存放编译后的文件。
(3) lib存放第三方JAR包。
(4) dist存放打包,发布以后的代码。
Ant构建文件是XML文件。每个构建文件定义一个唯一的项目(Project元素)。每个项目下可以定义很多目标(target元素),这些目标之间可以有依赖关系。当执行这类目标时,需要执行他们所依赖的目标。每个目标中可以定义多个任务,目标中还定义了所要执行的任务序列。Ant在构建目标时必须调用所定义的任务。任务定义了Ant实际执行的命令。Ant中的任务可以为3类。
(1) 核心任务。核心任务是Ant自带的任务。
(2) 可选任务。可选任务实来自第三方的任务,因此需要一个附加的JAR文件。
(3) 用户自定义的任务。用户自定义的任务实用户自己开发的任务。
1.<project>标签
每个构建文件对应一个项目。<project>标签时构建文件的根标签。它可以有多个内在属性,就如代码中所示,其各个属性的含义分别如下。
(1) default表示默认的运行目标,这个属性是必须的。
(2) basedir表示项目的基准目录。
(3) name表示项目名。
(4) description表示项目的描述。
每个构建文件都对应于一个项目,但是大型项目经常包含大量的子项目,每一个子项目都可以有自己的构建文件。
2.<target>标签
一个项目标签下可以有一个或多个target标签。一个target标签可以依赖其他的target标签。例如,有一个target用于编译程序,另一个target用于声称可执行文件。在生成可执行文件之前必须先编译该文件,因策可执行文件的target依赖于编译程序的target。Target的所有属性如下。
(1) name表示标明,这个属性是必须的。
(2) depends表示依赖的目标。
(3) if表示仅当属性设置时才执行。
(4) unless表示当属性没有设置时才执行。
(5) description表示项目的描述。
Ant的depends属性指定了target的执行顺序。Ant会依照depends属性中target出现顺序依次执行每个target。在执行之前,首先需要执行它所依赖的target。程序中的名为run的target的depends属性compile,而名为compile的target的depends属性是prepare,所以这几个target执行的顺序是prepare->compile->run。一个target只能被执行一次,即使有多个target依赖于它。如果没有if或unless属性,target总会被执行。3.<mkdir>标签
该标签用于创建一个目录,它有一个属性dir用来指定所创建的目录名,其代码如下:<mkdir dir=”${class.root}”/>通过以上代码就创建了一个目录,这个目录已经被前面的property标签所指定。
4.<jar>标签
该标签用来生成一个JAR文件,其属性如下。
(1) destfile表示JAR文件名。
(2) basedir表示被归档的文件名。
(3) includes表示别归档的文件模式。
(4) exchudes表示被排除的文件模式。
5.<javac>标签
该标签用于编译一个或一组java文件,其属性如下:
(1).srcdir表示源程序的目录。
(2).destdir表示class文件的输出目录。
(3).include表示被编译的文件的模式。
(4).excludes表示被排除的文件的模式。
(5).classpath表示所使用的类路径。
(6).debug表示包含的调试信息。
(7).optimize表示是否使用优化。
(8).verbose 表示提供详细的输出信息。
(9).fileonerror表示当碰到错误就自动停止。
6.<java>标签
该标签用来执行编译生成的.class文件,其属性如下。
(1).classname 表示将执行的类名。
(2).jar表示包含该类的JAR文件名。
(3).classpath所表示用到的类路径。
(4).fork表示在一个新的虚拟机中运行该类。
(5).failonerror表示当出现错误时自动停止。
(6).output 表示输出文件。
(7).append表示追加或者覆盖默认文件。
7.<delete>标签
该标签用于删除一个文件或一组文件,去属性如下:
(1).file表示要删除的文件。
(2).dir表示要删除的目录。
(3).includeEmptyDirs 表示指定是否要删除空目录,默认值是删除。
(4).failonerror 表示指定当碰到错误是否停止,默认值是自动停止。
(5).verbose表示指定是否列出所删除的文件,默认值为不列出。
8.<copy>标签
该标签用于文件或文件集的拷贝,其属性如下。
(1).file 表示源文件。
(2).tofile 表示目标文件。
(3).todir 表示目标目录。
(4).overwrite 表示指定是否覆盖目标文件,默认值是不覆盖。
(5).includeEmptyDirs 表示制定是否拷贝空目录,默认值为拷贝。
(6).failonerror 表示指定如目标没有发现是否自动停止,默认值是停止。
(7).verbose 表示制定是否显示详细信息,默认值不显示。
Ant的数据类型
在构建文件中为了标识文件或文件组,经常需要使用数据类型。数据类型包含在
org.apache.tool.ant.types包中。下面简单介绍构建文件中一些常用的数据类型。
1. argument 类型
由Ant构建文件调用的程序,可以通过<arg>元素向其传递命令行参数,如apply,exec和java任务均可接受嵌套<arg>元素,可以为各自的过程调用指定参数。以下是<arg>的所有属性。
(1).values 是一个命令参数。如果参数种有空格,但又想将它作为单独一个值,则使用此属性。
(2).file表示一个参数的文件名。在构建文件中,此文件名相对于当前的工作目录。
(3).line表示用空格分隔的多个参数列表。
(4).path表示路径。
2.ervironment 类型
由Ant构建文件调用的外部命令或程序,<env>元素制定了哪些环境变量要传递给正在执行的系统命令,<env>元素可以接受以下属性。
(1).file表示环境变量值得文件名。此文件名要被转换位一个绝对路径。
(2).path表示环境变量的路径。Ant会将它转换为一个本地约定。
(3).value 表示环境变量的一个直接变量。
(4).key 表示环境变量名。
注意 file path 或 value只能取一个。
3.filelist类型,Filelist 是一个支持命名的文件列表的数据类型,包含在一个filelist类型中的文件不一定是存在的文件。以下是其所有的属性。
(1).dir是用于计算绝对文件名的目录。
(2).files 是用逗号分隔的文件名列表。
(3).refid 是对某处定义的一个<filelist>的引用。
注意 dir 和 files 都是必要的,除非指定了refid(这种情况下,dir和files都不允许使用)。
4.fileset类型
Fileset 数据类型定义了一组文件,并通常表示为<fileset>元素。不过,许多ant任务构建成了隐式的fileset,这说明他们支持所有的fileset属性和嵌套元素。以下为fileset 的属性列表。
(1).dir表示fileset 的基目录。
(2).casesensitive的值如果为false,那么匹配文件名时,fileset不是区分大小写的,其默认值为true。
(3).defaultexcludes 用来确定是否使用默认的排除模式,默认为true。
(4).excludes 是用逗号分隔的需要派出的文件模式列表。
(5).excludesfile 表示每行包含一个排除模式的文件的文件名。
(6).includes 是用逗号分隔的,需要包含的文件模式列表。
(7).includesfile 表示每行包括一个包含模式的文件名。
5.patternset 类型
Fileset 是对文件的分组,而patternset是对模式的分组,他们是紧密相关的概念。<patternset>支持4个属性:includes excludex includexfile 和 excludesfile,与fileset相同。Patternset 还允许以下嵌套元素:include,exclude,includefile 和 excludesfile。
6.filterset 类型
Filterset定义了一组过滤器,这些过滤器将在文件移动或复制时完成文件的文本替换。
主要属性如下:
(1).begintoken 表示嵌套过滤器所搜索的记号,这是标识其开始的字符串。
(2).endtoken表示嵌套过滤器所搜索的记号这是标识其结束的字符串。
(3).id是过滤器的唯一标志符。
(4).refid是对构建文件中某处定义一个过滤器的引用。
7.Path类型
Path元素用来表示一个类路径,不过它还可以用于表示其他的路径。在用作揖个属性时,路经中的各项用分号或冒号隔开。在构建的时候,此分隔符将代替当前平台中所有的路径分隔符,其拥有的属性如下。
(1).location 表示一个文件或目录。Ant在内部将此扩展为一个绝对路径。
(2).refid 是对当前构建文件中某处定义的一个path的引用。
(3).path表示一个文件或路径名列表。
8.mapper类型
Mapper类型定义了一组输入文件和一组输出文件间的关系,其属性如下。
(1).classname 表示实现mapper类的类名。当内置mapper不满足要求时,用于创建定制mapper。
(2).classpath表示查找一个定制mapper时所用的类型路径。
(3).classpathref是对某处定义的一个类路径的引用。
(4).from属性的含义取决于所用的mapper。
(5).to属性的含义取决于所用的mapper。
(6).type属性的取值为identity,flatten glob merge regexp 其中之一,它定义了要是用的内置mapper的类型。
Ant 的运行
安装好Ant并且配置好路径之后,在命令行中切换到构建文件的目录,输入Ant命令就可以运行Ant.若没有指定任何参数,Ant会在当前目录下查询build.xml文件。如果找到了就用该文件作为构建文件。如果使用了 –find 选项,Ant 就会在上级目录中找构建文件,直至到达文件系统得跟目录。如果构建文件的名字不是build.xml ,则Ant运行的时候就可以使用 –buildfile file,这里file 指定了要使用的构建文件的名称,示例如下:
Ant如下说明了表示当前目录的构建文件为build.xml 运行 ant 执行默认的目标。
Ant –buildfile test.xml使用当前目录下的test.xml 文件运行Ant ,执行默认的目标
1. 需要使用svnant,从SVN中获取源码
需要使用的扩展包:svnant-1.3.1.zip里所有的jar
下载地址:http://subclipse.tigris.org/files/documents/906/49042/svnant-1.3.1.zip
build.xml中的写法
<!--定义SVN地址--> <property name="svnurl" value="http://xx/svn/projectName"/> <!-- --> <typedef resource="org/tigris/subversion/svnant/svnantlib.xml" /> <!--定义访问SVN的账号和密码--> <svnSetting id="svn.setting" svnkit="true" username="username" password="password" javahl="false" /> <!--检出代码到${project.dir}变量定义的目录下--> <target name="export"> <delete dir="${project.dir}" /> <mkdir dir="${project.dir}" /> <svn refid="svn.setting"> <export srcUrl="${svnurl}" destPath="${project.dir}" force="true"/> </svn> </target> |
2.foreach循环需要使用ant-contrib-1.0b3.jar
build.xml中的写法
<target name="loop" >
<foreach list="src" target="all" param="src_loop" delimiter=","></foreach>
</target>
3. yguard代码混淆
具体介绍参见:http://blog.csdn.net/vrix/article/details/7604636
官网下载地址:http://www.yworks.com/en/downloads.html
build.xml中的写法
<taskdef name="yguard" classname="com.yworks.yguard.YGuardTask"/> <yguard> <inoutpair in="${输入.jar}" out="${输出.jar}"/> <shrink logfile="${build.dir}/shrink.xml"> <keep> <class classes="friendly" methods="private" fields="friendly"/> <field name="serialVersionUID"/> </keep> </shrink> </yguard> |
4.执行脚本操作
启动tomcat在build.xml中的写法
linux <target name="start" > <echo>start tomcat</echo> <exec executable = "${tomcat.dir}/bin/startup.sh" /> </target> windows <target name="stop" > <echo>stop tomcat</echo> <exec executable="cmd" dir="${tomcat.dir}/bin" failonerror="false" append="true"> <env key="CATALINA_HOME" path="${tomcat.dir}"/> <arg value="/c startup.bat" /> </exec> </target> |
讲解了钩子程序的攻防实战,并实现了一套对框架页的监控方案,将防护作用到所有子页面。
到目前为止,我们防护的深度已经差不多,但广度还有所欠缺。
例如,我们的属性钩子只考虑了 setAttribute,却忽视还有类似的 setAttributeNode。尽管从来不用这方法,但并不意味人家不能使用。
例如,创建元素通常都是 createElement,事实上 createElementNS 同样也可以。甚至还可以利用现成的元素 cloneNode,也能达到目的。因此,这些都是边缘方法都是值得考虑的。
下面我们对之前讨论过的监控点,进行逐一审核。
内联事件执行 eval
在第一篇
文章结尾谈到,在执行回调的时候,最好能监控 eval,setTimeout('...') 这些能够解析代码的函数,以防止执行储存在其他地方的
XSS 代码。
先来列举下这类函数:
eval
setTimeout(String) / setInterval(String)
Function
execScript / setImmediate(String)
事实上,利用上一篇的钩子技术,完全可以把它们都监控起来。但现实并没有我们想象的那样简单。
eval 重写有问题吗
eval 不就是个函数,为什么不可以重写?
var raw_fn = window.eval;
window.eval = function(exp) {
alert('执行eval: ' + exp);
return raw_fn.apply(this, arguments);
};
console.log(eval('1+1'));
完全没问题啊。那是因为代码太简单了,下面这个 Demo 就可以看出山寨版 eval 的缺陷:
(function() {
eval('var a=1');
})();
alert(typeof a);
Run
按理说应该 undefined 才对,结果却是 number。局部变量都跑到全局上来了。这是什么情况?事实上,eval 并不是真正意义的函数,而是一个关键字!想了解详情请戳这里。
Function 重写有意义吗
Function 是一个全局变量,重写 window.Function 理论上完全可行吧。
var raw_fn = window.Function;
window.Function = function() {
alert('调用Function');
return raw_fn.apply(this, arguments);
};
var add = Function('a', 'b', 'return a+b');
console.log( add(1, 2) );
重写确实可行。但现实却是不堪一击的:因为所有函数都是 Function 类的实例,所以访问任何一个函数的 constructor 即可得到原始的 Function。
例如 alert.constructor,就可以绕过我们的钩子。甚至可以用匿名函数:
(function(){}).constructor
所以,Function 是永远钩不住的。
额外的执行方法
就算不用这类函数,仍有相当多的办法执行字符串,例如:
创建脚本,innerHTML = 代码
创建脚本,路径 = data:代码
创建框架,路径 = javascript:代码
......
看来,想完全把类似 eval 的行为监控起来,是不现实的。不过作为预警,我们只监控 eval,setTimeout/Interval 也就足够了。
可疑模块拦截
第二篇谈了站外模块的拦截。之所以称之『模块』而不是『脚本』,并非只有脚本元素才具备执行能力。框架页、插件都是可以运行代码的。
可执行元素
我们列举下,能执行远程模块的元素:
脚本
<script src="..." />
框架
<iframe src="...">
<frame src="...">
插件(Flash)
<embed src="...">
<object data="...">
<object><param name="moive|src" value="..."></object>
插件(其他)
<applet codebase="...">
这些元素的路径属性,都应该作为排查的对象。
不过,有这么个元素的存在,可能导致我们的路径检测失效,它就是:
<base href="...">
它能重定义页面的相对路径,显然是不容忽视的。
事实上,除了使用元素来执行站外模块,还可以使用网络通信,获得站外的脚本代码,然后再调用 eval 执行:
AJAX
目前主流浏览器都支持跨域请求,只要服务端允许就可以。因此,我们需监控 XMLHttpRequest::open 方法。如果请求的是站外地址,就得做策略匹配。不通过则放弃向上调用,或者抛出一个异常,或者给 XHR 产生一个 400 状态。
WebSocket
WebSocket 和 XHR 类似,也能通过钩子的方法进行监控。
不过,值得注意的是,WebSocket 并非是个函数,而是一个类。因此,在返回实例的时候,别忘了将 constructor 改成自己的钩子,否则就会泄露原始接口:
var raw_class = window.WebSocket; window.WebSocket = function WebSocket(url, arg) { alert('WebSocket 请求:' + url); var ins = new raw_class(url, arg); // 切记 ins.constructor = WebSocket; return ins; }; var ws = new WebSocket('ws://127.0.0.1:1000'); |
另外,因为它是一个类,所以不要忽略了静态方法或属性:
WebSocket.CONNECTING
WebSocket.OPEN
...
因此,还需将它们拷贝到钩子上。
框架页消息
HTML5 赋予了框架页跨域通信的能力。如果没有为框架元素建立白名单的话,攻击者可以嵌入自己的框架页面,然后将 XSS 代码 postMessage 给主页面,通过 eval 执行。
不过为了安全考虑,HTML5 在消息事件里保存了来源地址,以识别消息是哪个页面发出的。
因为是个事件,我们可以使用第一篇文章里提到的方法,对其进行捕获。每当有消息收到时,可以根据策略,决定是否阻止该事件的传递。
// 我们的防御系统 (function() { window.addEventListener('message', function(e) { if (confirm('发现来自[' + e.origin + ']的消息:\n\n' + e.data + '\n\n是否拦截?')) { e.stopImmediatePropagation(); } }, true); })(); window.addEventListener('message', function(e) { alert('收到:' + e.data) }) postMessage('hello', '*'); Run |
提问:如何对所发现的缺陷进行严密的等级划分?
功能问题(FunctionError):对产品、项目质量有影响,但尚难以确定是否是错误,暂时无法解决
功能缺陷(FunctionDefect):不满足用户需求等bug的总称
页面缺陷(UIDefect):页面美观性、协调性、错别字等
建议类(Suggestion):对产品、项目的建议性意见,不强制要求修改
硬件性能:进行
性能测试时使用,暂定:网络延时、内存问题、CPU占用、硬盘问题
安全性问题:进行系统
安全测试时使用,暂不订具体标准
业务流程问题:进行业务流程测试时进行
模块间接口问题:涉及有模块间数据传递时使用
其他(Other):其它
根据各类缺陷的严重程度将缺陷分为5个等级,具体如下:
1)低(Low)
建议类错误,对软件的改进意见或者建议。如:
a)功能建议
b)操作建议
c)校验建议
d)说明建议
e)UI建议
2)中(Medium)
使操作者不合理或者不方便或操作遇到麻烦,但它不影响执行
工作功能或重要功能,次要功能,对产品使用影响不大。如:
界面错误
a)使操作者不方便或者遇到麻烦,但不影响执行工作功能的实现
b)界面、控件的摆布、图标、输入输出不规范
提示类错误
a)删除操作未给出提示
b)长时间操作未给出提示
c)提示窗口文字未采用行业术语
d)出错没有提示
其他错误
a)不符合编码标准
b)辅助说明描述不清楚、不规范
c)快捷键无效,快捷键错误操作
d)打印内容、格式错误
3)高(High)
影响系统正常运行的缺陷,主要功能出现错误,影响到产品的使用。如:
数据库缺陷:数据库设计未达到第三范式的要求或需求规格说明的格式水平
操作错误:因错误操作迫使程序中断
功能错误:
a)程序功能无法实现
b)程序功能实现错误
其他错误:
a)脚本错误
b)软件产品的编译,打包,安装,卸载错误
4)非常高(Very High)
规定的功能没有实现或不完整或产生错误结果;设计不合理造成性能低下,影响系统的运营;使系统不稳定、或破坏数据;而且是常规操作中经常发生或非常规操作中不可避免的主要问题,且没有办法更正(重新安装或重新启动软件不属更正办法),须尽快修正,如:
数据缺陷
a)数据计算错误
b)数据约束错误
c)数据输入、输出错误
数据库缺陷
a)数据库发生死锁
b)数据库的表、业务规则、缺省值未加完整性等约束条件
c)数据库连接错误
d)数据通讯错误
接口缺陷
a)程序接口错误
b)硬件接口、通讯错误
功能错误:
a)程序功能无法实现
b)程序功能实现错误
5)紧急(Critical)
不能执行正常工作或重要功能,使系统崩溃或资源严重不足,数据丢失(金币,包子)非常死机等导致系统不能继续运行须马上修正,如:
a)由于程序所引起的死机,非法退出
b)程序死循环
c)性能与需求不一致(压力测试)
d)存在安全性与保密性问题
e)文件打开与保存错误
总结:
1级-建议问题的软件缺陷(Enhancemental):由问题提出人对测试对象的改进意见或测试人员提出的建议、质疑。
2级—较小错误的软件缺陷(Minor),使操作者不方便或遇到麻烦,但它不影响功能过的操作和执行,如错别字、界面不规范(字体大小不统一,文字排列不整齐,可输入区域和只读区域没有明显的区分标志),辅助说明描述不清楚。
3级—一般错误的软件缺陷(major):次要功能没有完全实现但不影响使用。如提示信息不太准确,或用户界面差,操作时间长,模块功能部分失效等,打印内容、格式错误,删除操作未给出提示,数据库表中有过多的空字段等。
4级—严重错误的软件缺陷(critical):系统的主要功能部分丧失、数据不能保存,系统的次要功能完全丧失。问题局限在本模块,导致模块功能失效或异常退出。如致命的错误声明,程序接口错误,数据库的表、业务规则、缺省值未加完整性等约束条件
5级—致命的软件缺陷(Fatal):造成系统或应用程序崩溃、死机、系统挂起,或造成数据丢失,主要功能完全丧失,导致本模块以及相关模块异常等问题。如代码错误,死循环,数据库发生死锁、与数据库连接错误或数据通讯错误,未考虑异常操作,功能错误等。
原帖地址:http://bbs.51testing.com/thread-1000328-1-1.html
反射机制简介
反射机制应用示例
简单的Ioc实现
代理模式
Java动态代理
简单的Aop实现
“程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言”。从这个观点看,Perl,
Python,
Ruby是动态语言,C++,Java,C#不是动态语言。
尽管在这样得定义与分类下Java不是动态语言,它却有着一个非常突出的动态相关的机制:反射机制 (Reflection)。
什么是反射?
反射的概念是由Smith在1982年首次提出的,主要是指程序可以访问、检测和修改它本身状态或者行为的一种能力。
JAVA反射机制都是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。
JAVA反射机制(Reflection)
动态获取类的信息,以及动态调用对象的方法的功能。
主要提供了以下功能:
在运行时判断任意一个对象所属的类;
在运行时构造任意一个类的对象;
在运行时判断任意一个类所具有的成员变量和方法;
在运行时调用任意一个对象的方法;
生成动态代理。
JAVA反射机制包
在 JDK 中,主要由以下类来实现 Java 反射机制,这些类都位于 java.lang.reflect包中。
Class 类:代表一个类。
Field 类:代表类的成员变量(成员变量也称为类的属性)。
Method 类:代表类的方法。
Constructor 类:代表类的构造方法。
Array 类:提供了动态创建数组,以及访问数组元素的静态方法。
java.lang.Class
static Class forName(String className)
返回描述类名为className的Class对象
Object newInstance()
返回一个类的一个新实例
Field[] getFields()
返回包含Field对象的数组,这些对象记录了这个类或者其超类的公共域
Field[] getDeclaredField()
返回包含Field对象的数组,这些对象记录了这个类的全部域
Method[] getMethods() 返回这个类或者其超类所有的公有方法
Method[] getDeclareMethods() 返回这个类或者接口的所有方法,不包括超类继承的方法
Constructor[] getConstructors() 返回所有包含了Class对象所描述类的公有构造器
Constructor[] getDeclaredConstructors() 返回包含了Class对象所描述的类的所有构造器
Java.lang.reflect.Constructor
Class[] getParameterTypes()
返回一个用于描述参数类型的Class对象数组
getReturnType()
返回一个用于描述返回类型的Class对象
int getModifiers()
返回一个用于描述方法抛出的异常类型的Class对象数组
Class getDeclaringClass()
返回一个用于描述类中定义的构造器、方法或域的Class对象
通过反射实例化参数
平常实例化对象通常使用new关键字。但是使用new关键字实例化的对象具有强耦合性。New对象无法胜任未知对象的实例化,这时候只能通过反射动态生成。例如Spring的DI。
实例化无参构造函数的对象
Class.newInstance()
Class.getConstructor(new Class[]{}).newInstance(new Object[]{})
实例化带参构造函数的对象
Clazz.getConstructor(Class<?> ...ParameterType).newInstance(Object ...initargs)
反射机制举例
import java.lang.reflect.*; public class DumpMethods { public static void main(String args[]) throws Exception{ //加载并初始化命令行参数指定的类 Class classType = Class.forName(args[0]); //获得类的所有方法 Method methods[] = classType.getDeclaredMethods(); for(int i = 0; i < methods.length; i++) System.out.println(methods[i].toString()); } } |
输入:java DumpMethods java.util.Stack public synchronized java.lang.Object java.util.Stack.pop() public java.lang.Object java.util.Stack.push(java.lang.Object) public boolean java.util.Stack.empty() public synchronized java.lang.Object java.util.Stack.peek() public synchronized int java.util.Stack.search(java.lang.Object) |
运用反射机制调用方法
getMethod和invoke方法的时序图
获取反射对象
import java.lang.reflect.*; public class ReflectTester { public Object copy(Object object) throws Exception{ //获得对象的类型 Class classType=object.getClass(); System.out.println("Class:"+classType.getName()); //通过默认构造方法创建一个新的对象 Object objectCopy=classType.getConstructor(new Class[]{}). newInstance(new Object[]{}); //获得对象的所有属性 Field fields[]=classType.getDeclaredFields(); for(int i=0; i<fields.length;i++){ Field field=fields[i]; String fieldName=field.getName(); String firstLetter=fieldName.substring(0,1).toUpperCase(); //获得和属性对应的 getXXX()方法的名字 String getMethodName="get"+firstLetter+fieldName.substring(1); //获得和属性对应的 setXXX()方法的名字 String setMethodName="set"+firstLetter+fieldName.substring(1); //获得和属性对应的 getXXX()方法 Method getMethod=classType.getMethod(getMethodName,new Class[]{}); //获得和属性对应的 setXXX()方法 Method setMethod=classType.getMethod(setMethodName,new Class[]{field.getType()}); //调用原对象的 getXXX()方法 Object value=getMethod.invoke(object,new Object[]{}); System.out.println(fieldName+":"+value); //调用复制对象的 setXXX()方法 setMethod.invoke(objectCopy,new Object[]{value}); } return objectCopy; } public static void main(String[] args) throws Exception{ Customer customer=new Customer("Tom",21); customer.setId(new Long(1)); Customer customerCopy=(Customer)new ReflectTester().copy(customer); System.out.println("Copy information:"+customerCopy.getName()+ “ "+ customerCopy.getAge()); } } |
运用反射机制调用方法
class Customer{ //Customer 类是一个 JavaBean private Long id; private String name; private int age; public Customer(){} public Customer(String name,int age){ this.name=name; this.age=age; } public Long getId(){return id;} public void setId(Long id){this.id=id;} public String getName(){return name;} public void setName(String name){this.name=name;} public int getAge(){return age;} public void setAge(int age){this.age=age;} } |
import java.lang.reflect.*; public class InvokeTester { public int add(int param1,int param2){ return param1+param2; } public String echo(String msg){ return "echo:"+msg; } public static void main(String[] args) throws Exception{ Class classType=InvokeTester.class; Object invokeTester=classType.newInstance(); //调用 InvokeTester 对象的 add()方法 Method addMethod=classType.getMethod("add",new Class[]{int.class,int.class}); Object result=addMethod.invoke(invokeTester, new Object[]{new Integer(100),new Integer(200)}); System.out.println((Integer)result); //调用 InvokeTester 对象的 echo()方法 Method echoMethod=classType.getMethod("echo",new Class[]{String.class}); result=echoMethod.invoke(invokeTester,new Object[]{"Hello"}); System.out.println((String)result); } } |
本文作者Alex已经从事Java开发15年了,最近帮助开发了COBOL和Magik语言的JVM 。当前,他正致力于Micro Focus的Java性能测试工具。在本文中,他阐述了在标准硬件中实现高速、多线程虚拟内存的可能性及方案。原文内容如下。
你想在标准硬件上运行TB级甚至PB级内存的JVM吗?你想与内存交互一样读写文件,且无需关心文件的打开、关闭、读、写吗?
JVM的64位地址空间使这些成为可能。
首先,不要在观念上将内存和磁盘进行区分,而是统一处理为内存映射文件。在32位地址空间时,内存映射文件只是为了高速访问磁盘;因为受限于虚拟机的有限地址空间,并不支持大规模的虚拟内存或大文件。如今JVM已经发展为64位,而且可以在64位
操作系统上运行。在一个进程的地址空间中,内存映射文件大小就可以达到TB甚至PB。
进程无需关心内存是在RAM或是磁盘上。操作系统会负责处理,而且处理得非常高效。
我们可以使用Java的MappedByteBuffer类访问内存映射文件。该类的实例对象与普通的ByteBuffer一样,但包含的内存是虚拟的——可能是在磁盘上,也可能是在RAM中。但无论哪种方式,都是由操作系统负责处理。因为的ByteBuffer的大小上限是Intger.MAX_VALUE,我们需要若干个ByteBuffer来映射大量内存。在这个示例中,我映射了40GB。
这是因为我的Mac只有16GB内存,下面代码证明了这一点!
public MapperCore(String prefix, long size) throws IOException { coreFile = new File(prefix + getUniqueId() + ".mem"); // This is a for testing - to avoid the disk filling up coreFile.deleteOnExit(); // Now create the actual file coreFileAccessor = new RandomAccessFile(coreFile, "rw"); FileChannel channelMapper = coreFileAccessor.getChannel(); long nChunks = size / TWOGIG; if (nChunks > Integer.MAX_VALUE) throw new ArithmeticException("Requested File Size Too Large"); length = size; long countDown = size; long from = 0; while (countDown > 0) { long len = Math.min(TWOGIG, countDown); ByteBuffer chunk = channelMapper.map(MapMode.READ_WRITE, from, len); chunks.add(chunk); from += len; countDown -= len; } } |
上面的代码在虚拟内存创建了40GB MappedByteBuffer对象列表。读取和写入时只需要注意处理两个内存模块的跨越访问。完整代码的可以在这里找到。
线程
一个极其强大且简单易用的方法就是线程。但是普通的Java IO简直就是线程的噩梦。两个线程无法在不引起冲突的情况下同时访问相同的数据流或RandomAccessFile 。虽然可以使用非阻塞IO,但是这样做会增加代码的复杂性并对原有的代码造成侵入。
与其他的内存线程一样,内存映射文件也是由操作系统来处理。可以根据读写需要,在同一时刻尽可能多的使用线程。我的测试代码有128个线程,而且工作得很好(虽然机器发热比较大)。唯一重要的技巧是复用MappedByteBuffer对象,避免自身位置状态引发问题。
现在可以执行下面的测试:
@Test public void readWriteCycleThreaded() throws IOException { final MapperCore mapper = new MapperCore("/tmp/MemoryMap", BIG_SIZE); final AtomicInteger fails = new AtomicInteger(); final AtomicInteger done = new AtomicInteger(); Runnable r = new Runnable() { public void run() { try { // Set to 0 for sequential test long off = (long) ((BIG_SIZE - 1024) * Math.random()); System.out.println("Running new thread"); byte[] bOut = new byte[1024]; double counts = 10000000; for (long i = 0; i < counts; ++i) { ByteBuffer buf = ByteBuffer.wrap(bOut); long pos = (long) (((BIG_SIZE - 1024) * (i / counts)) + off) % (BIG_SIZE - 1024); // Align with 8 byte boundary pos = pos / 8; pos = pos * 8; for (int j = 0; j < 128; ++j) { buf.putLong(pos + j * 8); } mapper.put(pos, bOut); byte[] bIn = mapper.get(pos, 1024); buf = ByteBuffer.wrap(bIn); for (int j = 0; j < 128; ++j) { long val = buf.getLong(); if (val != pos + j * 8) { throw new RuntimeException("Error at " + (pos + j * 8) + " was " + val); } } } System.out.println("Thread Complete"); } catch (Throwable e) { e.printStackTrace(); fails.incrementAndGet(); } finally { done.incrementAndGet(); } } }; int nThreads = 128; for (int i = 0; i < nThreads; ++i) { new Thread(r).start(); } while (done.intValue() != nThreads) { try { Thread.sleep(1000); } catch (InterruptedException e) { // ignore } } if (fails.intValue() != 0) { throw new RuntimeException("It failed " + fails.intValue()); } } |
我曾尝试进行其他形式的IO,但是只要像上面那样运行128个线程,性能都不如上面的方法。我在四核、超线程I7 Retina MacBook Pro上尝试过。代码运行时会启动128个线程,超出CPU的最大负载(800%),直到操作系统检测到该进程的内存不足。在这个时候,系统开始对内存映射文件的读写进行分页。为实现这一目标,内核会占用一定的CPU,Java进程的性能会下降到650~750%。Java无需关心读取、写入、同步或类似的东西——操作系统会负责处理。
结果会有所不同
如果读取和写入点不是连续而是随机的,性能下降有所区别(带有交换时会达到750%,否则会达到250%)。我相信这种方式可能更适合处理少量的大数据对象,而不适用于大量的小数据对象。对于后者,可能的处理办法是预先将大量小数据对象加载到缓存中,再将其映射到虚拟内存。
应用程序
到目前为止,我使用的技术都是虚拟内存系统。在示例中,一旦与虚拟内存交互完成,就会删除底层文件。但是,这种方法可以很容易地进行数据持久化。
例如,视频编辑是一个非常具有挑战性的工程问题。一般来说,有两个有效的方法:无损耗存储整个视频,并编辑存储的信息;或根据需要重新生成视频。因为RAM的制约,后一种方法越来越普遍。然而,视频是线性的——这是一种理想的数据类型,可用来存储非常大的映射虚拟内存。由于在视频算法上取得的进步,可以将它作为原始字节数组访问。操作系统会根据需要将磁盘到虚拟内存的缓冲区进行分页处理。
另一个同样有效的应用场景是替代文档服务中过度设计的RAM缓存解决方案。想想看,我们有一个几TB的中等规模的文档库。它可能包含图片、短片和PDF文件。有一种常见的快速访问磁盘的方法,使用文件的RAM缓存弱引用或软引用。但是,这会对JVM垃圾收集器产生重大影响,并且增加操作难度。如果将整个文档映射到虚拟内存,可以更加简单地完成同样的工作。操作系统会根据需要将数据读入内存。更重要的是,操作系统将尽量保持RAM中最近被访问的内存页。这意味着内存映射文件就像RAM缓存一样,不会对Java或JVM垃圾收集器产生任何影响。
最后,内存映射文件在科学计算和建模等应用中非常有效。在用来处理代表真实世界系统的计算模型时,经常需要大量的数据才能正常工作。在我的音频处理系统Sonic Field中,通过混合和处理单一声波,可以模拟真实世界中的音频效果。例如,创建原始音频副本是为模拟从硬表面反射的声波,并将反射回来的声波与原声波混合。这种方法需要大量的存储空间,这时就可以把音频信号放在虚拟内存中(也是这项工作的最初动机)。
读取一致性
· 强一致性
在任何时间访问集群中任一结点,得到的数据结果一致;
· 用户一致性
对同一用户,访问集群期间得到的数据一致;
解决用户一致性:使用粘性会话,将会话绑定到特定结点来处理;
这样会降低负载均衡器的性能;
· 最终一致性
集群中各结点间由于数据同步不及时造成暂时的数据不一致,但数据同步完成后,最终具有一致性;
更新一致性
· 悲观方式
使用写锁
大幅降低系统响应能力
可能导致死锁
· 乐观方式
先让冲突发生,再检测顺序
自动合并的处理方式极具“领域特定”问题
放宽“一致性约束”
· CAP定理
一致性(Consistency)、可用性(Availability)和分区耐受性(Partition tolerance),3个属性只可能同时满足2个;
分区耐受性的解释:集群因通信故障而划分为多个时仍然可用
· CA系统
单服务器
集群中出现”分区“,就不可用
· PA/PC
集群出现”分区“时,需要在”一致性“ 和“可用性”间权衡
一般会牺牲部分一致性(eg:使用最终一致性),保证可用性
放宽“持久性”约束
更严格的持久性,意味着更多的性能损失;
· 牺牲“持久性”换取更好的性能
· 复制“持久性”故障
主节点故障,未同步到从节点的数据丢失
主节点恢复,故障期间更新的数据冲突
解决方案:针对单个请求指定其所需的持久性
附思维导图
下面介绍一些开发者在
数据库操作中要注意的
SQL编码准则。虽然本文不能覆盖所有的准则,但还是希望能给开发者带来些许帮助。下面就来看看在编码实践中哪些应该做,哪些不应该做。
1. 在长时间运行的查询和短查询中使用事务
如果预期有一个长时间运行的查询,并且有大量的数据输出时,开发者就应该在BEGIN TRAN 和END TRAN之间使用事务。
这样事务会在缓冲区缓存为独立事务,并会被分配特定内存,以此来提高处理速度。
2. 不要使用SELECT *
如果使用SELECT * 来选择表中的所有记录,那么一些不必要的记录也被读取、缓存,增加了磁盘的I/O和内存消耗。
3. 避免在WHERE子句中使用显式或隐式函数,比如Convert ()
4. 避免在触发器中执行长时间的操作
5. 适当使用临时表和表变量
当结果集较小的时候,请尽量使用表变量;当结果集相当大时,使用临时表。
6. 使用连接(JOIN)代替子查询(Sub-Queries)
子查询通常作为内联代码来使用,而连接(JOIN)则作为表来使用,这样速度会更快。所以,应尽量避免在连接中使用子查询。
7. 连接条件中表的顺序
在连接条件中,应尽量首先使用较小的表,然后逐步使用较大的表。
8. 循环优化
如果操作在循环内部没有任何影响,那么应尽量将操作放到循环外面,这样可以减少不必要的重复
工作。因为,SQL Server优化器不会自动识别这种低效率的代码,更不会自动优化(其他一些语言的编译器可以)。
9. 参数探测
不要在正执行的SP(存储过程)中使用SP参数,这样会导致参数探测(Parameter Sniffing)。应该在声明和设置后再使用SP参数。由于这个原因,SP的行为在每次运行期间都不相同。
10. 当使用条件语句时,可以使用Index(索引)Hint(提示)
比如在SQL Server 2008中,可以使用Index hint,也可以使用fixed plan hint强制在查询中使用hint,以提高运行速度。
11. 在声明中明确指定存储过程中数据类型的大小
开发者随机声明数据类型的大小是不可取的,如:Varchar (500)。这在执行时会在缓冲区中增加不必要的预留空间。
12. 在查询中有效利用MAXDOP(最大并行度)设置
询问数据库管理员关于四核CPU可用性的设置,包括内存的设置,然后适当使用hint,可以有效改善查询速度。
13. SQL Server 2008中的GROUPING SETS
如果数据库服务器为SQL Server 2008,那么可以在所有的Unions中使用Grouping Set来代替Group By。这样在Union中重新进行group by排序时,优化器不会每次都制定一个计划。
14. 当发生死锁时,总是使用With (nolock) 和With (rowlock)