技术开发 频道

检测 Java 代码: 破坏者数据错误模式

  【IT168 技术文章】

  百万分之一

  作为一个勤奋的开发人员,您已经为几个需要更好地访问复杂的大量数据存储的客户安装了一个应用程序,它编写良好,而且经过了充分测试。

  对每个客户,现场测试阶段都畅通无阻地通过了。您在去银行的路上,心里极少考虑这六个月来的软件审查,这时您的传呼机响了起来。您的一个客户在使用您的软件运行一个报表时,系统崩溃了。

  您赶到出事地点,运行了一个随机测试。工作良好。您运行另一个。没出现问题。您又运行了数百个测试。还是没有问题。您又检查了持续六个月运行这个应用程序的其它客户。没有投诉。

  您重复运行那个引起问题的报表。崩溃!怎么回事?

  破坏者数据错误模式

  许多程序需要频繁访问和处理内部储存的数据来执行各种复杂的任务。这种数据可以从内存中的大型结构、数据库或网络上检索得到。

  这类程序非常容易遭受损坏的内部数据引起的崩溃。我称这种错误模式为破坏者数据模式,是因为这种数据可以无限期地存在于系统中(很象冷战中的潜伏间谍一样),不引发任何问题,直到访问一段特定的数据时,损坏的数据才象炸弹一样爆炸。

  语法原因

  假定我们有一个 JDBC 应用程序,它存储了一个名为 Mapping 的数据库表,该表将 String 的名称映射到一系列元素的集合。每个集合中的每个元素都引用另一个表中的一个关键字(该表名为 Properties,包含这些元素的不同已知属性)。

  这样说吧,Mapping 和 Properties 表最初都是从一个文本文件中读取的,这个文本文件由外部源( 外部意为不是内部产生的任意数据源)发展而来,而在外部源中,每行都以一个名称开头,后面跟着对应集合的表达,如下所示:

  清单 1. 样本,外部源文本文件

1 In the Mapping file:
2 apples {macintosh, gala, golden-delicious}
3 trees  {elm, beech, maple, pine, birch}
4 rocks  {quartz, limestone, marble, diamond}
5 ...
6 In the Properties file:
7 macintosh {color: red, taste: sour}
8 gala      {color: red, taste: sweet}
9 diamond   {color: clear, rigidity: hard, value: high}
10 ...
11

  可以对 Mapping 和 Properties 表条目进行语法分析并将其传递到一个方法中,此方法会把这些条目插入到一个数据库中。但这种方法存在潜在的缺陷。例如,假定我们已经编写了一个处理 JDBC 兼容数据库的类。遵照 JDBC API,我们可以定义一个 PreparedStatement 对象并使用它把信息传递到数据库中,如下所示:

  清单 2. 使用 StreamTokenizer 插入域和区域字符串

1 ...
2   PreparedStatement insertionStmt =
3     con.prepareStatement("INSERT INTO MAPPING VALUES(?,?)");
4   ...
5   public void insertEntry(String domain, String range)
6       throws SQLException {
7         insertionStatement.setString(1, domain);
8         insertionStatement.setString(2, range);
9         insertionStatement.executeUpdate();
10       }
11

  以这种方式插入两个 String 合适与否取决于从文本文件中获取 String 的方式。例如,假定一个简单的正则表达式匹配工具被用来将每一行拆分成两个 String :

  一个 String 包含第一个 String 之前的全部字符。

  一个 String 包含第一个 String 之后的全部字符。

  这种对文本文件进行的基本的语法分析不会捕获数据的较小损坏。例如,如果其中的一行是如下的形式:

  清单 3. 数据破坏者

1 trees  {elm, beech, maple, pine birch}
2

  “pine”和“birch”之间的逗号漏掉了。这样的错误很容易由生成文件的工具的错误或手工编辑文件而造成。

  无论如何,数据都会以损坏的形式进入数据库,静静地等待被访问。如果用于访问数据的方法要求用逗号和空格来分隔条目,在读取这个条目的时候就会导致崩溃。

  如果程序只是简单地使用逗号来区分集合中的元素,更加严重的错误都可能发生。系统可能将“pine birch”解释为一个单独的树类型(数据的单个条目),并将这个错误进一步传播到计算中去。

  语义原因

  我们的示例是一个违反了数据的一个简单的语法特征演示。当然,这不是可能损坏数据的唯一途径。

  语义级别上的限定因素也可能被破坏。在我们的示例中,Mapping 表中数据的一种要求是,每个集合中的每个元素都是 Properties 表中的一个域条目。如果这种不变量被破坏,程序就会在试图读取一个在 Properties 表中并不存在的元素时失败,导致异常被抛出。

  在本文中,我使用数据库条目作为示例,但是破坏者数据错误会以各种方式出现 ― 与数据输入的方式一样多。当程序读取数据时,不管它是从文件、键盘、麦克风、网络还是电子手套读取,破坏者数据错误都有可能存在。

  治疗和预防措施

  最好的防备破坏者数据错误的方法是编译器和解释器开发人员普遍采用的那种方法。由于输入到这些程序的数据是如此复杂,开发人员别无它法,只有在第一次读取输入内容的时候就执行尽可能彻底的完整性检查,而不是在以后访问的时候再进行检查。

  语法分析作为消除错误的方法

  实际上,对输入内容进行 语法分析的方法恰好是消除大量这些错误的途径。不幸的是,很多程序员(他们从来不会考虑编写没有语法分析器的编译器)没有能够为较简单的数据编写足够的语法分析方法。较简单的数据的语法分析当然要容易一点,但是这并不能成为根本不对它进行语法分析的借口。

  任何读取数据的程序 ― 不管有多简单 ― 都应该对数据进行语法分析。毕竟,这种程序在它的有效输入的集合所定义的“语言”上可以被看作是一个编译器(或者解释器)。

  从经历过的人那里吸取一点经验吧。我年轻时比较鲁莽,犯了个错误 ― 处理数据的时候没有作适当的语法分析,然后我就遭受了那样的后果 ― 猖獗的破坏者。我可不推荐这种经验。

  类型检查作为消除错误的方法

  编译器为许多语言(当然包括 Java 语言)所作的检查的另一种普遍形式是 类型检查。类型检查是程序完整性上的语义级别检查的一个示例。

  如果类型系统是健壮的(就象 Java 类型系统一样),这种完整性检查确实可以保证很多错误永远不会在运行时产生。象语法分析一样,编译器编写者的这个示例可以应用于其它经常在其输入数据上规定语义级别不变量的程序。这些不变量通常不是明确的,但可以通过进行对应的检查来把它们变成明确的。

  反复操作作为消除错误的方法

  当然,如果您怀疑这种错误模式的出现与已经读入并存储的数据有关,反复操作数据也是明智的:访问在实际配置的应用程序中可能要操作的每个数据,保证一切都象期望的那样工作。通过这种方法,您也能够修正简单的错误。

  关于消除错误方法的一个告诫

  我的意思当然不是暗示执行足够的检查来消除程序中的所有破坏者数据 总是可行的。如果真是那样,就不会有引起错误模式的潜在问题了。

  一个破坏者在开始带来灾难之前为什么会无法觉察是有很多原因的:

  执行所有检查必需的数据直到破坏者数据已经存储了以后才可用。

  整套的限定元素甚至是无法计算的(就象编译器和解释器的情况一样)。

  限定元素可以计算,但是程序无法访问检查它们所需要的资源。

  在这些情况下,我们最好是尽量消除可能的破坏者形式。

  结论

  下面是破坏者数据错误模式的总结:

  模式:破坏者数据

  症状:一个存储和处理复杂的输入数据的程序在执行任务时意外地崩溃,而这个任务和其它没有产生任何问题的任务很相似。

  起因:一些内部数据被损坏,可能是语法上的损坏,也可能是语义上的。

  治疗和预防措施:对输入数据尽量多地执行完整性检查,而且要尽量早执行。对于已经损坏的持久数据,研究它并检查其完整性。

  消除数据破坏者的黄金法则: 任何读取数据的程序都应该对数据进行语法分析。

0
相关文章