1.引言
软件开发的目标是要对世界的部分元素或者信息流建立模型,实现软件系统的工程需要将系统分解成可以创建和管理的模块。于是出现了以系统模块化特性的面向对象程序设计技术。模块化的面向对象编程极度极地提高了软件系统的可读性、复用性和可扩展性。向对象方法的焦点在于选择对象作为模块的主要单元,并将对象与系统的所有行为联系起来。对象成为问题领域和计算过程的主要元素。但面向对象技术并没有从本质上解决软件系统的可复用性。创建软件系统时,现实问题中存在着许多横切关注点,比如安全性检查、日志记录、性能监控,异常处理等,它们的实现代码和其他业务逻辑代码混杂在一起,并散落在软件不同地方(直接把处理这些操作的代码加入到每个模块中),这无疑破坏了OOP的“单一职责”原则,模块的可重用性会大大降低,这使得软件系统的可维护性和复用性受到极大限制。这时候传统的OOP设计往往采取的策略是加入相应的代理(Proxy)层来完成系统的功能要求,但这样的处理明显使系统整体增加了一个层次的划分,复杂性也随之增加,从而给人过于厚重的感觉。由此产生了面向方面编程(AOP)技术。这种编程模式抽取出散落在软件系统各处的横切关注点代码,并模块化,归整到一起,这样进一步提高软件的可维护性、复用性和可扩展性。
2.AOP简介
AOP: Aspect Oriented Programming 面向切面编程。 面向切面编程(也叫面向方面):Aspect Oriented Programming(AOP),是目前软件开发中的一个热点。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。 AOP是OOP的延续,是(Aspect Oriented Programming)的缩写,意思是面向切面(方面)编程。 主要的功能是:日志记录,性能统计,安全控制,事务处理,异常处理等等。 主要的意图是:将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改 变这些行为的时候不影响业务逻辑的代码。
可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。AOP实际是GoF设计模式的延续,设计模式孜孜不倦追求的是调用者和被调用者之间的解耦,AOP可以说也是这种目标的一种实现。
假设把应用程序想成一个立体结构的话,OOP的利刃是纵向切入系统,把系统划分为很多个模块(如:用户模块,文章模块等等),而AOP的利刃是横向切入系统,提取各个模块可能都要重复操作的部分(如:权限检查,日志记录等等)。由此可见,AOP是OOP的一个有效补充。
注意:AOP不是一种技术,实际上是编程思想。凡是符合AOP思想的技术,都可以看成是AOP的实现。
3.什么是方面编程
在考虑对象及对象与其他对象的关系时,我们通常会想到继承这个术语。例如,定义某一个抽象类 — Dog 类。在标识相似的一些类但每个类又有各自的独特行为时,通常使用继承来扩展功能。举例来说,如果标识了 Poodle,则可以说一个 Poodle 是一个 Dog,即 Poodle 继承了 Dog。到此为止都似乎不错,但是如果定义另一个以后标识为 Obedient Dog 的独特行为又会怎样呢?当然,不是所有的 Dogs 都很驯服,所以 Dog 类不能包含 obedience 行为。此外,如果要创建从 Dog 继承的 Obedient Dog 类,那么 Poodle 放在这个层次结构中的哪个位置合适呢?Poodle 是一个 Dog,但是 Poodle 不一定 obedient;那么 Poodle 是继承于 Dog 还是 Obedient Dog 呢?都不是,我们可以将驯服看作一个方面,将其应用到任何一类驯服的 Dog,我们反对以不恰当的方式强制将该行为放在 Dog 层次结构中。
4.与OOP面向对象编程的区别
AOP、OOP在字面上虽然非常类似,但却是面向不同领域的两种设计思想。OOP(面向对象编程)针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。 而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。这两种设计思想在目标上有着本质的差异。 上面的陈述可能过于理论化,举个简单的例子,对于“雇员”这样一个业务实体进行封装,自然是OOP/OOD的任务,我们可以为其建立一个“Employee”类,并将“雇员”相关的属性和行为封装其中。而用AOP设计思想对“雇员”进行封装将无从谈起。 同样,对于“权限检查”这一动作片断进行划分,则是AOP的目标领域。而通过OOD/OOP对一个动作进行封装,则有点不伦不类。
换而言之,OOD/OOP面向名词领域,AOP面向动词领域。
5.AOP 的基本概念
在面向对象编程中, 类,对象, 封装,继承,多态 等概念是描述面向对象思想主要术语。与此类似,在面向方面编程中,同样存在着一些基本概念: 联结点(JointPoint) :一个联结程序执行过程中的一个特定点。典型的联结点有:调用一个方法;方法执行这个过程本身;类初始化;对象初始化等。联结点是 AOP 的核心概念之一,它用来定义在程序的哪里通过 AOP 加入新的逻辑。 切入点(Pointcut) :一个切入点是用来定义某一个通知该何时执行的一组联结点。通过定义切入点,我们可以精确地控制程序中什么组件接到什么通知。上面我们提到,一个典型的联结点是方法调用,而一个典型的切入点就是对某一个类的所在方法调用的集合。通常我们会通过组建复杂的切入点来控制通知什么时候被执行。 通知(Advice) :在某一个特定的联结点处运行的代码称为“通知”。通知有很多种,比如 在联结点之前执行的前置通知(before advice)和在联结点之后执行的后置通知(after advice) 。 方面(Aspect) :通知和切入点的组合叫做方面,所以,方面定义了一段程序中应该包括的逻辑,以及何时应该执行该逻辑。 织入(Weaving) :织入是将方面真正加入程序代码的过程。对于静态 AOP 方案而言,织入是在编译时完成的,通常是在编译过程中增加一个步骤。类似的,动态 AOP 方案则是在程序运行是动态织入的。 目标(Target) :如果一个对象的执行过程受到某一个 AOP 的修改,那么它就叫一个目标对象。目标对象通常也称为被通知对象。 引入(Introduction) : 通过引入,可以在一个对象中加入新的方法或属性,以改变它的结构,这样即使该对象的类没有实现某一个接口,也可以修改它,使之成为该接口的一个实现。
静态和动态:静态 AOP 和动态 AOP 两者之间的区别主要在于什么时间织入,以及如何织入。最早的 AOP 实现大多都是静态的。在静态 AOP 中,织入是编译过程的一个步骤。用Java 的术语说,静态 AOP 通过直接对字节码进行操作,包括修改代码和扩展类,来完成织入过程。显然,这种办法生成的程序性能很好,因为最后的结果就是普通的 Java 字节码,在运行时不再需要特别的技巧来确定什么时候应该执行通知。这种方法的缺点是,如果想对方面做什么修改,即使只是加入一个新的联结点,都必须重新编译整个程序。AspectJ 是静态 AOP 的一个典型例子。与静态 AOP 不同,动态 AOP 中织入是在运行时动态完成的。织入具体是如何完成的,各个实现有所不同。Spring AOP 采取的方法是建立代理,然后代理在适当的时候执行通知。动态 AOP 的一个弱点就在于,其性能一般不如静态 AOP。而动态AOP 的主要优点在于可以随时修改程序的所有方面,而不需重新编译目标。
5.1 横切技术
“横切”是AOP的专有名词。它是一种蕴含强大力量的相对简单的设计和编程技术,尤其是用于建立松散耦合的、可扩展的企业系统时。横切技术可以使得AOP在一个给定的编程模型中穿越既定的职责部分(比如日志记录和性能优化)的操作。 如果不使用横切技术,软件开发是怎样的情形呢?在传统的程序中,由于横切行为的实现是分散的,开发人员很难对这些行为进行逻辑上的实现或更改。例如,用于日志记录的代码和主要用于其它职责的代码缠绕在一起。根据所解决的问题的复杂程度和作用域的不同,所引起的混乱可大可小。更改一个应用程序的日志记录策略可能涉及数百次编辑——即使可行,这也是个令人头疼的任务。 在AOP中,我们将这些具有公共逻辑的,与其他模块的核心逻辑纠缠在一起的行为称为“横切关注点(Crosscutting Concern)”,因为它跨越了给定编程模型中的典型职责界限。
5.2 横切关注点
一个关注点(concern)就是一个特定的目的,一块我们感兴趣的区域,一段我们需要的逻辑行为。从技术的角度来说,一个典型的软件系统包含一些核心的关注点和系统级的关注点。举个例子来说,一个信用卡处理系统的核心关注点是借贷/存入处理,而系统级的关注点则是日志、事务完整性、授权、安全及性能问题等,许多关注点——即横切关注点(crosscutting concerns)——会在多个模块中出现。如果使用现有的编程方法,横切关注点会横越多个模块,结果是使系统难以设计、理解、实现和演进。AOP能够比上述方法更好地分离系统关注点,从而提供模块化的横切关注点。 例如一个复杂的系统,它由许多关注点组合实现,如业务逻辑、性能,数据存储、日志和调度信息、授权、安全、线程、错误检查等,还有开发过程中的关注点,如易懂、易维护、易追查、易扩展等,
1 .由不同模块实现的一批关注点组成一个系统,即把模块作为一批关注点来实现,如图:
通过对系统需求和实现的识别,我们可以将模块中的这些关注点分为:核心关注点和横切关注点。对于核心关注点而言,通常来说,实现这些关注点的模块是相互独立的,他们分别完成了系统需要的商业逻辑,这些逻辑与具体的业务需求有关。而对于日志、安全、持久化等关注点而言,他们却是商业逻辑模块所共同需要的,这些逻辑分布于核心关注点的各处。在AOP中,诸如这些模块,都称为横切关注点。应用AOP的横切技术,关键就是要实现对关注点的识别。
2 .识别关注点
如果将整个模块比喻为一个圆柱体,那么关注点识别过程可以用三棱镜法则来形容,穿越三棱镜的光束(指需求),照射到圆柱体各处,获得不同颜色的光束,最后识别出不同的关注点。
1 ). 关注点识别:三棱镜法则,如图所示:
上图识别出来的关注点中,Business Logic属于核心关注点,它会调用到Security,Logging,Persistence等横切关注点。
代码语言:javascript复制 public class BusinessLogic {
public void SomeOperation() {
//验证安全性;Securtity关注点;
//执行前记录日志;Logging关注点;
DoSomething();
//保存逻辑运算后的数据;Persistence关注点;
//执行结束记录日志;Logging关注点;
}
}
3. 将横切关注点织入到核心关注点中
AOP的目的,就是要将诸如Logging之类的横切关注点从BusinessLogic类中分离出来。利用AOP技术,可以对相关的横切关注点封装,形成单独的“aspect”。这就保证了横切关注点的复用。由于BusinessLogic类中不再包含横切关注点的逻辑代码,为达到调用横切关注点的目的,可以利用横切技术,截取BusinessLogic类中相关方法的消息,例如SomeOperation()方法,然后将这些“aspect”织入到该方法中。将横切关注点织入到核心关注点中,如图:
通过利用AOP技术,改变了整个系统的设计方式。在分析系统需求之初,利用AOP的思想,分离出核心关注点和横切关注点。在实现了诸如日志、事务管理、权限控制等横切关注点的通用逻辑后,开发人员就可以专注于核心关注点,将精力投入到解决企业的商业逻辑上来。同时,这些封装好了的横切关注点提供的功能,可以最大限度地复用于商业逻辑的各个部分,既不需要开发人员作特殊的编码,也不会因为修改横切关注点的功能而影响具体的业务功能。
6.AOP 实践
6.1 JAVA实践
代码语言:javascript复制在 WEB 程序开发中,我们知道由于 HTTP 协议的无状态性,我们通常需要把用户的状态信息保存在 Session 中。在一些应用场景中,需要用户必须登录,才能继续操作。 传统实现方法 : 为此我们在进行每个业务操作之前,传统的实现方法会加入以下的逻辑:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws
ServletException, IOException {
HttpSession session = request.getSession();
if(session.getAttribute("user")==null){
request. getRequestDispatcher("login.jsp").forward(req,resp);
}
doSpecialBussinessLogic();
以这种方法实现的逻辑,要求程序员在应该实现登录检查的地方,都按以上的方法进行。这必然引起了代码的大量重复和混乱。在这里登录检查逻辑是一个非主要逻辑,而我们的主逻辑是doSpecialBussinessLogic(),主要逻辑和非主要逻辑的混乱是传统编程方法的一个主要局限。 用 AOP技术实现: AOP的出现,为以上问题提供了一个很好的解决方案。下面是用Aspectj 完成的登录检查逻辑的实现:
代码语言:javascript复制 public aspect LoginCheckAOP {
pointcut loginCheck(HttpServletRequest req, HttpServletResponse resp): (execution(void
*..*Action.doPost(HttpServletRequest,
HttpServletResponse))) && args(req,resp);
public before(HttpServletRequest req, HttpServletResponse resp) : loginCheck (req,resp) {
HttpSession session = request.getSession();
if(session.getAttribute("user")==null){
request. getRequestDispatcher("login.jsp").forward(req,resp);
}
}
我们定义了一个名字为LoginCheckAOP的方面,Aspectj的编译器通过名字匹配自动把登录检查逻辑的代码插入到需要的地方。 使用 AOP 方法进行登录检查比在需要的地方人工的插入检查代码有以下几条好处。 • 只需要在一个(LoginCheckAOP 方面中)地方放置所有的需要用于检查的功能代码。 • 插入和删除检查代码是很容易的。可以轻易地重新实现不同的检查方面,而不用对其它代码进行修改。 • 在任何需要的地方登录检查,即使增加了新方法或新类。这可以消除人为的错误。同时知道所有登录检查代码被删除了,并且当我们从构建配置中删除方面时不会忽略 任何东西。 • 有一个可重复使用的方面,它可以被应用和升级。
6.2 PHP实践
代码语言:javascript复制目前的PHP来说,还没有一个完整的AOP内置实现,虽然出现了RunKit,但一直都以BETA的状态呆在PECL项目里,估计很长时间内不太可能成为PHP的缺省设置。那是不是AOP在PHP里就破灭了呢?当然不是,因为我们有__get(),__set(),__call()等魔术方法,合理使用这些方法可以为我们实现某种程度的“准AOP”能力,之所以说是准AOP,是因为单单从实现上来看,称其为AOP有些牵强,但是从效果上来看,又部分实现了AOP的作用,虽然其实现方式并不完美,但对于一般的使用已经足够了。
<?php
/**
* 应用程序中某个业务逻辑类
*
*/
class Target
{
public function foobar(){
echo '业务逻辑<br />';
}
}
//业务逻辑类的包装类
class AOP
{
private $instance;
public function __construct($instance) {
$this->instance = $instance;
}
public function __call($method, $param) {
if(! method_exists($this->instance, $method)) {
throw new Exception("Call undefinded method ".get_class($this->instance)."::$method");
}
//前增强
$this->before();
$callBack = array($this->instance, $method);
$return = call_user_func_array($callBack, $param);
$this->after();
return $return;
}
/**
* 前增强
*
*/
public function before() {
echo '权限检查<br />';
}
/**
* 后增强
*
*/
public function after() {
echo '日志记录<br />';
}
}
/**
* 工厂方法
*
*/
class Factory
{
public function getTargetInstance(){
return new AOP(new Target());
}
}
//客户端调用演示
header("Content-Type: text/html; charset=utf8");
try {
$obj = Factory::getTargetInstance();
$obj->foobar();
} catch(Exception $e) {
echo 'Caught exception: ', $e->getMessage();
}
利用php5内置的魔术方法__call来实现AOP,唯一的缺点是要在AOP类里面实例客户端对象,返回的是被AOP包装后的对象。因此像get_class会遇到麻烦。
比如说,客户端通过getBizInstance()方法以为得到的对象是Target,但实际上它得到的是一个Target的包装对象AOP,这样的话,如果客户端进行一些诸如get_class()之类和对象类型相关的操作就会出错,当然,大多数情况下,客户端似乎不太会做类似的操作。
其实我们在代理模式也提到过,这其实就是一个动态代理模式。
我们用 runkit 扩展来实现方法调用拦截的例子:
/**
* 应用程序中某个业务逻辑类
*
*/
class Target
{
public function foobar(){
echo '业务逻辑<br />';
}
}
runkit_method_rename('Target', 'foobar', '#foobar');
runkit_method_add('Target','add','$a,$b','
echo "before calln";
$ret = $this->{"#foobar"}($a,$b);
echo "after calln";
return $ret;
');
也有人用了继承方式来实现:
<?php
//业务逻辑类的包装类
class AOP
{
private $instance;
public function __construct() {
}
public function __call($method, $param) {
if(strchr($method,'Aop_')){
$method = str_replace('Aop_','',$method);
if(! method_exists($this, $method)) {
throw new Exception("Call undefinded method ".get_class($this)."::$method");
}
}
//前增强
$this->before();
$callBack = array($this, $method);
$return = call_user_func_array($callBack, $param);
$this->after();
return $return;
}
/**
* 前增强
*
*/
public function before() {
echo '权限检查<br />';
}
/**
* 后增强
*
*/
public function after() {
echo '日志记录<br />';
}
}
/**
* 应用程序中某个业务逻辑类
*
*/
class Target extends AOP
{
public function foobar(){
echo '业务逻辑<br />';
}
}
//客户端调用演示
header("Content-Type: text/html; charset=utf8");
try {
$obj = new Target();
$obj->Aop_foobar();
} catch(Exception $e) {
echo 'Caught exception: ', $e->getMessage();
}
除了以上的实现方法,我们可以使用配置文件来配置把哪些关注点代码增强到目标对象的切入点上。
7.结论
面向方面编程是一个令软件开发人员激动的新技术, 它被用来寻找软件系统中新的模块化特性。面向方面编程是作为面向对象编程技术的一种补充而出现,它们之间并不存在竞争关系,实际上它们在软件开发中相辅相成,互为补充。面向方面编程作为一种崭新的编程技术,它具有十分光明的应用前景。