AOP = Proxy Pattern + Method Reflection + Aspect DSL + 自动代码生

  • Post author:
  • Post category:其他


学如逆水行舟,不进则退。

半年多来,居无常所,没有稳定的上网环境,这次抽空上来一看,又有好多陌生的名词漫天飞舞了。

落伍退化得很厉害。不过,我还能温故而知新,炒炒冷饭,写写基本的内容AOP。怀旧一番。

AOP = Proxy Pattern + Method Reflection + Aspect DSL + 自动代码生成

Declarative Programming & DSL

Aspect Oriented Programming(面向方面编程,简称AOP)是一种Declarative Programming(声明式编程)。

Declarative Programming(声明式编程)是和Imperative Programming(命令式编程)相对的概念。

我们平时使用的编程语言,比如C++、Java、Ruby、Python等,都属于Imperative Programming(命令式编程)。Imperative Programming(命令式编程)的意思是,程序员需要一步步写清楚程序需要如何做什么(How to do What)。

Declarative Programming(声明式编程)的意思是,程序员不需要一步步告诉程序如何做,只需要告诉程序在哪些地方做什么(Where to do What)。比起Imperative Programming(命令式编程)来,Declarative Programming(声明式编程)是在一个更高的层次上编程。Declarative Programming编程语言是更高级的语言。Declarative Programming通常处理一些总结性、总览性的工作,不适合做顺序相关的细节相关的底层工作。

如果说Imperative Programming是拼杀在第一线的基层工作人员,Declarative Programming就是总设计师、规则制定者。

Declarative Programming Language的概念,和Domain Specific Language(领域专用语言,简称DSL)的概念有相通之处。DSL主要是指一些对应专门领域的高层编程语言,和通用编程语言的概念相对。DSL对应的专门领域(Domain)一般比较狭窄,或者对应于某个行业,或者对应于某一类具体应用程序,比如数据库等。

最常见的DSL就是关系数据库的结构化数据查询语言SQL。同时,SQL也是一门Declarative Programming Language。SQL只需要告诉数据库,处理符合一定条件的数据,而不需要自己一步步判断每一条数据是否符合条件。SQL的形式一般是 select … where …,update … where …,delete … where …。

当然,这样一来,很多基层工作,SQL做不了。因此,大部分数据库都提供了另外的Imperative Programming Language,用来编写Stored Procedure(存储过程)等,以便处理一些更加细节的工作。

常见的DSL还有Rule Engine(规则引擎)语言、Workflow(工作流)语言等。Rule Engine和Workflow同时带有Imperative Programming和Declarative Programming的特点。

Rule Engine允许用户按照优先级定义一系列条件组合,并定义对满足条件的数据的处理过程。

Workflow也大致类似。Workflow把最基本的条件判断和循环语句的常见组合,定义为更加高级复杂的常用程序流程逻辑块。用户可以用这些高级流程块组合更加复杂的流程块,从而定义更加复杂的流程跳转条件。用户也可以定义当程序运行上下文满足一定条件的时候,应该做什么样的处理工作。

Rule Engine和Workflow的语言形式有可能是XML格式,也有可能是Ruby、Python、Javascript等脚本格式。我个人比较倾向于脚本格式,因为XML适合表达结构化数据,而不擅长表达逻辑流程。当然,XML格式的好处也是显而易见的。解析器可以很容易分析XML文件的结构,XML定义的条件或者程序流程都可以很方便地作为数据来处理。

介绍了Declarative Programming和DSL之后,我们来看本章题目表达的内容——AOP。

AOP是Declarative Programming,AOP语言也可以看作是DSL。AOP语言对应的专门领域(Domain)就是程序结构的方方面面(Aspect),比如程序的类、方法、成员变量等结构,以及针对这些程序结构的通用工作处理,比如日志管理、权限管理、事务管理等。

AOP处理的工作内容一般都是这样的一些总结性工作:“我想让所有的数据库类都自动进行数据库映射”、“我想打印出所有业务类的工作流程日志”、“我想给所有关键业务方法都加上事务管理功能”、“我想给所有敏感数据处理方法都加上安全管理授权机制”等等。

下面我们介绍AOP的实现原理和使用方法。

AOP实现原理

AOP的实现原理可以看作是Proxy/Decorator设计模式的泛化。

我们先来看Proxy模式的简单例子。

Proxy {

innerObject; // 真正的对象

f1() {

// 做一些额外的事情

innerObject.f1(); // 调用真正的对象的对应方法

// 做一些额外的事情

}

}

在Python、Ruby等动态类型语言中,只要实现了f1()方法的类,都可以被Proxy包装。在Java等静态类型语言中,则要求Proxy和被包装对象实现相同的接口。动态语言实现Proxy模式要比静态语言容易得多,动态语言实现AOP也要比静态语言容易得多。

假设我们用Proxy包装了10个类,我们通过调用Proxy的f1()方法来调用这10个类的f1()方法,这样,所有的f1()调用都会执行同样的一段“额外的工作”,从而实现了“所有被Proxy包装的类,都执行一段同样的额外工作”的任务。这段“额外的工作”可能是进行日志记录,权限检查,事务管理等常见工作。

Proxy模式是可以叠加的。我们可以定义多种完成特定方面任务(Aspect),比如,我们可以定义LogProxy、SecurityProxy、TransactionProxy,分别进行日志管理、权限管理、事务管理。

LogProxy {

f1(){

// 记录方法进入信息

innerObject.f1();// 调用真正的对象的对应方法

// 记录方法退出信息

}

}

SecurityProxy {

f1(){

// 进行权限验证

innerObject.f1();// 调用真正的对象的对应方法

}

}

TransactonProxy {

f1(){

Open Transaction

innerObject.f1();// 调用真正的对象的对应方法

Close Transaction

}

}

根据AOP的惯用叫法,上述的这些Proxy也叫做Advice。这些Proxy(or Advice)可以按照一定的内外顺序套起来,最外面的Proxy会最先执行。

包装f1()方法,也叫做截获(Intercept)f1()方法。Proxy/Advice有时候也叫做Interceptor。

看到这里,读者可能会产生两个问题。

问题一:上述代码采用的Proxy模式只是面向对象的特性,怎么会扯上一个新概念“面向方面(AOP)”呢?

问题二:Proxy模式虽然避免了重复“额外工作”代码的问题,但是,每个相关类都要被Proxy包装,这个工作也是很烦人。AOP Proxy如何能在应用程序中大规模使用呢?

下面我们来解答着两个问题。

对于问题一,我们来看一个复杂一点的例子。

假设被包装对象有f1()和f2()两个方法都要被包装。

RealObject{

f1() {…}

f2() {…}

}

这个时候,我们应该如何做?难道让Proxy也定义f1()和f2()两个方法?就象下面代码这样?

Proxy {

innerObject; // 真正的对象

f1() {

// 做一些额外的事情

innerObject.f1(); // 调用真正的对象的对应方法

// 做一些额外的事情

}

f2() {

// 做一些额外的事情

innerObject.f2(); // 调用真正的对象的对应方法

// 做一些额外的事情

}

}

这样做有几个不利之处。一是会造成代码重复,Proxy的f1()和f2()里面的“做一些额外的事情”代码重复。二是难以扩展,被包装对象可能有多个不同的方法,不同的被包装对象需要被包装的方法也可能不同。

现在的问题就变成,“Proxy如何才能包装截获任何类的任何方法?”

答案呼之欲出。对,就是Reflection。Java、Python、Ruby都支持Reflection,都支持Method(方法)对象。那么我们就利用Method Reflection编写一个能够解惑任何类的任何方法的Proxy/Advice/Interceptor。

MethodInterceptor{

around( method ){

// 做些额外的工作

method.invoke(…); // 调用真正的对象方法

// 做些额外的工作

}

}

上述的MethodInterceptor就可以分别包装和截获f1()和f2()两个方法。

这里的method参数就是方法对象,在Java、Ruby等面向对象语言中,需要用Reflection获取方法对象。这个方法对象就相当于函数式编程的函数对象。

在函数式编程中,函数对象属于“一等公民”,函数对象的获取不需要经过Reflection机制。所以,函数式编程对AOP的支持,比面向对象编程更好。

由此我们看到,AOP对应的问题领域确实超出了OOP的力所能及的范围。OOP只能处理同一个类体系内的同一个方法签名的截获和包装工作,一旦涉及到一个类的多个不同方法,或者多个不同类体系的不同方法,OOP就黔驴技穷,无能为力了。

使用Method Reflection的方式截获任何方法对象,是AOP的常用实现手段之一。另一个常见手段就是自动代码生成了。这也回答了前面提出的问题二——如何在应用系统中大规模使用AOP。

Proxy Pattern + Method Reflection + 自动代码生成 这样一个三元组合,就是AOP的基本实现原理。

Proxy Pattern 和 Method Reflection,前面已经做了阐述,下面我们来讲解自动代码生成。

首先,AOP需要定义一种Aspect描述的DSL。Aspect DSL主要用来描述这样的内容:“用TransactionProxy包装截获business目录下的所有类的公共业务方法”、“ 用SecurityProxy包装截获所有Login/Logout开头的类的所有公共方法”、“用LogProxy包装截获所有文件的所有方法”等等。

Aspect DSL的形式有多种多样。有的是一种类似Java的语法,比如AspectJ;有的是XML格式或者各种脚本语言,比如,Spring AOP等。

有了Aspect DSL,AOP处理程序就可以生成代码了。AOP生成代码有三种可能方式:

(1)静态编译时期,源代码生成。为每个符合条件的类方法产生对应的Proxy对象。AspectJ以前就是这种方式。

(2)静态编译时期,处理编译后的字节码。Java、Python之类的虚拟机语言都有一种中间代码(Java的中间代码叫做字节码),AOP处理程序可以分析字节码,并直接产生字节码形式的Proxy。这种方式也叫做静态字节码增强。AspectJ也支持这种方式。Java有一些开源项目,比如ASM、Cglib等,可以分析并生成Java字节码。这些开源项目不仅可以静态分析增强字节码,还可以在程序运行期动态分析增强字节码。很多AOP项目,比如Spring AOP,都采用ASM/Cglib处理字节码。

(3)动态运行时期,即时处理装载到虚拟机内部的类结构字节码。这也叫做动态增强。比如,Spring AOP。如前所述,Spring AOP使用ASM/Cglib之类的处理字节码的开源项目。Java运行库本身也提供了类似于ASM/Cglib的简单的动态处理字节码的API,叫做Dynamic Proxy。

以上就是AOP的实现原理:Proxy Pattern + Method Reflection + Aspect DSL + 自动代码生成。

总体来说,实现AOP的便利程度,函数式编程语言 > 动态类型语言 > 静态类型语言。当然,这个不等式并不是绝对的。有些动态类型语言提供了丰富强大的语法特性,实现AOP的便利程度,可能要超过函数式编程语言。

DSL & Parser

本章前面还提到了DSL。SQL、Rule Engine、Workflow、Aspect DSL都属于DSL。

DSL有可能是XML格式,也有可能是脚本格式。XML的解析比较容易,但是,XML不适合表达逻辑。脚本格式表达逻辑很方便灵活,但是,解析上就没有XML那么容易。

DSL不是通用编程语言,语法一般比较简单,没有通用编程语言那么复杂。很多DSL提供商都自己定义语法。一些程序设计大师甚至提出Language Oriented Programming(面向语言编程,简称LOP)的激进观点,认为每一个应用程序都可能需要一套独立的自定义DSL。这个观点提出来之后,跟风者众。业界掀起自定义DSL的狂潮。

从特定角度来看,这个观点是有一定道理的。一般来说,每一个应用程序都需要一套特定功能的公用函数库。DSL的目的正好就是处理特定领域的特定问题。从这个意义上来说,特定功能公用函数库和DSL的作用是一致的。DSL可以代替特定功能公用函数库的作用。DSL定义的关键字就相当于公用函数库的API函数名。

DSL关键字比起API函数名的优势在于可读性好。

比如,用DSL(比如SQL)我们可以写,select * from …. where a = 1

用API函数我们就要写,Query.find( tableName, new NumberEqualCriteria(“a”, 1) )

两者的可读性优劣一目了然。

而且,DSL一般都是解释执行,不需要参与源程序编译,这使得DSL程序(相当于API函数的调用程序)很容易修改和配置。

但DSL的实现不如编写API函数那么容易,尤其是脚本格式的DSL,程序员需要解析并执行DSL脚本,需要实现一个小型解释器。

对于很简单的语法,可以利用动态类型语言的高级特性,实现DSL脚本语句到函数方法调用的映射。比如,Open Transaction这条DSL语句经过简单的分析处理,可以映射为函数调用Transaction.open()。

复杂一些的DSL语法就需要用到专门的Parser(解析器)了。看过编译原理知识的读者,应该还有印象:Parser(解析器)是编译器的重要组成部分,Parser可以分为字符串分析、词法分析、语法分析、语义分析等部分,Parser是一个相当复杂的程序。

如果每一套自定义DSL都需要程序员从头开始重新写一个Parser,代价很大,得不偿失。幸亏有一些辅助产生Parser的应用程序,可以帮助生成Parser。

Parser产生器可以分为两种:

(1)Parser Generator。比如,antlr、javacc、yacc等项目。程序员定义BNF格式的语法说明,然后用Parser Generator根据BNF语法说明,产生对应的Parser程序。BNF格式是类似于这样的一种表达方式:a ::= b & c; d ::= e | f。有兴趣的读者可以查阅BNF的具体定义。Parser Generator的优点在于BNF语法简单通用,缺点在于需要静态生成Parser源代码。

(2)Parser Combinator。代表作是Haskell语言编写的一个叫做Parsec的项目。Parser Generator提供了基本的字符串解析器、词法分析器等,还提供了与或非、循环、选择、提前几步判断等语法组合子。

一个叫做ajoo的程序员把Parsec翻译成Java、c#.net、Ruby等多种语言版本的开源项目。这些项目把各种高级编程概念体现得淋漓尽致,是很好的学习和研究资料。

—————————–

多谢大家的挂念,上次庄表伟给我发了个短信,告诉我 javaeye有发贴提到我,当时上网不便。

这次进到信箱,看到一些信息。还有些是询问一些问题,关于我以前提到的 portal什么的。关于那些技术问题,我感到汗颜。