什么是边界情况呢?比方_?null g入一个ƈ未编写如何处?null 值的Ҏ(gu)中,q就是一U边界情c大多数开发h员通常都不能成功测试这L场景Q因没多大意义。但不管有没有意义,发生了这L情况Q就会抛Z?NullPointerExceptionQ然后整个程序就会崩溃?/p>
本月Q我ؓ(zhn)推荐一U多层面的方法,来处理代码中那些不易预料的缺陗尝试ؓ应用E序整合q防御性编E、契U式设计和一U叫?OVal 的易用的通用验证框架?/p>
清单 1 中的代码为给定的 Class 对象Q省M java.lang.ObjectQ因为所有对象都最l由它扩展)构徏一个类层次。但如果仔细看的话,(zhn)会注意C个有待发现的潜在~陷Q即该方法对对象值所做的假设?/p>
清单 1. 不检?null 的方?/a>
public static Hierarchy buildHierarchy(Class clzz){ Hierarchy hier = new Hierarchy(); hier.setBaseClass(clzz); Class superclass = clzz.getSuperclass(); if(superclass != null && superclass.getName().equals("java.lang.Object")){ return hier; }else{ while((clzz.getSuperclass() != null) && (!clzz.getSuperclass().getName().equals("java.lang.Object"))){ clzz = clzz.getSuperclass(); hier.addClass(clzz); } return hier; } } |
刚编好这个方法,我还没注意到q个~陷Q但׃我狂热地崇拜开发h员测试,于是我编写了一个?TestNG 的常规测试。而且Q我q利用了 TestNG 方便?DataProvider
Ҏ(gu),借助该特性,我创Z一个通用的测试用例ƈ通过另一个方法来改变它的参数。运行清?2 中定义的试用例会生两个通过l果Q一切都q{良好Q不是吗Q?/p>
清单 2. 验证两个值的 TestNG 试
import java.util.Vector; import static org.testng.Assert.assertEquals; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; public class BuildHierarchyTest { @DataProvider(name = "class-hierarchies") public Object[][] dataValues(){ return new Object[][]{ {Vector.class, new String[] {"java.util.AbstractList", "java.util.AbstractCollection"}}, {String.class, new String[] {}} }; } @Test(dataProvider = "class-hierarchies"}) public void verifyHierarchies(Class clzz, String[] names) throws Exception{ Hierarchy hier = HierarchyBuilder.buildHierarchy(clzz); assertEquals(hier.getHierarchyClassNames(), names, "values were not equal"); } } |
xQ我q是没有发现~陷Q但一些代码问题却困扰着我。如果有Zl意Cؓ Class 参数传入一?null g怎么样呢Q?a >清单 1 中第 4 行的 clzz.getSuperclass() 调用会抛Z?NullPointerExceptionQ是q样吗?
试我的理论很容易;甚至都不用从头开始。仅仅把 {null, null} d到初?BuildHierarchyTest ?dataValues Ҏ(gu)中的多维 Object 数组中,然后再次q行它。我定会得到如图 1 所C的 NullPointerExceptionQ?/p>
?1. 可怕的 NullPointerException
参见q里?全图?/p>
![]() |
|
一旦出现这个问题,下一步就是要拿出Ҏ(gu)的策略。问题是我控制不了这个方法能否接收这U输入。对于这c问题,开发h员通常会用防御性编E技术,该技术专门用来在发生摧毁性后果前捕捉潜在错误?/p>
对象验证是处理不定性的一经典的防M性编E策略。相应地Q我会添加一Ҏ(gu)验来验证 clzz
是否?null
Q如清单 3 所C。如果其值最lؓ null
Q我׃抛出一?RuntimeException
来警告他人注意这个潜在问题?/p>
清单 3. d验证 null 值的?
public static Hierarchy buildHierarchy(Class clzz){ if(clzz == null){ throw new RuntimeException("Class parameter can not be null"); } Hierarchy hier = new Hierarchy(); hier.setBaseClass(clzz); Class superclass = clzz.getSuperclass(); if(superclass != null && superclass.getName().equals("java.lang.Object")){ return hier; }else{ while((clzz.getSuperclass() != null) && (!clzz.getSuperclass().getName().equals("java.lang.Object"))){ clzz = clzz.getSuperclass(); hier.addClass(clzz); } return hier; } } |
很自Ӟ我也会编写一个快速测试用例来验证我的验是否真能避?NullPointerException
Q如清单 4 所C:
@Test(expectedExceptions={RuntimeException.class}) public void verifyHierarchyNull() throws Exception{ Class clzz = null; HierarchyBuilder.buildHierarchy(null); } |
在本例中Q防御性编E似乎解决了问题。但仅依靠这策略会存在一些缺陗?
![]() |
|
管防M性编E有效地保证了方法的输入条gQ但如果在一pdҎ(gu)中用它Q不免过于重复。熟(zhn)面向方面编E(?AOPQ的Z会把它认为是横切x点,q意味着防M性编E技术横跨了代码库。许多不同的对象都采用这些语法,管从纯面向对象的观Ҏ(gu)看这些语法跟对象毫不相关?/p>
而且Q横切关注点开始渗入到契约式设计(DBCQ的概念中。DBC 是这样一Ҏ(gu)术,它通过在组件的接口昑ּ地陈q每个组件应有的功能和客h的期望值来保pȝ中所有的lg完成它们应尽的职责。从 DBC 的角度讲Q组件应有的功能被认为是后置条gQ本质上是lg的责任,而客h的期望值则普遍被认为是前置条g。另外,在纯 DBC 术语中,遵@ DBC 规则的类针对其将l护的内部一致性与外部世界有一个契U,即h所q的类不变式?/p>
![]() ![]() |
我在以前的一关于用 Nice ~程的文章中介绍q?DBC 的概念,Nice 是一门与 JRE 兼容的面向对象编E语aQ它的特Ҏ(gu)侧重于模块性、可表达性和安全性。有的是,Nice q入了功能性开发技术,其中包括了一些在面向斚w~程中的技术。功能性开发得ؓҎ(gu)指定前置条g和后|条件成为可能?/p>
管 Nice 支持 DBCQ但它与 Java™ 语言完全不同Q因而很隑ְ其用于开发。幸q的是,很多针对 Java 语言的库也都?DBC 提供了方ѝ每个库都有其优点和~点Q每个库?DBC 内针?Java 语言q行构徏的方法也不同Q但最q的一些新Ҏ(gu)大都利用了 AOP 来更多地?DBC x点包括进来,q些x点基本上q当于Ҏ(gu)的包装器?/p>
前置条g在包装过的方法执行前dQ后|条件在该方法完成后d。?AOP 构徏 DBC l构的一个好处(请不要同该语a本n相淆!Q是Q可以在不需?DBC x点的环境中将q些l构xQ就像断a能被x一P。以横切的方式对待安全性关注点的真正妙处是Q可以有效地重用 q些x炏V众所周知Q重用是面向对象~程的一个基本原则。AOP 如此完美地补充了 OOP N不是一件极好的事情吗?
![]() ![]() |
OVal 是一个通用的验证框Ӟ它通过 AOP 支持单的 DBC l构q明地允许Q?
此外QOVal q带来大量预定义的约束条Ӟq让创徏新条件变得相当容易?/p>
׃ OVal 使用 AspectJ ?AOP 实现来ؓ DBC 概念定义Q所以必d AspectJ q入一个?OVal 的项目中。对于不熟?zhn)?AOP ?AspectJ 的h们来_好消息是q不隑֮玎ͼ且?OVal Q甚x创徏新的U束条gQƈ不需要真正对斚wq行~码Q只需~写一个简单的自引导程序即可,该程序会?OVal 所附带的默认方面植入?zhn)的代码中?/p>
在创个自引导E序斚w前,要先下蝲 AspectJ。具体地_(zhn)需要将 aspectjtools
?aspectjrt
JAR 文gq入(zhn)的构徏中来~译所需的自引导E序斚wq将其编入?zhn)的代码中?/p>
![]() ![]() |
下蝲?AspectJ 后,下一步是创徏一个可扩展 OVal GuardAspect
的方面。它本n不需要做什么,如清?5 所C。请保文g的扩展名?.aj l束Q但不要试着用常规的 javac
对其q行~译?/p>
清单 5. DefaultGuardAspect 自引导程序方?/a>
import net.sf.oval.aspectj.GuardAspect; public aspect DefaultGuardAspect extends GuardAspect{ public DefaultGuardAspect(){ super(); } } |
AspectJ 引入了一?Ant dQ称?iajc
Q充当着 javac
的角Ԍ此过E对斚wq行~译q将其编入主体代码中。在本例中,只要是我指定?OVal U束条g的地方,?OVal 代码中定义的逻辑׃~入我的代码Q进而充当v前置条g和后|条件?/p>
误?iajc
代替?javac
。例如,清单 6 是我?Ant build.xml 文g的一个代码片D,其中对代码进行了~译q把通过代码标注发现的所?OVal 斚w~入q来Q如下所C:
<target name="aspectjc" depends="get-deps"> <taskdef resource="org/aspectj/tools/ant/taskdefs/aspectjTaskdefs.properties"> <classpath> <path refid="build.classpath" /> </classpath> </taskdef> <iajc destdir="${classesdir}" debug="on" source="1.5"> <classpath> <path refid="build.classpath" /> </classpath> <sourceroots> <pathelement location="src/java" /> <pathelement location="test/java" /> </sourceroots> </iajc> </target> |
?OVal 铺好了\、ؓ AOP q程做了引导之后Q就可以开始?Java 5 标注来ؓ代码指定单的U束条g了?/p>
![]() ![]() |
?OVal 为方法指定前|条件必dҎ(gu)参数q行标注。相应地Q当调用一个用 OVal U束条g标注q的Ҏ(gu)ӞOVal 会在该方法真正执行前 验证该约束条件?/p>
在我的例子中Q我惌指定?Class
参数的gؓ null
ӞbuildHierarchy
Ҏ(gu)不能被调用。OVal 通过 @NotNull
标注支持此约束条Ӟ该标注在Ҏ(gu)所需的所有参数前指定。也要注意,M惌使用 OVal U束条g的类也必dcdơ上指定 @Guarded
标注Q就像我在清?7 中所做的那样Q?/p>
清单 7. OVal U束条g
import net.sf.oval.annotations.Guarded; import net.sf.oval.constraints.NotNull; @Guarded public class HierarchyBuilder { public static Hierarchy buildHierarchy(@NotNull Class clzz){ Hierarchy hier = new Hierarchy(); hier.setBaseClass(clzz); Class superclass = clzz.getSuperclass(); if(superclass != null && superclass.getName().equals("java.lang.Object")){ return hier; }else{ while((clzz.getSuperclass() != null) && (!clzz.getSuperclass().getName().equals("java.lang.Object"))){ clzz = clzz.getSuperclass(); hier.addClass(clzz); } return hier; } } } |
通过标注指定q个U束条g意味着我的代码不再会被重复的条件弄得ؕ七八p,q些条g?null
|q且一旦找到该值就会抛出异常。现在这w辑?OVal 处理Q且处理的方法有些相?—?事实上,如果q反了约束条ӞOVal 会抛Z?ConstraintsViolatedException
Q它?RuntimeException
的子cR?
当然Q我下一步就要编?HierarchyBuilder
cd 清单 5 中相应的 DefaultGuardAspect
cR我?清单 6 中的 iajc
d来实现这一目的Q这hp?OVal 的行为编入我的代码中了?
接下来,我更?清单 4 中的试用例来验证是否抛Z一?ConstraintsViolatedException
Q如清单 8 所C:
@Test(expectedExceptions={ConstraintsViolatedException.class}) public void verifyHierarchyNull() throws Exception{ Class clzz = null; HierarchyBuilder.buildHierarchy(clzz); } |
正如(zhn)所见,指定前置条g其实相当Ҏ(gu)Q指定后|条件的q程也是一栗例如,如果我想Ҏ(gu)有调?buildHierarchy
的程序保证它不会q回 null
|q样Q这些调用程序就不需要再查这个了Q,我可以在Ҏ(gu)声明之上攄一?@NotNull
标注Q如清单 9 所C:
@NotNull public static Hierarchy buildHierarchy(@NotNull Class clzz){ //method body } |
当然Q?code>@NotNull l不?OVal 提供的惟一U束条gQ但我发现它能非常有效地限制q些令h讨厌?NullPointerException
Q或臛_能够快速地暴露 它们?/p>
OVal 也支持在Ҏ(gu)调用前或后对cL员进行预先验证。这U机制具有限刉对特定约束条件的重复条g试的好处,如集合大或之前讨论q的?null
的情c?/p>
例如Q在清单 10 中,我?HierarchyBuilder
定义了一个ؓcdơ构建报告的 Ant d。请注意 execute()
Ҏ(gu)是如何调?validate
的,后者会依次验证 fileSet
cL员是否含|如果不含Q会抛出一个异常,因ؓ没有了要评估的类Q该报告不能q行?/p>
清单 10. 带条件检验的 HierarchyBuilderTask
public class HierarchyBuilderTask extends Task { private Report report; private List fileSet; private void validate() throws BuildException{ if(!(this.fileSet.size() > 0)){ throw new BuildException("must supply classes to evaluate"); } if(this.report == null){ this.log("no report defined, printing XML to System.out"); } } public void execute() throws BuildException { validate(); String[] classes = this.getQualifiedClassNames(this.fileSet); Hierarchy[] hclz = new Hierarchy[classes.length]; try{ for(int x = 0; x < classes.length; x++){ hclz[x] = HierarchyBuilder.buildHierarchy(classes[x]); } BatchHierarchyXMLReport xmler = new BatchHierarchyXMLReport(new Date(), hclz); this.handleReportCreation(xmler); }catch(ClassNotFoundException e){ throw new BuildException("Unable to load class check classpath! " + e.getMessage()); } } //more methods below.... } |
因ؓ我用的是 OValQ所以我可以完成下列dQ?/p>
fileSet
cL员指定一个约束条Ӟ保使用 @Size
标注时其大小L臛_?1 或更大?
@PreValidateThis
标注调用 execute()
Ҏ(gu)?验证q个U束条g?q两步让我能够有效地去除 validate()
Ҏ(gu)中的条g验,?OVal 为我完成q些Q如清单 11 所C:
@Guarded public class HierarchyBuilderTask extends Task { private Report report; @Size(min = 1) private List fileSet; private void validate() throws BuildException { if (this.report == null) { this.log("no report defined, printing XML to System.out"); } } @PreValidateThis public void execute() throws BuildException { validate(); String[] classes = this.getQualifiedClassNames(this.fileSet); Hierarchy[] hclz = new Hierarchy[classes.length]; try{ for(int x = 0; x < classes.length; x++){ hclz[x] = HierarchyBuilder.buildHierarchy(classes[x]); } BatchHierarchyXMLReport xmler = new BatchHierarchyXMLReport(new Date(), hclz); this.handleReportCreation(xmler); }catch(ClassNotFoundException e){ throw new BuildException("Unable to load class check classpath! " + e.getMessage()); } } //more methods below.... } |
清单 11 中的 execute()
一l调用(?Ant 完成Q,OVal ׃验证 fileSet
成员。如果其为空Q就意味着没有指定M要评估的c,׃抛出一?ConstraintsViolatedException
。这个异怼暂停q一q程Q就像初始代码一P只不q初始代码会抛出一?BuildException
?/p>