面向切面编程

原文

术语

编程范式

Programming_paradigm

横切关注点

Cross-cutting_concern

也被称为水平关注点,特定是在程序中横跨多个抽象(关注点)。

增强

Advice

连接点

Join Point

连接点准确的说是提供了增强什么时候可以运行的方式,连接点可以是一个抽象的运行概念,也可以是一个抽象的模型概念,关键是连接点是可监控的,这样才能提供一个合适的时机在连接点被操作时候,AOP进行增强代码的执行。

切入点

Pointcut

切入点是指一系列的连接点

切入点是指定哪些代码被修改,这里的修改不是修改已有代码本身,而是在切入点指定的地方新增一些额外的行为,而不破坏原有代码的封闭

拦截器

切面编织器

aspect weaver

部署时

Deploy-time

反射[Reflection]

reflection is the ability of a process to examine, introspect, and modify its own structure and behavior.

反省[introspect]

In computing, type introspection is the ability of a program to examine the type or properties of an object at runtime. Some programming languages possess this capability.

混合

在面向对象编程语言中,mix In(或mixin)是一个类,它包含供其他类使用的方法,而不必是这些其他类的父类。其他类如何访问mixin的方法取决于语言。mixin有时被描述为“包含”而不是“继承”。
mixin鼓励代码重用,并可用于避免多重继承可能导致的继承歧义(即“菱形问题”),或用于解决语言中对多重继承缺乏支持的问题。mixin也可以看作是一个带有实现方法的接口。此模式是实施依赖关系反转原则的一个示例。

mixin是一个允许程序员将一些代码注入类的语言概念。Mixin编程是一种软件开发风格,在一个类中创建功能单元,然后与其他类混合
mixin类充当父类,包含所需的功能。然后,子类可以继承或简单地重用此功能,但不能作为专门化的手段。通常,mixin会将所需的功能导出到子类,而不会创建一个僵硬的“是”关系。这里是mixin和继承概念的重要区别,因为子类仍然可以继承父类的所有特性,但是,关于子类是父类的“一种”的语义不必应用。

正文

在计算机技术里面,面向切面编程(AOP)目标是通过允许横切[1]关注点分离增强模块性的一种编程范例。

通过向已经存在的代码而不修改代码本身添加额外的行为(增强),而是单独指定称为切入点指定的代码,例如,记录所有以'set'开头的函数名的函数调用。

这样可以让非业务核心逻辑(类似日志)的行为添加到程序内,却不会使核心功能的代码混乱。

AOP是面向切面软件开发的基础。

AOP包含了在源码层级的编程方法和支持关注点模块化的工具,同时”面向切面软件开发“是指一个完整的工程学科。

面向切面编程把程序逻辑分解成不同的部分(即所谓的关注点,功能的内聚域)。几乎所有的编程范式通过提供用于实现,抽象,组合这些关注点的抽象(例如,函数,过程,模块,类,方法)支持某种程度的关注点分组和封装从而成为单独的,独立的实体。某些关注点在程序中“横切”多个抽象,并违背了这些实现形式[2],这些关注点我们称之为横切关注点或者水平关注点

日志示例横切关注点,因为日志策略必然影响系统中每一个日志记录的部分。日志因此横切所有日志记录的类和方法。

所有的AOP通过在某个位置上封装每一个关注点某些横切表达式实现。它们之前的不同存在于构建提供的能力,安全,和易用性。

例如,拦截器使用方法来表达有限形式的横切,而不太支持类型安全和调试。

AspectJ 有许多这样的表达式,并且封装在一个特定的类中, aspect.

例如,一个 aspect.可以通过切入点(检测是否和给定的连接点匹配)量化和查询指定的各种连接点上(程序中的点)应用一个advice改变基础代码的行为(程序的非切面部分)。一个切面同样对其他类可以使用二级制兼容方式的结构修改,例如添加成员和父类。

动机和基本概念

通常来说,一个切面是分散的,缠绕的代码,从而难以理解和维护。

切面分散的原因是由于(类似日志)函数分布在很多不相关的使用了切面函数的函数中,以及可能不相关的系统中,不同的源语言中。这就意味着改变日志需要修改所有相关的模块。

切面不仅仅缠绕在系统所表达的主线功能中,同样切面之间也互相缠绕。这意味着修改一个关注点需要理解所有缠绕的关注点,同样需要一些方法推测会修改带来的影响。

例如,考虑一个概念上用来从一个账号向另一个账号转账的简单方法银行应用程序。

void transfer(Account fromAcc, Account toAcc, int amount) throws Exception {
  if (fromAcc.getBalance() < amount)
      throw new InsufficientFundsException();

  fromAcc.withdraw(amount);
  toAcc.deposit(amount);
}

然而,这个转移方法忽略考虑部署应用方面的要求:它缺少安全检查以验证当前的用户是否有权限进行这个操作;使用数据库事务封装这个操作阻止异常的数据丢失;为了问题诊断,应该向系统日志进行记录。

为了演示目的,一个包含所有这些关注点的版本,可能看起来像这样

void transfer(Account fromAcc, Account toAcc, int amount, User user,
    Logger logger, Database database) throws Exception {
  logger.info("Transferring money...");
  
  if (!isUserAuthorised(user, fromAcc)) {
    logger.info("User has no permission.");
    throw new UnauthorisedUserException();
  }
  
  if (fromAcc.getBalance() < amount) {
    logger.info("Insufficient funds.");
    throw new InsufficientFundsException();
  }

  fromAcc.withdraw(amount);
  toAcc.deposit(amount);

  database.commitChanges();  // Atomic operation.

  logger.info("Transaction successful.");
}

在这个例子中其他关注已经和基础功能缠绕(业务逻辑关注点)。事务,安全,日志记录都是举例说明横切关注点。

现在考虑下我们如果突然间我们需要调整应用安全的考虑事项。在程序当前的版本,安全相关的操作分散在许多的方法中,这种调整需要付出很大的努力。

AOP尝试允许程序员在切面这种单独的模块中表达切面关注点来解决这种问题。

切面包含增强(程序内连接特定的点的代码)和类型间声明(添加到其他类的结构性成员,即成员注入)。

例如,一个安全模块包含在访问银行账户之前执行安全检查的增强。

切入点定义可以访问银行账号的时间(join points),然后在增强体中定义安全检查如何实现的代码。

这样的话,检查和位置可以位置在一个地方。

进一步,好的切入点可以预测程序的调整,这样如果另一个开发者创建了一个访问银行账号的新方法,增强也将在它执行时候应用在这个新方法上。

这样我们在切面中实现上面的日志例子。

aspect Logger {
  void Bank.transfer(Account fromAcc, Account toAcc, int amount, User user, Logger logger)  {
    logger.info("Transferring money...");
  }

  void Bank.getMoneyBack(User user, int transactionId, Logger logger)  {
    logger.info("User requested money back.");
  }

  // Other crosscutting code.
}

你也可以认为AOP作为调试工具或者用户级别的工具。增强可以作为你无法修改的函数(用户级)或者不想在产品代码中修改函数的保留手段。

连接点模型

面向切面的语言中增强关联组件定义连接点模型(JPM),一个JPM定义三个事物:

  1. 什么时候增强可以运行。

    这些使用连接点进行定义,因为这些可以在正在运行的程序中有效的连接额外的行为。

    连接点必须是可寻址的,和易被普通程序员理解的才是有用的。

    同样连接点应该是不重要的程序调整中能保持稳定,从而让切面在这种变化中保持稳定。

    很多的AOP实现都支持方法执行和字段引用作为接入点。[3]

  2. 指定(量化)连接点的方式,也就是切入点

    切入点决定一个给定的连接点是否匹配。

    大部分有效的切入点语言使用类似基础语言的语法(例如,AspectJ使用java签名),并且可以通过重命名和组合进行复用

  3. 指定代码在连接点上运行的方式。AspectJ称这些代码为增强,并可以在连接点的前,后,周围运行它。有些实现同样支持类似在另一个类中定义切面的方法。

基于暴露的连接点,连接点如何指定的方式,连接点上允许的操作,和结构性增强来比较连接点模型。

AspectJ's join-point model

  • AspectJ的连接点包含方法和构造函数调用和执行,类和对象的初始化,字段的读取和写入,异常处理等等。

    连接点不包括循环,父类调用,抛出子句,多个语句等。

  • 切入点使用PCD(基础切入点指示符)来指定

    “类型”PCD匹配某类特定类型的连接点(例如,方法执行),并且倾向于java签名作为输入。

     execution(* set*(*))
    

    如果方法名以'set'开头并且具有任意类型的一个参数,这个切入点匹配一个方法执行的连接点。

    “动态的”基础切入点指示符检查运行时类型和绑定变量,例如

     this(Point)
    

    这个切入点匹配当前执行的对象是一个Point的实例。注意类的非限定名可以通过java的普通类型查找来使用。

    “范围”基础切入点指示符限制连接点的词法范围,例如

    within(com.company.*)
    

    这个切入点匹配com.company包下的任意类型的任意连接点。*是一种用来匹配一个签名的许多内容的通配符形式。

    切入点可以组合和重命名成一个新的切入点进行重用。例如

     pointcut set() : execution(* set*(*) ) && this(Point) && within(com.company.*);
    

    这个切入点匹配一个执行方法的连接点,只要方法符合'set'方法名开头,并且this是属于com.company包下的一个Point的实例类型。

    这个切入点可以通过set()名进行引用。

  • 增强指定在(前,后,周围)某个连接点(通过切入点指定)运行特定的代码(在方法中指定代码)。

    AOP运行时当切入点匹配了连接点的时候自动的调用增强。

    例如:after() : set() { Display.update(); }

    这实际上指明:如果set()切入点匹配了连接点,在连接点完成之后运行Display.update()代码。

Other potential join point models

还有其他类型的JPM。所有增强语言都可以依据他们的JPM进行定义。

例如,一种假设的UML切面语言也许有以下的JPM:

  • 所有的模型元素都是连接点
  • 切入点是一些结合模型元素的布尔表达式
  • 影响这些点的方式是所有匹配连接点的可视化

类型间声明

类型间声明提供一个表达横切关注点影响模块结构的方式。

也被称作开放类扩展方法,这使得程序员可以在一个地方声明另一个类中成员或者父类,通常是为了合并切面内与关注点相关的所有代码。

例如,如果一个程序员使用访问者实现了一个更新显示的横切关注点,在AspectJ中一个类型间声明使用visitor pattern 可能像下面这样:

aspect DisplayUpdate {
    void Point.acceptVisitor(Visitor v) {
      v.visit(this);
    }
    // other crosscutting code...
  }

这个代码片段在Point类中添加acceptVisitor方法。

对于任何结构性的添加都需要和原来的类兼容,这样已有类的的客户端才能继续运行,除非AOP可以实现随时对客户端的控制。

Implementation

AOP通过两种不同的方式影响其他程序,取决于底层语言和环境:

  1. 生成一个组合的程序,在原语言上是有效的(可以理解成语言之间是可以互相连接的),并且对于最终的解释器就像普通的程序一样没有区别。
  2. 最终的解释器或者环境更新成理解和实现AOP特征。

修改环境的难度意味着大部分通过称为编织(一种特殊的程序转换特例)的过程实现实现生产兼容的组合程序(即通常使用第一种方式)。

aspect weaver 读取面向切面的代码,然后生成恰当的集成切面的面向对象的代码。

同一种AOP语言可以通过多种编织方法实现,所以不应该根据编织的实现去理解语言的语义。

使用何种方法组合仅仅影响实现的速度和部署的难易程度

系统可以通过预处理器,这通常需要访问程序的源代码,来实现源码级别的编织(像C++最初是在 CFront中实现)。然而,Java良好的二进制形式支持字节码编织器通过.class-file形式和所有的Java程序有效工作。

字节码编织器可以在构建过程中部署,或者如果编织模型是针对单个类,则在类加载的时候也可以进行部署

AspectJ在2001开始源码级别的编织,在2002年交付了单个类字节码编织器,然后在2005年整合了AspectWerkz 后提供了高级的加载时候支持。

任何在运行时组合程序的解决方案需要提供正确的隔离程序的视图以保持程序员的隔离模型[4]。

java对多个源文件的字节码支持使得调试器可以在源码编辑器中单步遍历一个正确的编织的class文件。

然而,一些第三方的反编译器不能处理编织代码,因为这些反编译器预期这些代码是通过javac生成的而不是所有的字节码形式。

部署时编织提供了另一种解决方案。这意味着后处理,而不是在生成的代码中打补丁,这种编织方案子类化已有类以便通过方法重载引入修改。已有类即使在运行时依然保持原封不动的,现在的所有工具(调试器,分析器,等)都可以在开发期间使用。类似的方案已经在许多Java EE应用服务器中,比如IBM的WebSphere中得到验证。

术语

Cross-cutting concerns

虽然面向对象模型的大部分类都执行单一的,特定的功能,但是它们常常会公用其他类通用的,次要的需要。

例如,我们需要在数据访问层中的类添加日志,或者任意时候进入或者退出UI层中类。

进一步关注可能是安全相关的问题,例如访问控制或者信息流控制。即使每个类都有不同的主要功能,但是执行次要功能的代码通常是相同的。

Advice

你想应用在现有模型上的额外代码。在我们的例子中,我们想应用在任何时候当线程进入和退出一个方法时的日志代码。

Pointcut

需要应用横切关注点的应用程序内对执行点的术语。在我们例子中,当线程进入一个方法时候,会达到一个切入点,当线程退出一个方法时候会到达另一个切入点。

Aspect

切入点和增强的组合称为切面。在我们的例子中,我们再应用程序中通过定义切入点和给出正确的增强添加了一个日志的切面。

与其他编程范式的比较

切面产生于面向对象编程和反射.AOP语言功能上类似元对象协议,但是比它更受限制。

切面和编程概念(如主题,混合,委托)紧密相关。

其他使用面向切面编程范式的方式包括合成过滤器和超切片方案。

至少从1970开始,开发者已经使用拦截和分发补丁的方式看起来像AOP中实现方法,但是这些都没有横切规范提供的语义编写在一个地方。

设计人员已经考虑了其他方式实现代码的分离,例如C#的部分类型,但是这种方案缺少一个量化机制允许使用一个声明语句触达代码的多个连接点。

虽然这看起来在测试中和AOP无关,但是mock和stub需要使用AOP技术,比如围绕增强。这里协作对象(mock,stub对象)是了测试,一个横切的关注点。因此,各种模拟对象框架提供了这些特征。

例如,调用服务获取余额的流程。在测试流程中,数据的来源并不重要,重要的是根据需求使用余额的流程才最重要。

选定问题

程序员需要能够阅读代码并理解发生了什么,以防止错误。即使有适当的培训,如果没有对程序的静态结构和动态流程提供适当的可视化支持,那对于横切关注点的理解也是十分困难的。

从2002年,AspectJ开始提供IDE插件用来支持横切关注点的可视化。这些特性,在切面的代码辅助和重构上都十分常见了。

考虑到AOP的强大功能,如果程序员在表达横切时犯了逻辑错误,可能会导致大面积的程序失败。相反,另一个程序员可能会改变程序中的连接点——例如,通过重命名或移动方法——以切面编写者没有预料到的方式,从而产生不可预见的后果。

模块化横切关注点的一个优点是使一个程序员能够轻松地影响整个系统;因此,这类问题表现为两个或多个开发人员对给定故障的责任冲突。然而,在AOP的存在下,这些问题的解决方案可以更容易,因为只需要改变方面,而没有AOP的相应问题会更广泛地展开(即蔓延在程序的各处,无法进行统一集中的处理)。

批评

对于AOP影响最基本的批评是使控制流变的费解,这个比饱受诟病的GOTO语句更糟糕,实际上这个十分类似于COME FROM这个笑话语句。

应用的一无所知,这个是许多AOP定义的基础,意味相对于显示的方法调用,增强是不可见的。例如,对于这个COME FROM程序:

 5 INPUT X
10 PRINT 'Result is :'
15 PRINT X
20 COME FROM 10
25      X = X * X
30 RETURN

对应的相似语义的AOP片段

定义了一个around()的切入点以及对应的增强代码

main() {
    input x
    print(result(x))
}
input result(int x) { return x }
around(int x): call(result(int)) && args(x) {
    int temp = proceed(x)
    return temp * temp
}

实际上,切入点可能依赖于运行时条件,因此不是静态确定的。这可以通过静态分析和IDE支持显示哪些增强可能匹配来缓解,但无法解决。

一般的批评是,AOP旨在改进“模块性和代码结构”,但也有人反驳说,AOP反而破坏了这些目标,阻碍了“程序的独立开发和可理解性”。具体来说,通过切入点进行量化打破了模块性:“总的来说,必须,有完整的程序知识来解释面向切面程序的动态执行。”此外,虽然它的目标(模块化横切关注点)是很好理解的,它的实际定义尚不清楚,与其他成熟的技术也没有明显的区别。交叉关注点可能相互交叉,需要一些解决机制,如排序。事实上,方面可以应用于自身,导致问题,如说谎者悖论。

技术批评包括,切入点的量化(定义执行增强的位置)对程序中的更改“极其敏感”,这就是所谓的脆弱切入点问题。切入点的问题被认为是棘手的:如果用显式注释代替切入点的量化,相反,我们得到的是面向属性的编程,这只是一个显式的子程序调用,并且遭受了与AOP设计用来解决的相同的分散问题。

附录

[1]横切:横切是横向展开的一段逻辑的切除,这段逻辑是具有平行的特性,是可以平行切除的横断面,从逻辑上不影响模块的主体功能,不是纵向贯穿的功能逻辑。

[2]这里说的违背了这些实现形式是什么意思呢?

例如一个日志类,可以封装成单独的,独立的实体,但是这个实体会在多个抽象中被调用,这种方式,和其他独立的实体在业务逻辑中调用有什么区别呢?

首先日志类的实现形式可能不是基于抽象方式的实现,其实日志类不是核心逻辑。

是看是否是核心逻辑,即使满足实现形式,作为核心逻辑的调用,是业务连接的基础,而横切的关注点并非是业务连接的基础,是可以进行水平剥离。

[3]方法执行:

字段引用:

[4]这意味着程序员的代码和切面的代码是需要有不同的视图进行展现,并且在源代码组织上也是需要有不同的视图进行呈现,这样已有的模型和AOP的模型的隔离便于开发和维护

你可能感兴趣的:(面向切面编程)