传统的 DB2 数据库访问程序
让我们先来温习几段代码,它们将从一个数据库连接池中得到可用的数据库连接,然后对数据库进行一些操作,操作完毕后再将这个连接返还给连接池。在此基础上,我们为这些数据库操作增加了对事务的支持,保证它们对数据库的更改要么同时成功,要么同时恢复如初。
相信每位编写过 DB2 数据库访问模块的开发人员都设计过类似的代码,我们长期以来一直都这么做,可这是最好的么?我们还能做得更好么?一开始,先让我们来分析一下这些代码片断的特点吧。
代码片断 1:DB2 数据库连接池的接口
public interface DB2ConnectionPool {
public Connection getConnection(String url, String userName, String password) throws SQLException;
public boolean putConnection(Connection connection);
public void registerConnection(String url, String userName, String password);
}
|
真实环境中的 DB2 数据库连接池会有着更为复杂的接口和实现,这里只是给出了最基本的操作:获取一个数据库连接,注册一个数据库连接以及释放一个数据库连接; 在很多工业项目中,常常会使用应用服务器产品诸如 WebSphere Application Server 来管理数据库连接池。
代码片断 2:DB2 数据库访问程序的典型片断
public void db2Manipulation() throws SQLException{
SimpleDB2ConnectionPool _connPool =
SimpleDB2ConnectionPool.getInstance(); // 获取 DB2 数据库连接池实例
Connection connection = _connPool.getConnection("db2:jdbc:SAMPLE", "user", "password");
if(null == connection)
{
connection = DriverManager.getConnection("db2:jdbc:SAMPLE", "user", "password");
} // 获取 DB2 数据库连接
...
// 进行和业务逻辑相关的数据库操作
...
if(false == _connPool.putConnection(connection)) // 释放 DB2 数据库连接
{
connection.close();
}
}
|
代码片断中的 SimpleDB2ConnectionPool 是 DB2ConnectionPool 的一个简单实现。这是个典型的 DB2 数据库访问程序,先是获取数据库连接,然后进行和业务逻辑相关的数据库操作,最后释放数据库连接。在这个代码片断中,列出的代码中相当部分其实都和应用中实际的业务逻辑没有多大关系,但它们却占用了代码的大段篇幅。
代码片断 3:增加事务处理支持的 DB2 数据库访问程序片断
public void db2Manipulation() throws SQLException{
// Get reference of Connection Pool
SimpleDB2ConnectionPool _connPool =
SimpleDB2ConnectionPool.getInstance(); // 获取 DB2 数据库连接池实例
Connection connection = _connPool.getConnection("db2:jdbc:SAMPLE", "user", "password");
if(null == connection)
{
connection = DriverManager.getConnection("db2:jdbc:SAMPLE", "user", "password");
} // 获取 DB2 数据库连接
Statement stmt = null;
try
{
connection.setAutoCommit(false); // 设置数据库连接不会自动提交事务
stmt = connection.createStatement(); // 在数据库连接基础上创建语句对象
stmt.executeUpdate("insertsql"); // 进行数据库插入记录操作
stmt.executeUpdate("updatesql"); // 进行数据库更新记录操作
stmt.executeUpdate("deletesql"); // 进行数据库删除记录操作
connection.commit(); // 提交数据库事务
} catch (Exception ex)
{
connection.rollback(); // 回滚数据库事务
// log // 记录日志
} finally
{
stmt.close(); // 释放语句对象
// log // 记录日志
}
if(false == _connPool.putConnection(connection)) // 释放DB2数据库连接
{
connection.close();
}
}
|
代码片断 3 在代码片断 2 的基础上增加了事务处理的支持。 默认情况下,数据库连接会自动提交事务,所以需要首先将其设置为手工提交事务;然后,在该事务管理范围之内,进行各种数据库更新操作,操作成功则提交事务,操作失败则回滚事务,最后释放数据库连接。这里判断数据库操作是否成功的原则是根据操作过程中是否发生异常。在代码片断 3 中,同样会发现大段与应用逻辑没有关系的代码占用了大量的开发时间和代码量。
从上面这几个代码片断,我们可以看出在 DB2 数据库访问程序中,常常会有这样几类与具体的业务逻辑关系不大,但却是不能缺少的代码:
- 数据库连接的获取和释放:没有它们,我们就无法对数据库做任何事情。
- 增加数据库事务的处理支持:少了事务的支持,会让我们在修改数据库的时候胆战心惊。
- 捕捉并且处理数据库处理异常:这些是必需要有的,不然我们的程序很容易崩溃。
- 记录日志:当程序运行时出了问题的时候,日志其实是最好的诊断材料。
看来,是到了考虑重构这些代码的时候了。
AOP 能够给我们带来什么
面向过程编程离我们已经有些遥远,面向对象编程正主宰着软件世界。当每个新的软件设计师都被要求掌握如何将需求功能转化成一个个类,并且定义它们的数据成员、行为,以及它们之间复杂的关系的时候,面向方面编程(Aspect-Oriented Programming,AOP)为我们带来了新的想法、新的思想、新的模式。
如果说面向对象编程是关注将需求功能划分为不同的并且相对独立,封装良好的类,并让它们有着属于自己的行为,依靠继承和多态等来定义彼此的关系的话;那么面向方面编程则是希望能够将通用需求功能从不相关的类当中分离出来,能够使得很多类共享一个行为,一旦发生变化,不必修改很多类,而只需要修改这个行为即可。
面向方面编程是一个令人兴奋不已的新模式。就开发软件系统而言,它的影响力必将会和有着十数年应用历史的面向对象编程一样巨大。面向方面编程和面向对象编程不但不是互相竞争的技术而且彼此还是很好的互补。面向对象编程主要用于为同一对象层次的公用行为建模。它的弱点是将公共行为应用于多个无关对象模型之间。而这恰恰是面向方面编程适合的地方。有了 AOP,我们可以定义交叉的关系,并将这些关系应用于跨模块的、彼此不同的对象模型。AOP 同时还可以让我们层次化功能性而不是嵌入功能性,从而使得代码有更好的可读性和易于维护。它会和面向对象编程合作得很好。
这是张非常经典的插图,你几乎可以在任何一本介绍 AOP 的书籍中找到它。它告诉我们,需求功能通过 AOP 的魔力三棱镜的折射之后,就会变成彼此相对独立的方面(Aspect),我们可以分别实现它们,然后再组合起来。透过这个三棱镜,我们看到了从未看到的奇妙的景象。
AOP 的应用范围
传统的程序通常表现出一些不能自然地适合单一的程序模块或者是几个紧密相关的程序模块的行为,AOP 将这种行为称为横切,它们跨越了给定编程模型中的典型职责界限。横切行为的实现都是分散的,软件设计师会发现这种行为难以用正常的逻辑来思考、实现和更改。最常见的一些横切行为如下面这些:
- 日志记录,跟踪,优化和监控
- 事务的处理
- 持久化
- 性能的优化
- 资源池,如数据库连接池的管理
- 系统统一的认证、权限管理等
- 应用系统的异常捕捉及处理
- 针对具体行业应用的横切行为
目前,前面几种横切行为都已经得到了密切的关注,也出现了各种有价值的应用,但也许今后几年,AOP 对针对具体行业应用的贡献会成为令人关注的焦点。
AOP 的具体实现
AOP 是一个概念,一个规范,本身并没有设定具体语言的实现,这实际上提供了非常广阔的发展的空间。本文中采用的是一个很悠久的实现 ― AspectJ,它能够和 Java 配合起来使用。
介绍 AspectJ 的使用和编码不是本文的目的,你可以在 Google 上找到很多有关它的材料。
这里只是重温 AspectJ 中几个必须要了解的概念:
- Aspect: Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。
- Joint point:表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。
- Pointcut:表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
- Advice:Advice 定义了在 pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。
如果你平时使用基于 Eclipse 的开发工具,可以在 http://eclipse.org/ajdt/ 下载到最新的支持Eclipse 的 AspectJ 插件,安装起来并不困难,它同时还提供了不错的帮助文档和范例代码,你可以从那里开始 AspectJ 的学习。
下面就给出了一些 AspectJ 的片断,来帮助重构我们的代码。
代码片断 4:横切 DB2 数据库连接的获取和释放(
附件 1 中有这个 Aspect 的全部代码) 附件 1 中有这个 Aspect 的全部代码)
pointcut connectionCreation(
String url,
String username,
String password) : call(
Connection DriverManager.getConnection(String, String, String))
&& args(url, username, password);
Connection around(String url, String userName, String password)
throws SQLException : connectionCreation(url, userName, password) {
Connection connection =
_connPool.getConnection(url, userName, password);
if (connection == null) {
connection = proceed(url, userName, password);
_connPool.registerConnection(connection, url, userName, password);
}
return connection;
}
|
分析:connectionCreation 定义的是所有涉及到调用 DriverManager.getConnection(String, String, String) 的代码,这里还列出了它的参数列表;相应地,在它的 around advice 处理代码中判断数据库连接是否为空;如果为空,将重新申请数据库连接并且在连接池中注册它。这些与业务逻辑无关但又是必须的代码就会从原来的程序中分离出来。
pointcut connectionRelease(Connection connection) : call(
void Connection.close())
&& target(connection);
void around(Connection connection) : connectionRelease(connection) {
if (!_connPool.putConnection(connection)) {
proceed(connection);
}
}
|
分析:connectionRelease 定义了所有有关释放数据库连接的代码;相应的around advice会将其释放到数据库连接池当中。这样,从原来的代码看来,不用考虑数据库连接池的存在,而只需正常地关闭一个数据库连接。
代码片断 5:横切 DB2 数据库事务支持代码以及异常捕捉(
附件 2 中有这个 Aspect 的全部代码) 附件 2 中有这个 Aspect 的全部代码)
pointcut getConnection() : call(
Connection *.getConnection(String url, String user, String password));
Connection around()
throws SQLException : getConnection() && cflow(transactedOperation()) {
if (null == _connection) {
_connection = proceed();
_connection.setAutoCommit(false);
}
return _connection;
}
|
分析:getConnection() && cflow(transactedOperation()) 对应的 around advice 则是做了获取数据库连接的操作,另外显式地设置了 setAutoCommit 属性为 false,使得数据库事务的提交能够由代码来控制。
pointcut transactedOperation() : execution(needTransactionFunction.execute (String sqlstr));
pointcut topLevelTransactedOperation() : transactedOperation()
&& !cflowbelow(transactedOperation());
Object around() : topLevelTransactedOperation() {
Object operationResult;
try {
operationResult = proceed();
if (null != _connection) {
_connection.commit();
}
} catch (Exception ex) {
if (null != _connection) {
_connection.rollback();
}
} finally {
if (null != _connection) {
_connection.close();
}
}
return operationResult;
}
|
分析:topLevelTransactedOperation 定义了所有最顶层的涉及到需要数据库事务支持的函数调用;在其相应的 around advice 中,会先执行原来的函数并且得到它的返回值。如果返回值非空,则提交数据库事务;如果返回值为空,则回滚数据库事务;在最后释放该数据库连接。而在核心的代码中,我们不用增加有关数据库事务支持的任何代码,只需要更改这里 Aspect 的定义即可。
private static aspect SoftenSQLException {
declare soft : java.sql.SQLException : (
call(void Connection.rollback()) || call(void Connection.close()))
&& within(DB2TransactionAspect);
}
|
分析:SoftenSQLException 定义了调用 Connection.rollback() 和 Connection.close() 以及 DB2TransactionAspect 中的所有 Point Cut 范围之内所发生的 SQLException 异常,这里只是将其声明为 soft,并没有提供异常发生后的处理。这为统一处理 SQLException 提供了一种方法。
pointcut illegalConnectionManagement() : (
call(void Connection.close())
|| call(void Connection.commit())
|| call(void Connection.rollback())
|| call(void Connection.setAutoCommit(boolean)))
&& !within(DB2TransactionAspect);
void around() : illegalConnectionManagement() {
}
|
分析:illegalConnectionManagement 定义了一个有趣的规则,它限制在 DB2TransactionAspect 所规定的 Point Cut 范围之外不允许调用有关数据库连接的操作。这个规则非常有用,会在我们不小心写错代码的时候提醒我们!
代码片断 6:横切程序运行日志的记录(
附件 3 中有这个 Aspect 的全部代码) 附件 3 中有这个 Aspect 的全部代码)
public pointcut connectionActivities(Connection conn) : (
call(* Connection.commit(..)) || call(* Connection.rollback(..)))
&& target(conn);
before(Connection conn) : connectionActivities(conn) {
Signature sig = thisJoinPointStaticPart.getSignature();
System.out.println("[" + sig.getName() + "] " + conn);
}
|
分析:connectionActivities 会在每一个涉及到事务提交和回滚的调用时将有关当前的数据库连接的相应信息记录下来。
public pointcut updateActivities(Statement stmt) : call(
* Statement.executeUpdate(..))
&& target(stmt);
before(Statement stmt) throws SQLException : updateActivities(stmt) {
Signature sig = thisJoinPointStaticPart.getSignature();
System.out.println("[" + sig.getName() + "] " + stmt.getConnection());
}
|
分析:updateActivities 会在每一个涉及到数据库更新的操作调用时将有关当前语句对象的信息记录下来。
程序运行日志的记录是一个健壮的应用程序的重要模块,这里将程序中重要的方法分为两类,一类是和数据库连接相关的操作,一类是与数据库更新操作有关的操作。在系统运行时,可以通过记录的日志跟踪系统内部运行的情况。
抛砖引玉
上面的 AspectJ 的代码片断,只是抛砖引玉,给读者一些启发,下面要讨论的这些问题,也许正是接触了 AOP 之后所困惑的。
AOP 帮助我们解决了新的问题了么
AOP 并没有帮助我们解决任何新的问题,它只是提供了一种更好的办法,能够用更少的工作量来解决现有的一些问题,并且使得系统更加健壮,可维护性更好。同时,它让我们在进行系统架构和模块设计的时候多了新的选择和新的思路
AOP 和 OOP 到底是什么关系
很多人在初次接触 AOP 的时候可能会说,AOP 能做到的,一个定义良好的 OOP 的接口也一样能够做到,我想这个观点是值得商榷的。AOP和定义良好的 OOP 的接口可以说都是用来解决并且实现需求中的横切问题的方法。但是对于 OOP 中的接口来说,它仍然需要我们在相应的模块中去调用该接口中相关的方法,这是 OOP 所无法避免的,并且一旦接口不得不进行修改的时候,所有事情会变得一团糟;AOP 则不会这样,你只需要修改相应的 Aspect,再重新编织(weave)即可。 当然,AOP 也绝对不会代替 OOP。核心的需求仍然会由 OOP 来加以实现,而 AOP 将会和 OOP 整合起来,以此之长,补彼之短。
DB2 数据库访问程序还能做哪些进一步的改进
本文中所给出的范例并不一定是最优的解决方案,它只是用来启发大家能够有意识地发掘出自己应用中的横切需求,并运用 AOP 加以实现,在实践中不断体会 AOP 给我们带来的好处。在范例代码的基础上,我们还可以进行很多改进,比如让我们支持的事务适合分布式的应用,优化对 SQLException 异常的处理等等。
AOP 适合工业化的应用么
这个问题很难回答,其实最好的答案就是尝试,用成功的项目或是产品来回答。Jboss 4.0 就是完全采用 AOP 的思想来设计的 EJB 容器,它已经通过了 J2EE 的认证,并且在工业化应用中证明是一个优秀的产品。相信在不远的将来,会出现更多采用 AOP 思想设计的产品和行业应用。
小结
AOP 正向我们走来,我们需要关注的是怎么样使得它能够为我们的软件系统的设计和实现带来帮助。本文选取了 DB2 数据库访问程序这样一个角度,抛砖引玉,旨在给大家一点启发,能够在更多的领域更深入的应用 AOP 的思想。
- http://eclipse.org/aspectj/ 这里是 AspectJ 的大本营,你可以从中得到 AspectJ 及其相关工具,还有很多有关 AOP 的有益的讨论。
- http://eclipse.org/ajdt/ Eclipse 已经有了 AspectJ 的插件,你可以将它和 WSAD 集成起来使用。
- http://aosd.net/index.php 这里你可以找到有关 AOP 更为丰富的资源。
- http://www.ibm.com/developerworks/cn/java/j-aspectj/ 这是一篇不错的介绍 AspectJ 的入门文章,你可以从中了解到有关 AOP 的基本概念。
- http://www.ibm.com/developerworks/cn/rational/r-interview/ 聆听 Grady Booch 教诲,大师的思想,他本人非常看好 AOP 的前景。
- http://www.microsoft.com/china/MSDN/library/windev/COMponentdev/AspectOrientedProgrammingEnablesBetterCodeEncapsulationandReuse.mspx 从本文我们可以了解在 .NET 平台下为 AOP 提供了哪些宝贵的支持。
- 《AspectJ in Action》 这是本非常不错的书,尽管篇幅有些长,是它把我领进 AOP 的大门的。