依赖注入(DI)有助于应用对象之间的解耦,而面向切面编程(AOP)有助于横切关注点与所影响的对象之间的解耦(转good)

依赖注入(DI)有助于应用对象之间的解耦,而面向切面编程(AOP)有助于横切关注点与所影响的对象之间的解耦。所谓横切关注点,即影响应用多处的功能,这些功能各个应用模块都需要,但又不是其主要关注点,常见的横切关注点有日志、事务和安全等。

将横切关注点抽离形成独立的类,即形成了切面。切面主要由切点和通知构成,通知定义了切面是什么,以及何时执行何种操作;切点定义了在何处执行通知定义的操作。

http://ju.outofmemory.cn/entry/216839

引子: 
AOP(面向方面编程:Aspect Oriented Programing)和IoC一样是Spring容器的内核,声明式事务的功能在此基础上开花结果。但是AOP和OOP差别较大,要很好地理解这个概念,做到心领神会还是不容易的,不过相信看完帖子,你就不再迷惑了。 

编程语言最终极的目标就是能以更自然、更灵活的方式模拟世界,从原始机器语言到过程语言再到面向对象的语言,编程语言一步步地用更自然、更灵活的方式描述软件。AOP是软件开发思想发展到一定阶段的产物,但AOP的出现并不是要完全替代OOP,而仅是作为OOP的有益补充。虽然AOP作为一项编程技术已经有多年的历史,但一直长时间停留在学术领域,直到近几年,AOP才作为一项真正的实用技术在应用领域开疆扩土。需要指出的是AOP的应用场合是受限的,它一般只适合于那些具有横切逻辑的应用场合:如性能监测、访问控制、事务管理以及日志记录(虽然有很多文章用日志记录作为讲解AOP的实例,但很多人认为很难用AOP编写实用的程序日志,笔者对此观点非常认同)。不过,这丝毫不影响AOP作为一种新的软件开发思想在软件开发领域所占有的地位。

AOP到底是什么

AOP是Aspect Oriented Programing的简称,最初被译为“面向方面编程”,这个翻译向来为人所诟病,但是由于先入为主的效应,受众广泛,所以这个翻译依然被很多人使用,但我们更倾向于用“面向切面编程”的译法,因为它更加达意。

按照软件重构思想的理念,如果多个类中出现相同的代码,应该考虑定义一个共同的抽象类,将这些相同的代码提取到抽象类中。比如Horse、Pig、Camel这些对象都有run()、eat()的方法,通过引入一个包含这两个方法抽象的Animal父类,Horse、Pig、Camel就可以通过继承Animal复用到run()和eat()的方法。通过引入父类消除多个类中重复代码的方式在大多情况下是可行的,但世界并非永远这样简单,请看下面论坛管理业务类的代码:

代码清单6-1  ForumService

Java代码  

  1. package com.baobaotao.concept;
  2. public class ForumService {
  3. private TransactionManager transManager;
  4. private PerformanceMonitor pmonitor;
  5. private TopicDao topicDao;
  6. private ForumDao forumDao;
  7. public void removeTopic(int topicId) {
  8. pmonitor.start();//①-1性能监控开始
  9. transManager.beginTransaction();//②-1 事务处理开始
  10. topicDao.removeTopic(topicId); //③-1 业务逻辑
  11. transManager.commit();//②-1事务处理结束
  12. pmonitor.end();//①-2 性能监控结束
  13. }
  14. public void createForum(Forum forum) {
  15. pmonitor.start();//①-1性能监控开始
  16. transManager.beginTransaction();//②-1 事务处理开始
  17. forumDao.create(forum); //③-2 业务逻辑
  18. transManager.commit();//②-1事务处理结束
  19. pmonitor.end();//①-2 性能监控结束
  20. }
  21. }

代码清单6-1中①的代码是方法性能监视代码,它在方法调用前启动,在方法调用返回前结束,并在内部记录性能监视的结果信息。

而②的代码是事务开始和事务提交的代码。我们发现③处的业务代码淹没在重复化非业务性的代码之中,性能监视和事务管理这些非业务性代码葛藤缠树般包围着业务性代码。

如图6-1所示,假设我们将ForumService业务类看成一段圆木,将removeTopic()和createForum()方法分别看成圆木的一截,我们会发现性能监视和事务管理的代码就好像一个年轮,而业务代码是圆木的树心,这也正是横切代码概念的由来。

我们无法通过抽象父类的方式消除以上所示的重复性横切代码,因为这些横切逻辑依附在业务类方法的流程中,它们不能转移到父类中去。

AOP独辟蹊径通过横向抽取机制为这类无法通过纵向继承体系进行抽象的重复性代码提供了解决方案。对于习惯了纵向抽取的开发者来说,可能不容易理解横向抽取方法的工作机制,因为Java语言本身不直接提供这种横向抽象的能力,我们暂把具体实现放在一旁,先通过图解的方式归纳出AOP的解决思路,如图6-2所示。

从图6-2中,我们可以看出AOP希望将这些分散在各个业务逻辑代码中的相同代码,通过横向切割的方式抽取到一个独立的模块中,还业务逻辑类一个清新的世界。

当然,我们知道将这些重复性的横切逻辑独立出来是很容易的,但如何将这些独立的逻辑融合到业务逻辑中完成和原来一样的业务操作,这才是事情的关键,也正是AOP要解决的主要问题。

引用

我们现在常用“雁过拔毛”来形容某人爱贪便宜,凡他经手的事情都要捞点好处,“雁过拔毛”就是现实生活中AOP的一个很形象的例子。其实“雁过拔毛”原意是形容武艺高超,大雁飞过时也能拔下毛来。

AOP术语

如学习电学就得先学习电阻、电压、电容等专业术语一样,AOP也有一些自己的行话,学习这些“行话”后,就可知道AOP解决横切问题的思路了。

连接点(Joinpoint)?

程序执行的某个特定位置:如类开始初始化前、类初始化后、类某个方法调用前、调用后、方法抛出异常后。一个类或一段程序代码拥有一些具有边界性质的特定点,这些代码中的特定点就称为“连接点”。Spring仅支持方法的连接点,即仅能在方法调用前、方法调用后、方法抛出异常时以及方法调用前后这些程序执行点织入增强。我们知道黑客攻击系统需要找到突破口,没有突破口就无法进行攻击,从这一角度上来说,AOP是一个黑客(因为它要向目前类中嵌入额外的代码逻辑),连接点就是AOP向目标类打入楔子的候选点。

连接点由两个信息确定:第一是用方法表示的程序执行点;第二是用相对点表示的方位。如在Test.foo()方法执行前的连接点,执行点为Test.foo(),方位为该方法执行前的位置。Spring使用切点对执行点进行定位,而方位则在增强类型中定义。

切点(Pointcut)

每个程序类都拥有多个连接点,如一个拥有两个方法的类,这两个方法都是连接点,即连接点是程序类中客观存在的事物。但在这为数众多的连接点中,如何定位到某个感兴趣的连接点上呢?AOP通过“切点”定位特定接连点。通过数据库查询的概念来理解切点和连接点的关系再适合不过了:连接点相当于数据库中的记录,而切点相当于查询条件。切点和连接点不是一对一的关系,一个切点可以匹配多个连接点。

在Spring中,切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件,Spring AOP的规则解析引擎负责解析切点所设定的查询条件,找到对应的连接点。其实确切地说,用切点定位到的应该是“程序的执行点”而非连接点,因为连接点是方法执行前、执行后等包括方位信息的具体程序执行点,而切点只定位到某个方法上,所以如果希望定位到具体连接点上,还需要提供方位信息。

增强(Advice)?

增强是织入到目标类连接点上的一段程序代码。是不是觉得AOP越来越像黑客了:),这不是往业务类中装入木马吗?读者大可按照这一思路去理解增强,因为这样更形象易懂。在Spring中,增强除用于描述一段程序代码外,还拥有另一个和连接点相关的信息,这便是执行点的方位。结合执行点方位信息和切点信息,我们就可以找到特定的连接点了!正因为增强既包含了用于添加到目标连接点上的一段执行逻辑,又包含了用于定位连接点的方位信息,所以Spring所提供的增强接口都是带方位名的:BeforeAdvice、AfterRetuningAdvice、ThrowsAdvice等。BeforeAdvice表示方法调用前的位置,而AfterReturingAdvice表示访问返回后的位置。所以只有结合切点和增强两者一起上阵才能确定特定的连接点并实施增强逻辑。

引用

有很多书籍和文章将Advice译为通知,我们觉得“通知”的译法很不达意。我们来看几个使用“通知”的语境:银行向张三发出了一个催款通知;班主任通知学生明天进行大扫除。从这些语境中,我们可以知道通知者只是把某个消息传达给被通知者,并不会替被通知者做任何事情,而Spring的Advice必须嵌入到某个类的连接点上,并完成了一段附加的应用逻辑,这明显是去“增强”目标类的功能。当然,我们不能对这个翻译有过多的微词,毕竟Advice这个英文单词本身就有些不知所云,如果将其改为Enhancer,相信理解起来会更容易一些。

目标对象(Target)

增强逻辑的织入目标类。如果没有AOP,目标业务类需要自己实现所有逻辑,就如中ForumService所示。在AOP的帮助下,ForumService只实现那些非横切逻辑的程序逻辑,而性能监视和事务管理等这些横切逻辑则可以使用AOP动态织入到特定的连接点上。

引介(Introduction) 

引介是一种特殊的增强,它为类添加一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过AOP的引介功能,我们可以动态地为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。

织入(Weaving) 

织入是将增强添加对目标类具体连接点上的过程,AOP像一台织布机,将目标类、增强或者引介通过AOP这台织布机天衣无缝地编织到一起。我们不能不说“织入”这个词太精辟了。根据不同的实现技术,AOP有三种织入的方式: 
1)编译期织入,这要求使用特殊的Java编译器; 
2)类装载期织入,这要求使用特殊的类装载器; 
3)动态代理织入,在运行期为目标类添加增强生成子类的方式。 
Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。

代理(Proxy) 

一个类被AOP织入增强后,就产出了一个结果类,它是融合了原类和增强逻辑的代理类。根据不同的代理方式,代理类既可能是和原类具有相同接口的类,也可能就是原类的子类,所以我们可以采用调用原类相同的方式调用代理类。

切面(Aspect)

切面由切点和增强(引介)组成,它既包括了横切逻辑的定义,也包括了连接点的定义,Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。

AOP的工作重心在于如何将增强应用于目标对象的连接点上,这里首先包括两个工作:第一,如何通过切点和增强定位到连接点上;第二,如何在增强中编写切面的代码。本章大部分的内容都围绕这两点展开。

AOP的实现者

AOP工具的设计目标是把横切的问题(如性能监视、事务管理)模块化。使用类似于OOP的方式进行切面的编程工作。位于AOP工具核心的是连接点模型,它提供了一种机制,可以识别出在哪里发生了横切。

AspectJ

AspectJ是语言级的AOP实现,2001年由Xerox PARC的AOP小组发布,目前版本已经更新到1.6。AspectJ扩展了Java语言,定义了AOP语法,能够在编译期提供横切代码的织入,所以它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。主页位于http://www.eclipse.org/aspectj。

AspectWerkz 
?  
基于Java的简单、动态、轻量级的AOP框架,该框架2002年就已经发布,由BEA Systems提供支持。它支持运行期或类装载期织入横切代码,所以它拥有一个特殊的类装载器。现在,AspectJ和AspectWerkz项目已经合并,以便整合两者的力量和技术创建统一的AOP平台。他们合作的第一个发布版本是AspectJ 5:扩展AspectJ语言,以基于注解的方式支持类似AspectJ的代码风格。

JBoss AOP  
?  
2004年作为JBoss应用程序服务器框架的扩展功能发布,读者可以从这个地址了解到JBoss AOP的更多信息:http://www.jboss.org/products/aop。

Spring AOP 
?  
Spring AOP使用纯Java实现,它不需要专门的编译过程,不需要特殊的类装载器,它在运行期通过代理方式向目标类织入增强代码。Spring并不尝试提供最完整的AOP实现,相反,它侧重于提供一种和Spring IoC容器整合的AOP实现,用以解决企业级开发中的常见问题。在Spring中,我们可以无缝地将Spring AOP、IoC和AspectJ整合在一起。

这些文章摘自于我的《Spring 3.x企业应用开发实战》的第6章。欢迎大家讨论。

延伸阅读:学习Spring必学的Java基础知识----动态代理http://www.iteye.com/topic/1123293

http://www.iteye.com/topic/1123398

转载:http://wayfarer.cnblogs.com/articles/241024.html

AOP技术的诞生并不算晚,早在1990年开始,来自Xerox Palo Alto Research Lab(即PARC)的研究人员就对面向对象思想的局限性进行了分析。他们研究出了一种新的编程思想,借助这一思想或许可以通过减少代码重复模块从而帮助开发人员提高工作效率。随着研究的逐渐深入,AOP也逐渐发展成一套完整的程序设计思想,各种应用AOP的技术也应运而生。

AOP技术在Java平台下是最先得到应用的。就在PARC对于面向方面编程进行研究的同时,美国Northeastern University的博士生Cristina Lopes和其同事也开始了类似的思考。最终,美国国防先进技术研究计划署(Defense Advanced Research Projects Agency即DARPA)注意到了这项工作,并提供了科研经费,鼓励将二者的工作成果结合起来。他们通过定义一套Java语言的扩展系统,使开发者可以方便的进行面向方面的开发,这套扩展系统被称为AspectJ。之后,AspectJ在2002年被转让给Eclipse Foundation,从而成为在开源社区中AOP技术的先锋,也是目前最为流行的AOP工具。

AspectWerkz则是基于Java的动态的、轻量级AOP框架。AspectWerkz仍然是开源社区中的产品,由BEA System提供赞助,开发者则是BEA的两名员工Jonas Bonér和Alexandre Vasseur。最近版本是AspectWerkz 2.0。2005年1月,AspectJ和AspectWerkz达成协议,同意将二者的成果综合到一起,取其精华创建一个单一的工具。他们合作的第一个发布版本为AspectJ 5,它扩展了AspectJ语言,以支持基于Annotation开发风格而又支持类似AspectJ代码风格。AspectJ 5也为Java 5的语言特性提供完全的AOP支持。

在Java阵营中,商用软件制造商JBoss在其2004年推出的JBoss 4.0中,引入了AOP框架和组件。在JBoss 4.0中,用户可以在JBoss应用服务器外部单独使用JBoss AOP,该版本为JBoss AOP 1.0,是在2004年10月发布的。在2005年,JBoss AOP框架又发布了1.3.0版本,新版本对加载期织入(Weev)和切点(point cut)匹配的性能做了很大的优化,使应用程序的启动时间大大缩短。

作为轻型的Framework,Spring在开发轻量级的J2EE时,应用是非常广泛的。它通过IoC模式(Inversion of Control,控制反转模式)来实现AOP,通常被称为Spring AOP。在2004年,被作为Spring框架的扩展而发布,目前版本已更新到1.1.3。Spring AOP作为一种非侵略性的,轻型的AOP框架,开发者无需使用预编译器或其他的元标签,在Java程序中应用AOP。目前,AOP的功能完全集成到了Spring事务管理、日志和其他各种特性的上下文中。

在.Net的阵营中,AOP技术的应用远不如Java阵营对AOP的关注。2005年1月,微软发布的Enterprise Library提供了7种不同的“应用程序块(application blocks)”。有个别专家认为,这些组件可以被认为是方面。但该观点并未得到一致的认同。事实上,在.Net平台下,推动AOP技术发展的原动力并非微软,而是开源社区。虽然,微软的技术专家们亦然听到了在.Net Framework中增加AOP技术的群众呼声,但作为如此巨大的软件公司,要让它灵活地转变战略方向,显然是不太现实的。正因为此,才赐予了开源社区在AOP技术的研究与探索上一个巨大的发展空间。

与Java阵营中的AOP技术不同,目前在.Net平台下的各种AOP工具,基本上还停留在实验室阶段。但一些在技术上领先且逐渐成熟的AOP产品,也在开源社区中渐露峥嵘。这其中主要包括Aspect#,AspectDNG,Eos AOP等。

Aspect#是基于Castle动态代理技术来实现的。Castle源于Apache Avalon项目,其目的在于实现一个轻量级的IoC容器。Aspect#于2005年6月被收录为Castle的其中一个子项目。它是针对CLI(.Net和Mono)实现的AOP框架,利用了反射、代理等机制。目前的Aspect#版本为2.1.1。

AspectDNG目前的版本为0.7,仍然处于beta版的阶段。它的实现技术是基于rail的静态织入。Rail属于IL级别下的代码织入,它自定义的一套xml格式的ILML语言,能够将原有的程序集拆散成ILML格式,以便于对静态程序集进行修改和扩展,从而达到静态织入的目的。因为AspectDNG是属于IL级别下的代码织入,因此在.Net平台下,并不受具体的编程语言限制。

Eos AOP与AspectDNG一样,仍然采用静态织入的方式,但从语法定义上,它更近似于AspectJ关于AOP的实现。它扩展了C#语法,引入了aspect、introduce、before、after等关键字,并且提供了专用的Eos编译器。Eos项目是于2004年9月开始启动,2005年6月推出的0.3.3版本为最新版本,主要的开发人员为Hridesh Rajan和Kevin Sullivan。前者为Virginia大学计算机系的研究生,Eos项目最初是由Hridesh Rajan提出的;而后者则为该计算机系的副教授(Associate Professor)。所以自Eos诞生之初,就带有浓厚的学院派特色。

从AOP技术的整体发展来看,高性能、稳定、可扩展、易用的AOP框架是其趋势与目标。从上述对各种AOP技术的分析来看,AOP技术无疑是具有共同特点的,而各种实现技术就是围绕着这些共性深入与延伸。接下来,我将概要地介绍AOP的本质,以及它的技术要素。

2.2 AOP技术本质

2.2.1 技术概览

AOP(Aspect-Oriented Programming,面向方面编程),可以说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善。OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

而AOP技术则恰恰相反,它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。AOP代表的是一个横向的关系,如果说“对象”是一个空心的圆柱体,其中封装的是对象的属性和行为;那么面向方面编程的方法,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息。而剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。

使用“横切”技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处都基本相似。比如权限认证、日志、事务处理。Aop 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。正如Avanade公司的高级方案构架师Adam Magee所说,AOP的核心思想就是“将应用程序中的商业逻辑同对其提供支持的通用服务进行分离。”

实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。然而殊途同归,实现AOP的技术特性却是相同的,分别为:

1、join point(连接点):是程序执行中的一个精确执行点,例如类中的一个方法。它是一个抽象的概念,在实现AOP时,并不需要去定义一个join point。
2、point cut(切入点):本质上是一个捕获连接点的结构。在AOP中,可以定义一个point cut,来捕获相关方法的调用。
3、advice(通知):是point cut的执行代码,是执行“方面”的具体逻辑。
4、aspect(方面):point cut和advice结合起来就是aspect,它类似于OOP中定义的一个类,但它代表的更多是对象间横向的关系。
5、introduce(引入):为对象引入附加的方法或属性,从而达到修改对象结构的目的。有的AOP工具又将其称为mixin。

上述的技术特性组成了基本的AOP技术,大多数AOP工具均实现了这些技术。它们也可以是研究AOP技术的基本术语。

2.2.2 横切技术

“横切”是AOP的专有名词。它是一种蕴含强大力量的相对简单的设计和编程技术,尤其是用于建立松散耦合的、可扩展的企业系统时。横切技术可以使得AOP在一个给定的编程模型中穿越既定的职责部分(比如日志记录和性能优化)的操作。

如果不使用横切技术,软件开发是怎样的情形呢?在传统的程序中,由于横切行为的实现是分散的,开发人员很难对这些行为进行逻辑上的实现或更改。例如,用于日志记录的代码和主要用于其它职责的代码缠绕在一起。根据所解决的问题的复杂程度和作用域的不同,所引起的混乱可大可小。更改一个应用程序的日志记录策略可能涉及数百次编辑——即使可行,这也是个令人头疼的任务。

在AOP中,我们将这些具有公共逻辑的,与其他模块的核心逻辑纠缠在一起的行为称为“横切关注点(Crosscutting Concern)”,因为它跨越了给定编程模型中的典型职责界限。

2.2.2.1 横切关注点

一个关注点(concern)就是一个特定的目的,一块我们感兴趣的区域,一段我们需要的逻辑行为。从技术的角度来说,一个典型的软件系统包含一些核心的关注点和系统级的关注点。举个例子来说,一个信用卡处理系统的核心关注点是借贷/存入处理,而系统级的关注点则是日志、事务完整性、授权、安全及性能问题等,许多关注点——即横切关注点(crosscutting concerns)——会在多个模块中出现。如果使用现有的编程方法,横切关注点会横越多个模块,结果是使系统难以设计、理解、实现和演进。AOP能够比上述方法更好地分离系统关注点,从而提供模块化的横切关注点。

例如一个复杂的系统,它由许多关注点组合实现,如业务逻辑、性能,数据存储、日志和调度信息、授权、安全、线程、错误检查等,还有开发过程中的关注点,如易懂、易维护、易追查、易扩展等,图2.1演示了由不同模块实现的一批关注点组成一个系统。

图2.1 把模块作为一批关注点来实现

通过对系统需求和实现的识别,我们可以将模块中的这些关注点分为:核心关注点和横切关注点。对于核心关注点而言,通常来说,实现这些关注点的模块是相互独立的,他们分别完成了系统需要的商业逻辑,这些逻辑与具体的业务需求有关。而对于日志、安全、持久化等关注点而言,他们却是商业逻辑模块所共同需要的,这些逻辑分布于核心关注点的各处。在AOP中,诸如这些模块,都称为横切关注点。应用AOP的横切技术,关键就是要实现对关注点的识别。

如果将整个模块比喻为一个圆柱体,那么关注点识别过程可以用三棱镜法则来形容,穿越三棱镜的光束(指需求),照射到圆柱体各处,获得不同颜色的光束,最后识别出不同的关注点。如图2.2所示:

图2.2 关注点识别:三棱镜法则

上图识别出来的关注点中,Business Logic属于核心关注点,它会调用到Security,Logging,Persistence等横切关注点。

Java代码  

  1. public class BusinessLogic {
  2. public void SomeOperation() {
  3. //验证安全性;Securtity关注点;
  4. //执行前记录日志;Logging关注点;
  5. DoSomething();
  6. //保存逻辑运算后的数据;Persistence关注点;
  7. //执行结束记录日志;Logging关注点;
  8. }
  9. }

AOP的目的,就是要将诸如Logging之类的横切关注点从BusinessLogic类中分离出来。利用AOP技术,可以对相关的横切关注点封装,形成单独的“aspect”。这就保证了横切关注点的复用。由于BusinessLogic类中不再包含横切关注点的逻辑代码,为达到调用横切关注点的目的,可以利用横切技术,截取BusinessLogic类中相关方法的消息,例如SomeOperation()方法,然后将这些“aspect”织入到该方法中。例如图2.3:

图2.3 将横切关注点织入到核心关注点中

通过利用AOP技术,改变了整个系统的设计方式。在分析系统需求之初,利用AOP的思想,分离出核心关注点和横切关注点。在实现了诸如日志、事务管理、权限控制等横切关注点的通用逻辑后,开发人员就可以专注于核心关注点,将精力投入到解决企业的商业逻辑上来。同时,这些封装好了的横切关注点提供的功能,可以最大限度地复用于商业逻辑的各个部分,既不需要开发人员作特殊的编码,也不会因为修改横切关注点的功能而影响具体的业务功能。

为了建立松散耦合的、可扩展的企业系统,AOP应用到的横切技术,通常分为两种类型:动态横切和静态横切。

2.2.2.2 动态横切

动态横切是通过切入点和连接点在一个方面中创建行为的过程,连接点可以在执行时横向地应用于现有对象。动态横切通常用于帮助向对象层次中的各种方法添加日志记录或身份认证。在很多应用场景中,动态横切技术基本上代表了AOP。

动态横切技术的核心主要包括join point(连接点),point cut(切入点),advice(通知)和aspect(方面)。在前面,我已经概要地介绍了这些术语分别代表的含义。接下来,我将以一个具体的实例来进一步阐述它们在AOP动态横切中实现的意义。

考虑一个电子商务系统,需要对订单进行添加、删除等管理操作。毫无疑问,在实际的应用场景中,这些行为应与权限管理结合,只有获得授权的用户方能够实施这些行为。采用传统的设计方法,其伪代码如下:

Java代码  

  1. public class OrderManager {
  2. private ArrayList m_Orders;
  3. public OrderManager() {
  4. m_Orders = new ArrayList();
  5. }
  6. public void AddOrder(Order order) {
  7. if (permissions.Verify(Permission.ADMIN)) {
  8. m_Orders.Add(order);
  9. }
  10. }
  11. public void RemoveOrder(Order order) {
  12. if (permissions.Verify(Permission.ADMIN)) {
  13. m_Orders.Remove(order);
  14. }
  15. }
  16. }

同样的,在该电子商务系统中,还需要对商品进行管理,它采用了同样的授权机制:

Java代码  

  1. public class ProductManager {
  2. private ArrayList m_Products;
  3. public ProductManager() {
  4. m_Products = new ArrayList();
  5. }
  6. public void AddProduct(Product product) {
  7. if (permissions.Verify(Permission.ADMIN)) {
  8. m_Products.Add(product);
  9. }
  10. }
  11. public void RemoveProduct(Product product) {
  12. if (permissions.Verify(Permission.ADMIN)) {
  13. m_Products.Remove(product);
  14. }
  15. }
  16. }

如此以来,在整个电子商务系统中,核心业务包括订单管理和商品管理,它们都需要相同的权限管理,如图2.4所示:

图2.4 电子商务系统的权限验证实现

毫无疑问,利用AOP技术,我们可以分离出系统的核心关注点和横切关注点,从横向的角度,截取业务管理行为的内部消息,以达到织入权限管理逻辑的目的。当执行AddOrder()等方法时,系统将验证用户的权限,调用横切关注点逻辑,因此该方法即为AOP的join point。对于电子商务系统而言,每个需要权限验证的方法都是一个单独的join point。由于权限验证将在每个方法执行前执行,所以对于这一系列join point,只需要定义一个point cut。当系统执行到join point处时,将根据定义去查找对应的point cut,然后执行这个横切关注点需要实现的逻辑,即advice。而point cut和advice,就组合成了一个权限管理aspect。

图2.5 AOP动态横切的技术实现

由于aspect是一个封装的对象,我们可以定义这样一个aspect:
private static aspect AuthorizationAspect{……}

然后在这个aspect中定义point cut,在point cut中,定义了需要截取上下文消息的方法,例如:
private pointcut authorizationExecution():
execution(public void OrderManager.AddOrder(Order)) ||
execution(public void OrderManager.DeleteOrder(Order)) ||
execution(public void ProductManager.AddProduct(Product)) ||
execution(public void ProductManager.DeleteProduct(Product));

由于权限验证是在订单管理方法执行之前完成,因此在before advice中,定义权限检查:
before(): authorizationExecution()
{
    if !(permissions.Verify(Permission.ADMIN))
    {
        throw new UnauthorizedException();
    }
}

通过定义了这样一个完整的aspect,当系统调用OrderManager或ProductManager的相关方法时,就触发了point cut,然后调用相应的advice逻辑。如此以来,OrderManager和ProductManager模块就与权限管理模块完全解除了依赖关系,同时也消除了传统设计中不可避免的权限判断的重复代码。这对于建立一个松散耦合、可扩展的系统软件是非常有利的。

2.2.2.3 静态横切

静态横切和动态横切的区别在于它不修改一个给定对象的执行行为。相反,它允许通过引入附加的方法字段和属性来修改对象的结构。此外,静态横切可以把扩展和实现附加到对象的基本结构中。在AOP实现中,通常将静态横切称为introduce或者mixin。

静态横切在AOP技术中,受到的关注相对较少。事实上,这一技术蕴含的潜力是巨大的。使用静态横切,架构师和设计者能用一种真正面向对象的方法有效地建立复杂系统的模型。静态横切允许您不用创建很深的层次结构,以一种本质上更优雅、更逼真于现实结构的方式,插入跨越整个系统的公共行为。尤其是当开发应用系统时,如果需要在不修改原有代码的前提下,引入第三方产品和API库,则静态横切技术将发挥巨大的作用。

举例来说,当前已经实现了一个邮件收发系统,其中类Mail完成了收发邮件的功能。但在产品交付后,发现该系统存在缺陷,在收发邮件时,未曾实现邮件地址的验证功能。现在,第三方产品已经提供了验证功能的接口IValidatable:
public interface IValidatable
{
    bool ValidateAddress();
}

我们可以利用设计模式中的Adapter模式,来完成对第三方产品API的调用。我们可以定义一个新的类MailAdapter,该类实现了IValidatable接口,同时继承了Mail类:
public class MailAdapter:Mail,IValidatable
{
     public bool ValidateAddress()
     {
         if(this.getToAddress() != null)
         {
             return true;
         }
         else
         {
             return false;
         }
     }
}

通过引入MailAdapter类,原来Mail对象完成的操作,将全部被MailAdapter对象取代。然而,此种实现方式虽然能解决引入新接口的问题,但类似下面的代码,却是无法编译通过的:
Mail mail = new Mail();
IValidatable validate = ((IValidatable)mail).ValidateAddress();

必须将第一行代码作如下修改:
Mail mail = new MailAdapter();

利用AOP的静态横切技术,可以将IValidatable接口织入到原有的Mail类中,这是一种非常形象的introduce功能,其实现仍然是在aspect中完成:
import com.acme.validate.Validatable;

public aspect MailValidateAspect
{
    declare parents: Mail implements IValidatable;

public boolean Mail.validateAddress()
    {
         if(this.getToAddress() != null)
         {
              return true;
         }
         else
         {
              return false;
         }
    }
}

静态横切的方法,并没有引入类似MailAdapter的新类,而是通过定义的MailValidateAspect方面,利用横切技术为Mail类introduce了新的方法ValidateAddress(),从而实现了Mail的扩展。因此如下的代码完全可行。
Mail mail = new Mail();
IValidatable validate = ((IValidatable)mail).ValidateAddress();

2.3 AOP技术的优势

AOP技术的优势是显而易见的。在面向对象的世界里,人们提出了各种方法和设计原则来保障系统的可复用性与可扩展性,以期建立一个松散耦合、便于扩展的软件系统。例如GOF提出的“设计模式”,为我们提供了设计的典范与准则。设计模式通过最大程度的利用面向对象的特性,诸如利用继承、多态,对责任进行分离、对依赖进行倒置,面向抽象,面向接口,最终设计出灵活、可扩展、可重用的类库、组件,乃至于整个系统的架构。在设计的过程中,通过各种模式体现对象的行为、暴露的接口、对象间关系、以及对象分别在不同层次中表现出来的形态。然而鉴于对象封装的特殊性,“设计模式”的触角始终在接口与抽象中大做文章,而对于对象内部则无能为力。

通过“横切”技术,AOP技术就能深入到对象内部翻云覆雨,截取方法之间传递的消息为我所用。由于将核心关注点与横切关注点完全隔离,使得我们能够独立的对“方面”编程。它允许开发者动态地修改静态的OO模型,构造出一个能够不断增长以满足新增需求的系统,就象现实世界中的对象会在其生命周期中不断改变自身,应用程序也可以在发展中拥有新的功能。

设计软件系统时应用AOP技术,其优势在于:

(一)在定义应用程序对某种服务(例如日志)的所有需求的时候。通过识别关注点,使得该服务能够被更好的定义,更好的被编写代码,并获得更多的功能。这种方式还能够处理在代码涉及到多个功能的时候所出现的问题,例如改变某一个功能可能会影响到其它的功能,在AOP中把这样的麻烦称之为“纠结(tangling)”。

(二)利用AOP技术对离散的方面进行的分析将有助于为开发团队指定一位精于该项工作的专家。负责这项工作的最佳人选将可以有效利用自己的相关技能和经验。

(三)持久性。标准的面向对象的项目开发中,不同的开发人员通常会为某项服务编写相同的代码,例如日志记录。随后他们会在自己的实施中分别对日志进行处理以满足不同单个对象的需求。而通过创建一段单独的代码片段,AOP提供了解决这一问题的持久简单的方案,这一方案强调了未来功能的重用性和易维护性:不需要在整个应用程序中一遍遍重新编写日志代码,AOP使得仅仅编写日志方面(logging aspect)成为可能,并且可以在这之上为整个应用程序提供新的功能。

总而言之,AOP技术的优势使得需要编写的代码量大大缩减,节省了时间,控制了开发成本。同时也使得开发人员可以集中关注于系统的核心商业逻辑。此外,它更利于创建松散耦合、可复用与可扩展的大型软件系统。

http://www.iteye.com/topic/655362

附件中有本文的源代码和Pdf版。本文写的很长的原因,是不希望大家学习AOP时到处找资料,大家有时间可以按照本文动手实践下,相信会有非常大的收获的,有什么问题互相交流,有问必答!

1 AOP各种的实现

AOP就是面向切面编程,我们可以从几个层面来实现AOP。

在编译器修改源代码,在运行期字节码加载前修改字节码或字节码加载后动态创建代理类的字节码,以下是各种实现机制的比较。


类别


机制


原理


优点


缺点


静态AOP


静态织入


在编译期,切面直接以字节码的形式编译到目标字节码文件中。


对系统无性能影响。


灵活性不够。


动态AOP


动态代理


在运行期,目标类加载后,为接口动态生成代理类,将切面植入到代理类中。


相对于静态AOP更加灵活。


切入的关注点需要实现接口。对系统有一点性能影响。


动态字节码生成


在运行期,目标类加载后,动态构建字节码文件生成目标类的子类,将切面逻辑加入到子类中。


没有接口也可以织入。


扩展类的实例方法为final时,则无法进行织入。


自定义类加载器


在运行期,目标加载前,将切面逻辑加到目标字节码里。


可以对绝大部分类进行织入。


代码中如果使用了其他类加载器,则这些类将不会被织入。


字节码转换


在运行期,所有类加载器加载字节码前,前进行拦截。


可以对所有类进行织入。

2 AOP里的公民 ?

  • Joinpoint:拦截点,如某个业务方法。
  • Pointcut:Joinpoint的表达式,表示拦截哪些方法。一个Pointcut对应多个Joinpoint。
  • Advice:  要切入的逻辑。
  • Before Advice 在方法前切入。
  • After Advice 在方法后切入,抛出异常时也会切入。
  • After Returning Advice 在方法返回后切入,抛出异常则不会切入。
  • After Throwing Advice 在方法抛出异常时切入。
  • Around Advice 在方法执行前后切入,可以中断或忽略原有流程的执行。 ?
  • 公民之间的关系 

    织入器通过在切面中定义pointcut来搜索目标(被代理类)的JoinPoint(切入点),然后把要切入的逻辑(Advice)织入到目标对象里,生成代理类。

3 AOP的实现机制 
  本章节将详细介绍AOP有各种实现机制。

3.1 动态代理 
  Java在JDK1.3后引入的动态代理机制,使我们可以在运行期动态的创建代理类。使用动态代理实现AOP需要有四个角色:被代理的类,被代理类的接口,织入器,和InvocationHandler,而织入器使用接口反射机制生成一个代理类,然后在这个代理类中织入代码。被代理的类是AOP里所说的目标,InvocationHandler是切面,它包含了Advice和Pointcut。

3.1.1 使用动态代理 
  那如何使用动态代理来实现AOP。下面的例子演示在方法执行前织入一段记录日志的代码,其中Business是代理类,LogInvocationHandler是记录日志的切面,IBusiness, IBusiness2是代理类的接口,Proxy.newProxyInstance是织入器。 
清单一:动态代理的演示

Java代码

  1. public static void main(String[] args) {
  2. //需要代理的接口,被代理类实现的多个接口都必须在这里定义
  3. Class[] proxyInterface = new Class[] { IBusiness.class, IBusiness2.class };
  4. //构建AOP的Advice,这里需要传入业务类的实例
  5. LogInvocationHandler handler = new LogInvocationHandler(new Business());
  6. //生成代理类的字节码加载器
  7. ClassLoader classLoader = DynamicProxyDemo.class.getClassLoader();
  8. //织入器,织入代码并生成代理类
  9. IBusiness2 proxyBusiness = (IBusiness2) Proxy.newProxyInstance(classLoader, proxyInterface, handler);
  10. //使用代理类的实例来调用方法。
  11. proxyBusiness.doSomeThing2();
  12. ((IBusiness) proxyBusiness).doSomeThing();
  13. }
  14. /**
  15. * 打印日志的切面
  16. */
  17. public static class LogInvocationHandler implements InvocationHandler {
  18. private Object target; //目标对象
  19. LogInvocationHandler(Object target) {
  20. this.target = target;
  21. }
  22. @Override
  23. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  24. //执行原有逻辑
  25. Object rev = method.invoke(target, args);
  26. //执行织入的日志,你可以控制哪些方法执行切入逻辑
  27. if (method.getName().equals("doSomeThing2")) {
  28. System.out.println("记录日志");
  29. }
  30. return rev;
  31. }
  32. }
  33. 接口IBusiness和IBusiness2定义省略。

业务类,需要代理的类。

Java代码

  1. public class Business implements IBusiness, IBusiness2 {
  2. @Override
  3. public boolean doSomeThing() {
  4. System.out.println("执行业务逻辑");
  5. return true;
  6. }
  7. @Override
  8. public void doSomeThing2() {
  9. System.out.println("执行业务逻辑2");
  10. }
  11. }

输出

Java代码

  1. 执行业务逻辑2
  2. 记录日志
  3. 执行业务逻辑

可以看到“记录日志”的逻辑切入到Business类的doSomeThing方法前了。

3.1.2 动态代理原理 
    本节将结合动态代理的源代码讲解其实现原理。动态代理的核心其实就是代理对象的生成,即Proxy.newProxyInstance(classLoader, proxyInterface, handler)。让我们进入newProxyInstance方法观摩下,核心代码其实就三行。 
清单二:生成代理类

Java代码

  1. //获取代理类
  2. Class cl = getProxyClass(loader, interfaces);
  3. //获取带有InvocationHandler参数的构造方法
  4. Constructor cons = cl.getConstructor(constructorParams);
  5. //把handler传入构造方法生成实例
  6. return (Object) cons.newInstance(new Object[] { h });

其中getProxyClass(loader, interfaces)方法用于获取代理类,它主要做了三件事情:在当前类加载器的缓存里搜索是否有代理类,没有则生成代理类并缓存在本地JVM里。清单三:查找代理类。

Java代码

  1. // 缓存的key使用接口名称生成的List
  2. Object key = Arrays.asList(interfaceNames);
  3. synchronized (cache) {
  4. do {
  5. Object value = cache.get(key);
  6. // 缓存里保存了代理类的引用
  7. if (value instanceof Reference) {
  8. proxyClass = (Class) ((Reference) value).get();
  9. }
  10. if (proxyClass != null) {
  11. // 代理类已经存在则返回
  12. return proxyClass;
  13. } else if (value == pendingGenerationMarker) {
  14. // 如果代理类正在产生,则等待
  15. try {
  16. cache.wait();
  17. } catch (InterruptedException e) {
  18. }
  19. continue;
  20. } else {
  21. //没有代理类,则标记代理准备生成
  22. cache.put(key, pendingGenerationMarker);
  23. break;
  24. }
  25. } while (true);
  26. }

代理类的生成主要是以下这两行代码。 清单四:生成并加载代理类

Java代码

  1. //生成代理类的字节码文件并保存到硬盘中(默认不保存到硬盘)
  2. proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);
  3. //使用类加载器将字节码加载到内存中
  4. proxyClass = defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);

ProxyGenerator.generateProxyClass()方法属于sun.misc包下,Oracle并没有提供源代码,但是我们可以使用JD-GUI这样的反编译软件打开jre\lib\rt.jar来一探究竟,以下是其核心代码的分析。 
清单五:代理类的生成过程

Java代码

  1. //添加接口中定义的方法,此时方法体为空
  2. for (int i = 0; i < this.interfaces.length; i++) {
  3. localObject1 = this.interfaces[i].getMethods();
  4. for (int k = 0; k < localObject1.length; k++) {
  5. addProxyMethod(localObject1[k], this.interfaces[i]);
  6. }
  7. }
  8. //添加一个带有InvocationHandler的构造方法
  9. MethodInfo localMethodInfo = new MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V", 1);
  10. //循环生成方法体代码(省略)
  11. //方法体里生成调用InvocationHandler的invoke方法代码。(此处有所省略)
  12. this.cp.getInterfaceMethodRef("InvocationHandler", "invoke", "Object; Method; Object;")
  13. //将生成的字节码,写入硬盘,前面有个if判断,默认情况下不保存到硬盘。
  14. localFileOutputStream = new FileOutputStream(ProxyGenerator.access$000(this.val$name) + ".class");
  15. localFileOutputStream.write(this.val$classFile);

那么通过以上分析,我们可以推出动态代理为我们生成了一个这样的代理类。把方法doSomeThing的方法体修改为调用LogInvocationHandler的invoke方法。 
清单六:生成的代理类源码

Java代码

  1. public class ProxyBusiness implements IBusiness, IBusiness2 {
  2. private LogInvocationHandler h;
  3. @Override
  4. public void doSomeThing2() {
  5. try {
  6. Method m = (h.target).getClass().getMethod("doSomeThing", null);
  7. h.invoke(this, m, null);
  8. } catch (Throwable e) {
  9. // 异常处理(略)
  10. }
  11. }
  12. @Override
  13. public boolean doSomeThing() {
  14. try {
  15. Method m = (h.target).getClass().getMethod("doSomeThing2", null);
  16. return (Boolean) h.invoke(this, m, null);
  17. } catch (Throwable e) {
  18. // 异常处理(略)
  19. }
  20. return false;
  21. }
  22. public ProxyBusiness(LogInvocationHandler h) {
  23. this.h = h;
  24. }
  25. //测试用
  26. public static void main(String[] args) {
  27. //构建AOP的Advice
  28. LogInvocationHandler handler = new LogInvocationHandler(new Business());
  29. new ProxyBusiness(handler).doSomeThing();
  30. new ProxyBusiness(handler).doSomeThing2();
  31. }
  32. }

3.1.3 小结 
    从前两节的分析我们可以看出,动态代理在运行期通过接口动态生成代理类,这为其带来了一定的灵活性,但这个灵活性却带来了两个问题,第一代理类必须实现一个接口,如果没实现接口会抛出一个异常。第二性能影响,因为动态代理使用反射的机制实现的,首先反射肯定比直接调用要慢,经过测试大概每个代理类比静态代理多出10几毫秒的消耗。其次使用反射大量生成类文件可能引起Full GC造成性能影响,因为字节码文件加载后会存放在JVM运行时区的方法区(或者叫持久代)中,当方法区满的时候,会引起Full GC,所以当你大量使用动态代理时,可以将持久代设置大一些,减少Full GC次数。

3.2 动态字节码生成 
   使用动态字节码生成技术实现AOP原理是在运行期间目标字节码加载后,生成目标类的子类,将切面逻辑加入到子类中,所以使用Cglib实现AOP不需要基于接口。

本节介绍如何使用Cglib来实现动态字节码技术。Cglib是一个强大的,高性能的Code生成类库,它可以在运行期间扩展Java类和实现Java接口,它封装了Asm,所以使用Cglib前需要引入Asm的jar。 清单七:使用CGLib实现AOP

Java代码

  1. public static void main(String[] args) {
  2. byteCodeGe();
  3. }
  4. public static void byteCodeGe() {
  5. //创建一个织入器
  6. Enhancer enhancer = new Enhancer();
  7. //设置父类
  8. enhancer.setSuperclass(Business.class);
  9. //设置需要织入的逻辑
  10. enhancer.setCallback(new LogIntercept());
  11. //使用织入器创建子类
  12. IBusiness2 newBusiness = (IBusiness2) enhancer.create();
  13. newBusiness.doSomeThing2();
  14. }
  15. /**
  16. * 记录日志
  17. */
  18. public static class LogIntercept implements MethodInterceptor {
  19. @Override
  20. public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
  21. //执行原有逻辑,注意这里是invokeSuper
  22. Object rev = proxy.invokeSuper(target, args);
  23. //执行织入的日志
  24. if (method.getName().equals("doSomeThing2")) {
  25. System.out.println("记录日志");
  26. }
  27. return rev;
  28. }
  29. }

3.3 自定义类加载器 
   如果我们实现了一个自定义类加载器,在类加载到JVM之前直接修改某些类的方法,并将切入逻辑织入到这个方法里,然后将修改后的字节码文件交给虚拟机运行,那岂不是更直接。

Javassist是一个编辑字节码的框架,可以让你很简单地操作字节码。它可以在运行期定义或修改Class。使用Javassist实现AOP的原理是在字节码加载前直接修改需要切入的方法。这比使用Cglib实现AOP更加高效,并且没太多限制,实现原理如下图:

我们使用系统类加载器启动我们自定义的类加载器,在这个类加载器里加一个类加载监听器,监听器发现目标类被加载时就织入切入逻辑,咱们再看看使用Javassist实现AOP的代码: 
清单八:启动自定义的类加载器

Java代码

  1. //获取存放CtClass的容器ClassPool
  2. ClassPool cp = ClassPool.getDefault();
  3. //创建一个类加载器
  4. Loader cl = new Loader();
  5. //增加一个转换器
  6. cl.addTranslator(cp, new MyTranslator());
  7. //启动MyTranslator的main函数
  8. cl.run("jsvassist.JavassistAopDemo$MyTranslator", args);

清单九:类加载监听器

Java代码

  1. public static class MyTranslator implements Translator {
  2. public void start(ClassPool pool) throws NotFoundException, CannotCompileException {
  3. }
  4. /* *
  5. * 类装载到JVM前进行代码织入
  6. */
  7. public void onLoad(ClassPool pool, String classname) {
  8. if (!"model$Business".equals(classname)) {
  9. return;
  10. }
  11. //通过获取类文件
  12. try {
  13. CtClass  cc = pool.get(classname);
  14. //获得指定方法名的方法
  15. CtMethod m = cc.getDeclaredMethod("doSomeThing");
  16. //在方法执行前插入代码
  17. m.insertBefore("{ System.out.println(\"记录日志\"); }");
  18. } catch (NotFoundException e) {
  19. } catch (CannotCompileException e) {
  20. }
  21. }
  22. public static void main(String[] args) {
  23. Business b = new Business();
  24. b.doSomeThing2();
  25. b.doSomeThing();
  26. }
  27. }

输出:

Java代码

  1. 执行业务逻辑2
  2. 记录日志
  3. 执行业务逻辑

其中Bussiness类在本文的清单一中定义。看起来是不是特别简单,CtClass是一个class文件的抽象描述。咱们也可以使用insertAfter()在方法的末尾插入代码,使用insertAt()在指定行插入代码。

3.3.1 小结 
    从本节中可知,使用自定义的类加载器实现AOP在性能上要优于动态代理和Cglib,因为它不会产生新类,但是它仍然存在一个问题,就是如果其他的类加载器来加载类的话,这些类将不会被拦截。

3.4 字节码转换 
    自定义的类加载器实现AOP只能拦截自己加载的字节码,那么有没有一种方式能够监控所有类加载器加载字节码呢?有,使用Instrumentation,它是 Java 5 提供的新特性,使用 Instrumentation,开发者可以构建一个字节码转换器,在字节码加载前进行转换。本节使用Instrumentation和javassist来实现AOP。

3.4.1 构建字节码转换器 
    首先需要创建字节码转换器,该转换器负责拦截Business类,并在Business类的doSomeThing方法前使用javassist加入记录日志的代码。

Java代码

  1. public class MyClassFileTransformer implements ClassFileTransformer {
  2. /**
  3. * 字节码加载到虚拟机前会进入这个方法
  4. */
  5. @Override
  6. public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
  7. ProtectionDomain protectionDomain, byte[] classfileBuffer)
  8. throws IllegalClassFormatException {
  9. System.out.println(className);
  10. //如果加载Business类才拦截
  11. if (!"model/Business".equals(className)) {
  12. return null;
  13. }
  14. //javassist的包名是用点分割的,需要转换下
  15. if (className.indexOf("/") != -1) {
  16. className = className.replaceAll("/", ".");
  17. }
  18. try {
  19. //通过包名获取类文件
  20. CtClass cc = ClassPool.getDefault().get(className);
  21. //获得指定方法名的方法
  22. CtMethod m = cc.getDeclaredMethod("doSomeThing");
  23. //在方法执行前插入代码
  24. m.insertBefore("{ System.out.println(\"记录日志\"); }");
  25. return cc.toBytecode();
  26. } catch (NotFoundException e) {
  27. } catch (CannotCompileException e) {
  28. } catch (IOException e) {
  29. //忽略异常处理
  30. }
  31. return null;
  32. }

3.4.2 注册转换器 
    使用premain函数注册字节码转换器,该方法在main函数之前执行。

Java代码

  1. public class MyClassFileTransformer implements ClassFileTransformer {
  2. public static void premain(String options, Instrumentation ins) {
  3. //注册我自己的字节码转换器
  4. ins.addTransformer(new MyClassFileTransformer());
  5. }
  6. }

3.4.3 配置和执行 
    需要告诉JVM在启动main函数之前,需要先执行premain函数。首先需要将premain函数所在的类打成jar包。并修改该jar包里的META-INF\MANIFEST.MF 文件。

Java代码

  1. Manifest-Version: 1.0
  2. Premain-Class: bci. MyClassFileTransformer

然后在JVM的启动参数里加上。-javaagent:D:\java\projects\opencometProject\Aop\lib\aop.jar

             3.4.4 输出

    执行main函数,你会发现切入的代码无侵入性的织入进去了。

Java代码

  1. public static void main(String[] args) {
  2. new Business().doSomeThing();
  3. new Business().doSomeThing2();
  4. }

输出

Java代码

  1. model/Business
  2. sun/misc/Cleaner
  3. java/lang/Enum
  4. model/IBusiness
  5. model/IBusiness2
  6. 记录日志
  7. 执行业务逻辑
  8. 执行业务逻辑2
  9. java/lang/Shutdown
  10. java/lang/Shutdown$Lock

从输出中可以看到系统类加载器加载的类也经过了这里。

4 AOP实战 
说了这么多理论,那AOP到底能做什么呢? AOP能做的事情非常多。

  • 性能监控,在方法调用前后记录调用时间,方法执行太长或超时报警。
  • 缓存代理,缓存某方法的返回值,下次执行该方法时,直接从缓存里获取。
  • 软件破解,使用AOP修改软件的验证类的判断逻辑。
  • 记录日志,在方法执行前后记录系统日志。
  • 工作流系统,工作流系统需要将业务代码和流程引擎代码混合在一起执行,那么我们可以使用AOP将其分离,并动态挂接业务。
  • 权限验证,方法执行前验证是否有权限执行当前方法,没有则抛出没有权限执行异常,由业务代码捕捉。

4.1 Spring的AOP 
    Spring默认采取的动态代理机制实现AOP,当动态代理不可用时(代理类无接口)会使用CGlib机制。但Spring的AOP有一定的缺点,第一个只能对方法进行切入,不能对接口,字段,静态代码块进行切入(切入接口的某个方法,则该接口下所有实现类的该方法将被切入)。第二个同类中的互相调用方法将不会使用代理类。因为要使用代理类必须从Spring容器中获取Bean。第三个性能不是最好的,从3.3章节我们得知使用自定义类加载器,性能要优于动态代理和CGlib。 
可以获取代理类

Java代码

  1. public IMsgFilterService getThis()
  2. {
  3. return (IMsgFilterService) AopContext.currentProxy();
  4. }
  5. public boolean evaluateMsg () {
  6. // 执行此方法将织入切入逻辑
  7. return getThis().evaluateMsg(String message);
  8. }
  9. @MethodInvokeTimesMonitor("KEY_FILTER_NUM")
  10. public boolean evaluateMsg(String message) {

不能获取代理类

Java代码

  1. public boolean evaluateMsg () {
  2. // 执行此方法将不会织入切入逻辑
  3. return evaluateMsg(String message);
  4. }
  5. @MethodInvokeTimesMonitor("KEY_FILTER_NUM")
  6. public boolean evaluateMsg(String message) {

4.2 参考资料

  • Java 动态代理机制分析及扩展
  • CGlib的官方网站
  • ASM官方网站
  • JbossAOP
  • Java5特性Instrumenttation实践

http://www.iteye.com/topic/1116696

时间: 2024-10-09 23:19:15

依赖注入(DI)有助于应用对象之间的解耦,而面向切面编程(AOP)有助于横切关注点与所影响的对象之间的解耦(转good)的相关文章

控制反转IOC与依赖注入DI

1. IoC理论的背景我们都知道,在采用面向对象方法设计的软件系统中,它的底层实现都是由N个对象组成的,所有的对象通过彼此的合作,最终实现系统的业务逻辑. 图1:软件系统中耦合的对象 如果我们打开机械式手表的后盖,就会看到与上面类似的情形,各个齿轮分别带动时针.分针和秒针顺时针旋转,从而在表盘上产生正确的时间.图1中描述的就是这样的一个齿轮组,它拥有多个独立的齿轮,这些齿轮相互啮合在一起,协同工作,共同完成某项任务.我们可以看到,在这样的齿轮组中,如果有一个齿轮出了问题,就可能会影响到整个齿轮组

谈谈依赖注入DI

控制反转(Inversion of Control,英文缩写为IoC)是一个重要的面向对象编程的法则来削减计算机程序的耦合问题,也是轻量级的Spring框架的核心. 控制反转一般分为两种类型,依赖注入(Dependency Injection,简称DI)和依赖查找(Dependency Lookup).依赖注入应用比较广泛. 这其实是一种设计模式,目的是为了减少硬编码和对象中的强依赖. 就是要提供一个容器,由容器来完成(1)具体ServiceProvider的创建(2)ServiceUser和S

控制反转(Ioc)和依赖注入(DI)

控制反转IOC, 全称 “Inversion of Control”.依赖注入DI, 全称 “Dependency Injection”. 面向的问题:软件开发中,为了降低模块间.类间的耦合度,提倡基于接口的开发,那么在实现中必须面临最终是有“谁”提供实体类的问题.(将各层的对象以松耦合的方式组织起来,各层对象的调用面向接口.) 当一个类的实例需要另一个类的实例协助时,在传统的程序设计过程中,通常有调用者来创建被调用者的实例. 然后,采用依赖注入原则,创建被调用者的实例的工作不再由调用者完成,而

控制反转IOC与依赖注入DI - 理论篇

学无止境,精益求精 十年河东十年河西,莫欺少年穷 昨天是五一小长假归来上班的第一天,身体疲劳,毫无工作热情.于是就看看新闻,喝喝茶,荒废了一天 也就在昨天,康美同事张晶童鞋让我学习下IOC的理论及实现,毕竟是之前的好同事,好朋友,我也就抽时间百度了很多资料 在查阅网上资料的过程中,我发现大多技术篇幅都是IOC的代码实现,并没有一篇介绍IOC理论的篇幅!这显然不是我想要的. 我知道要想搞明白IOC,就必须要弄明白什么是IOC(控制反转)?为什么叫IOC(控制反转)?为什么之后又可以称为DI(依赖注

ASP.NET Core 依赖注入(DI)

原文:ASP.NET Core 依赖注入(DI) ASP.NET Core的底层设计支持和使用依赖注入.ASP.NET Core 应用程序可以利用内置的框架服务将服务注入到启动类的方法中,并且应用程序服务也可以配置注入.由ASP.NET Core 提供的默认服务容器提供了最小功能集,并不是取代其他容器. 1.浅谈依赖注入 依赖注入(Dependency injection,DI)是一种实现对象和依赖者之间松耦合的技术,将类用来执行其操作的这些对象以注入的方式提供给该类,而不是直接实例化依赖项或者

浅析Spring IOC、依赖注入(DI)和依赖查找(DL)

为什么要用IOC? 第一:对象的实例化不是一件简单的事情,比如对象的关系比较复杂,依赖关系往往需要程序员去维护,这是一件非常头疼的事. 第二:解耦,由容器去维护具体的对象 第三:托管了类的产生过程,比如我们需要在类的产生过程中做一些处理,最直接的例子就是代理,如果有容器程序可以把这部分过程交给容器,应用程序则无需去关心类是如何完成代理的 控制反转(Inverse of Control) 控制反转即IoC(Incersion of Control),从字面上理解就是控制反转,将对在自身对象中的一个

Spring核心思想:IOC(控制反转)、DI(依赖注入)和AOP(面向切面编程)

Spring有三大核心思想,分别是控制反转(IOC,Inversion Of Controller),依赖注入(DI,Dependency Injection)和面向切面编程(AOP,Aspect Oriented Programming). 控制反转(IOC,Inversion Of Controller) 控制反转不是什么技术,而是一种设计思想.在Java开发中,IOC意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制. 传统正转 控制有反转,当然也就有原来的正转.正转就

iOS控制反转(IoC)与依赖注入(DI)的实现

背景 最近接触了一段时间的SpringMVC,对其控制反转(IoC)和依赖注入(DI)印象深刻,此后便一直在思考如何使用OC语言较好的实现这两个功能.Java语言自带的注解特性为IoC和DI带来了极大的方便,要在OC上较好的实现这两个功能,需要一些小小的技巧. 控制反转和依赖注入 控制反转 简单来说,将一个类对象的创建由手动new方式改为从IOC容器内获取,就是一种控制反转,例如我们现在要创建一个ClassA类,则常规方法为 ClassA *a = [ClassA new]; 如果使用控制反转,

控制反转(IoC)与依赖注入(DI)

1.控制反转(Inversion of Control)与依赖注入(Dependency Injection) 控制反转即IoC (Inversion of Control),它把传统上由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理.所谓的“控制反转”概念就是对组件对象控制权的转移,从程序代码本身转移到了外部容器. IoC是一个很大的概念,可以用不同的方式来实现.其主要实现方式有两种:<1>依赖查找(Dependency Lookup):容器提供回调接口和上下文环