日期:2014-05-16  浏览次数:20482 次

与数据库连接释放有关的设计模式
使用Java做过商业应用开发的人想必一定用到过数据库。

不论具体方案是使用JDBC还是JDO还是直接使用J2EE提供的连接池又或者是使用Hibernate屏蔽了这一切,不变的原则是,数据库连接是有限的资源,为了实现数据持久化的高效和稳定,不应该不加控制地频繁创建数据库连接;更不能在建立连接(或从连接池中取出)并使用之后就简单地结束,而不做资源回收。即使这种资源有超时空闲的自动回收机制,这种做法也是不可接受的。

打住,随便发了点儿评论。言归正传,本文的标题虽然是讨论数据库连接释放的问题,但实质是借着这个话题说明一下设计模式在Java中的应用。

现在,让我们设想一个虚拟场景:当前项目的数据库连接方式采用了JDBC + 连接池。如此设想是因为大概这种实现的效率最高,且可以发挥的地方也最多。

系统启动阶段建立了数据库的连接池,里面预先放了很多的连接,供需要持久化的模块调用,并接收返还的连接。

这里,每块借用了连接的代码都要记得用完了之后一定得返还连接。否则,连接池里的连接用完了,系统就不得不新建连接,迟早会抛出连接无法创建的异常。

那么,第一种方案是,把所有的数据库操作放在一个try块里。然后在finally里执行返还连接的操作。这种方案好处是便于实现,目的单纯。对于只有一、两处数据库操作的应用来说,效果明显,维护起来也不困难。

但是假设应用中不只含有一个数据库,比如说是按照功能划分了安全管理、商品库存、销售记录、员工考勤等库(表空间),而实际上会用到数据操作的不下几十处,那么第一种方案就是有问题的了。尤其对于一次访问若干个库,比如说同时访问商品库存和销售记录,同时访问安全管理和员工考勤,这时候程序员要时刻牢记目前有哪些连接在使用,finally要释放哪些连接,即使项目完成了,也不可能通过某种便捷有效的手段保证所有的连接都能够正常释放。举个最简单的例子。
// ...
Connection connA = DbUtil.getConnA();
Connection connB = DbUtil.getConnB();

try {
    // ...
} catch (Exception ex) {
    // ...
} finally {
    connA.close();
    connB.close();
}

粗看上去,似乎两个连接都释放掉了。但实际并非如此,connB不一定会被关掉。假设在try块中,connA的服务器突然宕机了。这时候,在finally块里,connA.close();将会抛出异常,connB.close();的代码将不会被执行。

这时候内行说话了,只要在finally块中给每个close()方法套上try块就行了,像这样:
        // ...
    } finally {
        try {
            connA.close();
        } catch (Exception ex) {
        }
        try {
            connB.close();
        } catch (Exception ex) {
        }
    }
}

这样改是没有毛病了,不过稍微有些冗长。再修改一下:
        // ...
    } finally {
        DbUtil.close(connA);
        DbUtil.close(connB);
    }
}
// ...
public final class DbUtil
{
    private DbUtil()
    {
    }

    public static void close(Connection conn)
    {
        try {
            conn.close();
        } catch (Exception ex) {
        }
    }
}

那么如何保证几十个用到连接的地方都释放了呢?尤其C+P之后,编译通过也不说明逻辑没有问题。例如:
        // ...
    } finally {
        DbUtil.close(connA);
        DbUtil.close(connA); // C + P 后遗症,忘记修改变量名了
    }
}
// ...

用了什么就要记住释放什么,这种事,一点儿安全感都没有。

那么,现在要隆重祭出设计模式这杆大旗了。

不过很不好意思地说,具体是什么模式,实际上我心里也没底。我先解释解释吧。

首先,设计目的是,要能够比较自由地使用某种资源,并且在停止使用资源之后必然做到自动的资源释放。

其次,设计思想是,把使用资源的这个操作变为整个操作中可以被替换的部分,整个流程应该是这样的:取得资源,使用资源,释放资源。其中使用资源是不断变化的,相应取得资源也有可能发生变化,但是释放资源不会有变化,而且这样的三步走流程也始终保持不变。

因此,我们可以设想有一个对象,创建的时候把资源和资源操作当作参数注入其中,然后执行三步走。这样,只要所有的数据库操作都使用这个对象来执行,我们就可以很确信的说,所有的资源必然都会被正确的释放。除非我们设计的对象有什么其他缺陷。不过这样的方案肯定能大大简化代码的维护,把散布在各处的数据持久化操作的维护转移到对某个类的维护。

由于Java没有闭包,因此只能使用对象模仿函数指针。代码如下:
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * DbAdapter
 * 数据库访问接口类
 */
public abstract class DbAdapter
{
    /**
     * 处理sql,并根据要求执行数据操作或查询
     */
    static Object process(Connection conn, String sql, DbParamInjector injector, DbReader reader) throws SQLException
    {
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            pstmt = conn.prepareStatement(sql);
            if (injector != null) {
                if (reader != null) {
                    injector.setParams(pstmt);
                    rs = pstmt.executeQuery();
                    return reader.read(rs);
                } else {
                    do {
                        injector.setParams(pstmt);
                        pstmt.execute();
                    } while (injector.hasNext());
                    return null;
                }
            } else {
                if (reader != null) {
                    rs = pstmt.executeQuery();
                    return reader.read(rs);
                } else {
                    pstmt.execute();
                    return null;
                }
            }
        } finally {
            // 释放数据库资源
            DBUtil.