理解面向切面编程AOP思想

@wanqiuz 2018-05-19 14:12:29发表于 wanqiuz/blog-articles

1. 什么是AOP

定义:以非侵入的方式将系统级的功能代码切入到目标类的指定方法、指定字段上的编程思想就是面向切面的编程。将代码织入到目标类的过程,在编译器、类加载期或运行期均可以实现。

2. AOP理解1

面向切面编程(AOP是Aspect Oriented Programming的首字母缩写) ,我们知道,面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用。
但是人们也发现,在分散代码的同时,也增加了代码的重复性。什么意思呢?比如说,我们在两个类中,可能都需要在每个方法中做日志。按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。
也许有人会说,那好办啊,我们可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。但是,这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类。那么,有没有什么办法,能让我们在需要的时候,随意地加入代码呢?这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。
一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。这样看来,AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。有了AOP,OOP变得立体了。如果加上时间维度,AOP使OOP由原来的二维变为三维了,由平面变成立体了。从技术上来说,AOP基本上是通过代理机制实现的。
AOP在编程历史上可以说是里程碑式的,对OOP编程是一种十分有益的补充。

3. AOP理解2

Spring中IoC和AOP这两个缩写总是一起出现。在形式上,两者同为三个字母的缩写,而且第二个字母都是O,有对仗美;在性质上,两者同为Spring的核心技术和特色,是最常被提起的概念。

但与面向切面编程AOP真正对应的,是OOP,即面向对象编程。
未说面向切面,先说面向过程。

面向对象侧重静态,名词,状态,组织,数据,载体是空间;
面向过程侧重动态,动词,行为,调用,算法,载体是时间;

这两者,运行于不同维度,本不互相冲突,理应携手合作,相互配合。

所以,web项目中的controller,service,dao等各层组件,有行为无状态,有方法无属性,即使有属性,也只是对下一层组件的持有;
所以,web项目中的entity,dto等各种实体,有状态无行为,有属性无方法,即使有方法,也只是getter/setter等,围着状态打转;

反倒是我们刚学「面向对象」时说的「既有眼睛又会叫」的小狗那种状态行为俱全的对象,基本见不到了。

程序需要状态,但对象不需要状态。
如果对象有了状态,就会引发烦人的多线程问题,在集群环境下更是麻烦。
程序的状态,统一由数据库,缓存,任务队列这些外部容器来容纳,在处理时,仅仅在对象的方法中以局部变量的面目偶尔出现,被封在线程内部,朝生夕灭,任由回收。

基于Java语言的web开发,本质是用面向对象的组织,面向过程的逻辑,来解决问题。应用实践中灵活具体,不拘泥,不教条。

但仍会遇到一种麻烦,即假如一个流程分三个步骤,分别是X,A,Y,另一个流程的三个步骤是X,B,Y。
写在程序里,两个方法体分别是XAY和XBY,显然,这出现了重复,违反了DRY原则。
你可以把X和Y分别抽成一个方法,但至少还是要写一条语句来调用方法,xAy,xBy,重复依然存在。

如果控制反转来处理这问题,将采用模板方法的模式,在抽象父类方法体中声明x?y,其中?部分为抽象方法,由具体子类实现。
但这就出现了继承,而且调用者只能调用父类声明的方法,耦合性太强,不灵活。
所以,我们常看到,只有那些本来就是调用者调用父类声明的方法的情况,比如表现层,或者本来就不用太灵活,比如只提供增删改查的持久层,才总出现抽象父类的身影。

具体Controller is-a 抽象Controller,具体Dao is-a 抽象Dao,这大家都能接受。
但除了在抽象Controller、抽象Dao中固定的步骤之外,我们就不需要点别的吗?
比如在某些Controller方法运行之前做点什么,在某些Dao方法运行之前之后做点什么?
而且最好能基于配置,基于约定,而不是都死乎乎硬编码到代码里。

这些需求,基本的编程手段就解决不了了。
于是乎,面向切面横空出世。
《Spring3.x企业应用开发实战》(下称《3.x》)第6章写道:
AOP是OOP的有益补充。
Spring实现的AOP是代理模式,给调用者使用的实际是已经过加工的对象,你编程时方法体里只写了A,但调用者拿到的对象的方法体却是xAy。

x和y总还是需要你来写的,这就是增强。
x和y具体在什么时候被调用总还是需要你来规定的,虽然是基于约定的声明这种简单的规定,这就是切点。
《EXPERT ONE ON ONE J2EE DEVELOPMENT WITHOUT EJB》第8章、《Spring实战》第4章:
增强(advice,另译为通知,但《3.x》作者不赞成):在特定连接点执行的动作。
切点(pointcut):一组连接点的总称,用于指定某个增强应该在何时被调用。
连接点(join point):在应用执行过程中能够插入切面的一个点。(我注:就是抽象的「切点」声明所指代的那些具体的点。)
切面(aspect):通知(即增强)和切点的结合。
其他概念不赘,如果有兴趣可以自行去翻书,我每次看到这些东西都很头大。

用人话说就是,增强是「干啥」,切入点是「啥时候干」。
生活中例子如端碗-吃饭-放筷子,端碗-吃面-放筷子,你只要定义好端碗和放筷子,并声明在吃点啥之前之后调用它们,业务方法只要实现吃饭、吃面就行了,以后想加个吃饺子也很方便。
生产中例子如事务、安全、日志(*),用声明的方式一次性配好,之后漫漫长夜专注于写业务代码就行了,不再为这些事而烦。
《Spring实战》第4章:
散布于应用中多处的功能(日志、安全、事务管理等)被称为横切关注点。
把横切关注点与业务逻辑分离是AOP要解决的问题。
*:但《Spring3.x企业应用开发实战》第6章说:
很多人认为很难用AOP编写实用的程序日志。笔者对此观点非常认同。(我注:我也认同)

总之,面向切面的目标与面向对象的目标没有不同。
一是减少重复,二是专注业务。
相比之下,面向对象是细腻的,用继承和组合的方式,绵绵编织成一套类和对象体系。
而面向切面是豪放的,大手一挥:凡某包某类某开头的方法,一并如斯处理!

《Javascript DOM编程艺术》说,dom是绣花针,innerHTML是砍柴斧。
我看面向对象和面向切面,也可做如是观。

没有依赖注入,面向切面就失去立足之本。
没有面向切面,依赖注入之后也只好在各个方法里下死力气写重复代码,或者搞出来一个超级复杂的抽象基类。
同时有了这两者,才真正能履行拆分、解耦、模块化、约定优于配置的思想,才真正能实现合并重复代码、专注业务逻辑的愿望。

不过,这面向切面不是Spring的专利,Java Web开发中最基本的Filter,就是一层一层的切面,突破了之后才能触及Servlet这内核。
但Filter过于暴力粗放,只能运行在Servlet之外而不能在之内,能上不能下,稍微细一点的批处理它就不行了,而Spring的AOP可以。
(Struts2的Intercepter也算,关于这就不多说了,如感兴趣可看《Struts2技术内幕》第8章Intercepter部分)
从理论上说,Filter和Spring AOP前者是责任链模式(Struts2 Intercepter也是),后者是代理模式,性质不同,但从「层层包裹核心」的共同特点看,是一致的。

所以无论是宽是窄,只要你遇到了「好多方法里都有重复代码好臭哇呀」的情况(关于代码的坏气味可以参考《重构》),而又无法应用策略、装饰器、模板方法等模式,就考虑AOP吧!
毕竟虽然Spring的书籍里讲到AOP就连篇累牍、名词繁多、配法多样、望而生畏,但具体写起来还是非常简单的。
(不过,如果能用「绣花针」OOP的设计模式实现,还是不建议轻易动用AOP这「劈柴刀」,不得已才用之。关于设计模式,推荐《Java与模式》一书)

4. 我的AOP理解

为了符合DRY(Don't repeat yourself )原则,减少重复代码。我们一般有几种方法:第一种,如果多个目标类有共同的基类,则在基类中用模板方法设计模式,将业务流程以模板的方式确定下来;第二种,是在全局用一个类来统一处理某事件,并在目标类中引用该全局类;以上两种都将直接侵入业务代码,第三种方法是用AOP的思想,以非侵入的方式将系统级的功能代码切入到目标类的指定方法、指定位置上。

参考并感谢: