【 tulaoshi.com - 编程语言 】
本文的写作源于一个真实的大型软件开发项目,我们努力尝试在这个项目中推广应用AOP。在此我们将对曾经面临过的一些实际问题与困难进行分析,试图引发关于面向方面软件开发(AOSD)的一些更深层次的思考。
!-- frame contents -- !-- /frame contents --
本文的作者将站在开发者的角度做出客观的判定,既不是AOP的狂热鼓吹者,同样也不是AOP反对阵营的一员。因此可以视作来自Java开发者对AOP技术应用的客观分析和建设性意见。
关于AOP 关于AOP的概念,笔者在这里不再赘述。谁最先创造了AOP,业界一直有些争议,但普遍接受的说法大概是最先由Gregor J Kiczales在ECOOP'97提出来的,随后Gregor又申请了AOP的专利[US06467086]。很多人可能不太服气,因为他们或多或少早已有了类似的想法,只不过没有想到给他起个新名字罢了。无论是OOP,MOP,还是AOP,其本质的想法都是试图在更贴近现实世界的层次上实现软件开发的模块化。从这个角度看,AOP的想法不过是新瓶装旧酒罢了。其实AOP作为新生事物的出现,并不是一种技术上的飞跃,而是软件模块化发展到某一个阶段的一个阶段性产物。人的思维通常都有一些惯性,在我们饱尝了OOP的艰辛后,有一种新的概念跳出来分析总结了OOP的某些缺点,而且以看起来合理的方式做出改进,难免会给大家一种耳目一新的感觉。但不可否认的是,到目前为止,AOP角色所扮演的应用角色更多的只是对OOP的一种补充,因此作为一种重要的"OP"存在似乎有些名过其实,看起来更像是一种高级的设计模式。然而,在很多人的眼中AOP的分量甚至不亚于OOP,甚至AOP被视作未来软件开发的一个趋势。笔者一直思考一个问题,AOP出现的七八年时间在IT界并不算很短了,有趣的现象是AOP始终保持了小火慢炖的热度,一直没有像大家所期望的那样大红大紫起来。
那么AOP究竟在多大程度上可以帮助我们解决实际的问题呢?让我们尝试在一个真实的软件开发项目中应用AOP。对AOP所推崇的各个典型应用方向加以研究,例如,日志(Log),事务(Transaction), 安全性(Security), 线程池等等。必须说明,我们这里提到的场景,是有一定规模的软件产品开发,我们最终完成的是百兆数量级的软件产品,因此我们研究的范围既不是程序员的个人行为,也不是小范围的示例。让我们一起来看一看有什么有趣的发现。
AOP的实践
我们试验应用AOP的方向很多,这里仅以最具代表性的Log为例。大多数人了解AOP,都是从经典的Log 关注点的剥离开始的。因此这是每一个AOP的爱好者都耳熟能详的案例。按道理,应该是无可争辩的。很不幸,在我们的研究过程中还是碰到了很棘手的问题。
让我们来看一看一个经典的AOP 做日志的例子。
我们假定,按照AOP的思想,主逻辑的开发人员在写代码的时候不应该考虑边缘逻辑。因此,上述的日志代码实际对主逻辑的开发者不可见。假定我们以主流的Log4J为记日志的实现方式,以ASPectJ作为Aspect的实现方式。需要重申,本文的写作目的并不是针对某一种AOP的实现平台,选用AspectJ主要因为从语法的角度而言,AspectJ是目前所有AOP实现中覆盖范围最广的一种实现。
这样一个记日志的横切关注点描述,是最经典的AOP应用,它本身是没有任何问题的。通常我们会怎样用它呢?在继续了这个抽象Aspect的子Aspect实现中指定切入点的位置①,并在这个位置上将实现的逻辑填入通知(Advice)②。
在一个小规模的应用开发环境中这样做是不会有问题的,首先,记日志的切入点不多,无论是采用一对一的位置直接描述,还是利用统一的编码规范来约束都是可行的方案;其次,通知中的逻辑不会很复杂。整体的软件开发流程不会有什么变化的需要,通常的做法是由专门的Aspect开发人员统一编写Aspect,而由大家共享记Log的Aspect。但是不鼓励每一个开发人员都写自己的Aspect,这样就不是横(cross-cut),变成过筛子了(cross-point),软件开发变成一盘散沙,失去控制,AOP带来的好处丧失殆尽。
那么,在我们的项目中,情况怎样呢?上述看似简单的两个点都存在问题:
(1) 具我们统计,在我们开发的软件上一个版本的软件代码中,总共有7万句记Log的调用。假如我们不做任何相关的总结工作,直接一对一的对切入点进行描述,那么在位置①上的切入点描述就有7万条之多;姑且不算工作量,即使这样做了,将来带来的代码维护将是天文数字的成本,此路不通。
那么我们只能寄希望能够提炼出这7万句日志调用的公共模式,我们在这里用到的是一种优化过的Log组件,接口与LOG4J类似,考虑到LOG4J的广泛应用,我们下面将以LOG4J为参照。Log Level类中预定义了五个级别,DEBUG, INFO, WARN, ERROR,FATAL,根据统计,Fatal类型的调用最少,根据Fatal的级别定义,我们或许可以花一定时间整理代码,提炼出捕捉Fatal点的规则。然后次之,WARN和ERROR大约占7%左右,这一部分就不好办了,WARN/ERROR类型的LOG并没有严格的界定,代码的分布点也难寻规律,一定要找到规律,要付出相当大的代价。最后,DEBUG, INFO占据了很大的比例30%-50%,顾名思义,这一部分的代码出现的随机性很大,无论怎样努力都不可能找到有意义的公共规律。此路还是不通。
有一种说法也许可以解释这种想象:假如切入点难于描述的时候,很大原因是因为关注点的定义不准确。此说法有一定道理,以"日志"作为一个方面来切入粒度似乎太大了。那么,唯一的办法是将"日志"关注点进一步拆解。一直拆解到可以接受的程度。但是,我们的问题似乎还没有解决,假如我们的项目足够小,那么这样的拆解总是有一定的限度的,这种做法或许可行。但很不幸,我们的项目很大,要经过相当多的分解才能最终找到日志的规律性。我们还是可能需要成百上千条语句来指定切入点的位置,最终的结果将很难维护,这样的做法对于一个不断演化中的项目而言是脆弱乃至于不可接受的。况且,像Debug这样的Log级别,无论你怎样拆解,都不可能找到完美的规律。通常,任何一个系统中的Log都会保持逻辑的一致性,假如经过了这样的层层分解,Log作为一个逻辑主体的完整性被完全破坏了。这是一种为了AOP而AOP的做法,非但工作量没有减轻,还带来了无穷的后患。
好了,只剩最后一招了,为了用AOP, 我们牺牲掉Log的某些特性,预先定义好编码的规则和日志原则,强制推行。当然,假如想要全面覆盖所有的日志点,这样的日志原则只能定得非常粗。从AOP的角度来讲,技术上是可行的,但粗放的日志规则会带来Log的信息量疯长,对于我们的软件项目来说,还是不可接受,因为日志失去了它的精确性,会对系统的维护产生较大影响,而且大量日志信息的增长对系统整体运行性能的冲击是显而易见的。
(2) 在图1 的第二个要点上我们也同样面临问题,也许从图上的例子你可能还看不出来,因为在所有的介绍AOP的文档材料中介绍Log的例子都很简单。但是现实生活中的Log很不留情面。很多时候程序对Log的调用都有其非凡的原因,它们的Advice需要分别编写。例如在例子产品中我们经常需要把一个变量传给"日志"方面, 而且,经常要传入一个局部变量。至少现在,所有的AOP实现都还不支持对局部变量的捕捉,在可见的将来也不会有这种实现。好吧,只好重构代码,假如您想传参数,必须要遵守我们预先定义的编码命名规则。但是,这样给编程人员带来的限制会很大,你很难说服开发人员手捧厚厚的编码规范,小心翼翼的写程序。
综合上述两个致命缺陷,我们试图推行Log Aspect的工作已经碰到了相当的阻力。进入讨论组讨论。