IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  Java technology  >

诊断 Java 代码:: Split Cleaner 错误模式

获取和释放资源应该协同工作

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

Eric E. Allen (eallen@cs.rice.edu), 博士研究生候选人

2001 年 7 月 05 日

Java 编程语言的一个特色是存储自动管理,它把程序员从很容易出错的释放使用后的内存的工作中解放出来。尽管如此,许多程序还是得处理资源问题,例如文件和数据库连接,这些都必须在使用之后明确地释放掉。跟手工管理存储一样,程序员在手工管理资源时也会犯很多错误。其中一个就是本周专栏的主题 ― Split Cleaner错误模式。在 讨论论坛与作者及其他读者交流本文的心得。

分开还是不分开

在管理诸如文件和数据库连接这样的资源时,您必须在使用完资源后把它释放掉。当然,对代码的任何指定的执行,您希望一次获得资源,然后一次将其释放。要做到这点,您可以采用两种方式:

  • 您可以在同一个方法中获得并释放资源。用这种方式,可以保证资源每获得一次,也释放一次。
  • 您可以跟踪代码的每一个可能的执行路径,并确保在每一个实例中资源最后都被释放掉了。
快速跟踪代码

清单 1. 遍历员工表的代码沿每一条执行路径关闭连接。 清单 2. 更新过时数据的 walker 代码在第一个 walker 结束后关闭连接 清单 3. 通过重新组织代码使资源的获得和释放发生在同一个方法内在获取资源的方法中把资源释放掉,就避开了没把资源释放掉的任何可能的执行路径。

第二种方式可能会出问题。因为您的代码库不可避免地要变大,另一个不熟悉您代码的程序员或许会添加一条没把资源释放掉的执行路径,其后果当然是资源泄漏。





回页首


Split Cleaner 错误模式

我把符合这种模式的错误称为 split cleaner,是因为释放资源的代码是沿各种可能的执行路径分开的。因为沿各条路径的释放代码很可能都是一样的,所以大多数 split cleaner 也是 rogue tile的例子。(Rogue tile 是我对一种错误的称呼,这种错误的起因是:起初用拷贝和粘贴的方式编写代码,但后来在做了一些更改后却忘了适当地修改代码的所有副本。如想更多了解 rogue tile,请参阅我的论文“ 错误模式:介绍。”)

例如,假设您正用 JDBC 处理一张员工姓名表。您希望执行的许多操作中包括遍历这张表并对其中包含的数据进行计算。您要完成的最后一个操作可能是打印出所有员工的名字,如下所示:


清单 1. 遍历一个员工表的代码
import java.sql.*;
public class Example {
    public static void main(String[] args) {
	String url = "your database url";
	try {
	    Connection con = DriverManager.getConnection(url);
	    new TablePrinter(con, "Names").walk();
	}
	catch (SQLException e) {
	    throw new RuntimeException(e.toString());
	}
    }
}
abstract class TableWalker {
    
    Connection con;
    String tableName;
    public TableWalker(Connection _con, String _tableName) {
	this.con = _con;
	this.tableName = _tableName;
    }
    public void walk() throws SQLException {
	String queryString =("SELECT * FROM " + tableName);
	Statement stmt = con.createStatement();
	ResultSet rs = stmt.executeQuery(queryString);
	
	while (rs.next()) {
	    execute(rs);	    
	}
	con.close();
    }
    public abstract void execute(ResultSet rs) throws SQLException;
}
class TablePrinter extends TableWalker {
    
    public TablePrinter(Connection _con, String _tableName) {
	super(_con, _tableName);
    }
    public void execute(ResultSet rs) throws SQLException {
	String s = rs.getString("FIRST_NAME");
	System.out.println(s);
    }
    
}

先说点题外话。请注意,我已把用来遍历表的代码抽出来放到了抽象类 Walker 中,以使新的子类可以很容易地遍历表中的行。虽然试图预测程序被扩展的各种方式并为其编写代码通常是浪费时间,但还是让我们假设在此例中 绝对地,毫无疑问地,无论如何对代码只做这一类的扩展。(事实上,我可以保证在本文结束前,只会有这样一种扩展)。





回页首


症状

现在,请注意数据库连接被传递到了 TableWalker 的构造函数中。一旦完成对表的遍历,它就将关闭连接。

所以,在这个例子中,我们采用第二种策略来清除连接。我们已经尝试过沿着每一条执行路径分别关闭连接。

让我们假设在我们的系统环境中,在一次遍历数据后关闭连接是有意义的(例如,也许这段代码会被从命令提示符中调用)。即使在那种情况下,我们也不能捕捉到每一条可能的执行路径 — 如果抛出了 SQLException ,程序可能会在关闭连接前异常终止。

因为 SQLException 在成熟代码中很少见,所以这个错误可能在很长一段时间内都不会(可能在原开发人员已经转行后也不会)表现出什么症状。自然地,这使得在症状 真的表现出来时,诊断起来就更加困难。

但是如果扩展了代码,就会有一些方式使症状的出现变得快得多。

例如,我们假设在原始代码写好后,发现存档的许多电话号码明显是过时的。于是管理人员决定把所有员工的电话号码都替换为 411。为完成这个更新,新写一个 TableWalker 如下:


清单 2. 更新过时数据的 walker 代码
class TableFixer extends TableWalker {
    
    public TableFixer(Connection _con, String _tableName) {
	super(_con, _tableName);
    }
    public void execute(ResultSet rs) throws SQLException {
	String s = rs.getString("FIRST_NAME");
	String updateString = "UPDATE " + tableName +
                              "SET PHONE_NUMBER = " + "411" +
                              "WHERE FIRST_NAME LIKE '" + s + "'";
	Statement stmt = con.createStatement();
	stmt.executeUpdate(updateString);
    }
}	

因为 TableFixer 也继承 TableWalker ,所以在这个类的一个实例上调用 walk 将关闭与该数据库的连接,就象 TablePrinter 一样。如果一个程序试图用同一个连接生成两个 walker 的实例,将发现第一个 walker 一完成遍历后连接就被关闭了。

编程新手很容易犯这样的错误,特别是如果不变量 ― 就是说只能构造一个 walker ― 没有文档或未测试过的话。





回页首


治疗及预防措施

当您发现有一条执行路径中没有包含适当的清除代码时,您可能会上当,只是简单地把它添加到那条路径中。

例如,您可以把 walk 方法的程序正文包到一个 try 程序块中,并加入一条 finally 子句以 确保关闭了连接。但这样一个解决方案却不是个好办法。

我们的 TableWalker 完全不必担心关闭连接的问题。即使每个 TableWalker 都 确实设法关闭了连接,我们也会陷入到第二种方式中,这种方式可以让这类错误模式自动现身 ― 当我们运行多个 walker 时,就会有 太多walker 试图关闭连接。

更糟的是,如果我们两次调用 con.close() (一次在 try 块中,一次在 catch 块中,而不是在 finally 语句中简单地单独调用),我们就会把 rogue tile 引入到代码中。

如果代码中添加了很多这种 rogue tile,那么要成功地重新组织代码将变得很困难。即使在测试期间,其中一些 rogue tile 可能处理的是基本上不会出现的执行路径。

一个好得多的解决方案是重新组织代码,用第一种方式来管理这些资源:把获得和释放资源的代码放到同一个方法中。

Andrew Hunt 和 Dave Thomas 在他们的一本优秀书籍 The Pragmatic Programmer 中用一个成语 ― “有始有终”来提倡这种思想。每个方法都要负责把它获得的资源释放掉。在我们的示例中,就是把对 con.close() 的调用移到类 Example 的 main 方法中,如下所示:


清单 3. 通过重新组织代码使资源的获得和释放发生在同一个方法内
public class Example {
    public static void main(String[] args) {
	String url = "your database url";
	// con must be declared and initialized here, so as to be in
	// the scope of both the try and finally clauses.
	Connection con = null;
	try {
	    con = DriverManager.getConnection(url, "Fernanda", "J8");
	    new TablePrinter(con, "Names").walk();
	}
	catch (SQLException e) {
	    throw new RuntimeException(e.toString());
	}
	finally {
	    try {
		con.close();
	    }
	    catch (Exception e) {
		throw new RuntimeException(e.toString());
	    }
	}
    }
}

这里,对 con.close() 的调用是在创建连接的相同 try 块的 finally 子句中,避开了没调用它的任何可能的执行路径。





回页首


总结

我们把本周的错误模式总结如下:

  • 模式:Split Cleaner
  • 症状:程序没能正确地管理资源,表现为泄漏或过早地释放了它们。
  • 起因:程序的一些执行路径没有做到它们应该做的工作:释放资源 正好一次
  • 治疗和预防措施:把负责释放资源的代码移到获得资源的同一方法中。

不再赘述。



参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.

  • 参加本文和本系列的其它文章的 讨论论坛


  • JUnit 主页提供了讨论程序测试方法的很多有趣文章的链接,并提供 JUnit 的最新版本。


  • 如果您喜欢 JUnit,请查阅全套 xUnit 测试工具(提供许多不同语言的版本)。


  • 如果我没有提及 xUnit 工具套件是被设计成用于 极端编程(一种新的快速开发干净、健壮的软件的强大方法)的,那我就不大负责任了。


  • 框架体系结构的 UML 概要文件”(PDF 幻灯片放映)展示了一个详细的 JUnit 个案研究。


  • Jinsight2.1是个 IBM alphaWorks 工具,它可以让您使 Java 应用程序的执行可视化并进行分析,因此您能够识别潜在的错误模式。


  • 利用 Java 调试教程(developerWorks,2001 年 2 月),获取一般调试技术的帮助。


  • Andrew Hunt 和 David Thomas 写的 The Pragmatic Programmer (Addison-Wesley,1999 年 10 月)能帮助您提高编程技巧。


  • 阅读 Eric 所有 诊断 Java 代码的文章,其中多数着重讨论的是错误模式。


  • developerWorks Java 技术专区上查找更多的 Java 参考资料。


关于作者

Eric Allen 在 Cornell 大学获得了计算机科学及数学的学士学位。他是 JavaWorld上 Java 初学者论坛的主持人,也是 Rice 大学 Java 编程语言小组的博士研究生。他的研究涉及在源程序和字节码层次上的语义模型和 Java 语言静态分析工具的开发。目前,他正在为 NextGen 编程语言实现一种从源代码到字节码的编译器,这也是 Java 语言的泛型运行时类型的一种扩展。可通过 eallen@cs.rice.edu与 Eric 联系。




对本文的评价










回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款