来源于:有关 Spring AOP (AspectJ) 你该了解的一切创作者:zejian(转截已得到 创作者受权,如需转截请与创作者联络)
关系文章内容
有关Spring IOC (DI-依赖注入)你需要了解的一切
这篇是年之后第一篇博闻,因为时尚博主用了许多時间在设计构思这篇博闻,再加上近期很忙,因此 这篇文档写的较为久,也分了不一样的时间范围在写,散尽较大 工作能力去连贯性博闻中的內容,竭尽全力展现出简单易懂的文本含意,如原文中有不正确请留言板留言,感谢。
OOP即面向对象编程的程序设计,说起了OOP,大家就迫不得已了解一下POP即面向对象方法程序设计,它是以作用为管理中心来开展思索和机构的一种程序编写方法,注重的是系统软件的数据信息被生产加工和解决的全过程,简言之便是重视多功能性的完成,实际效果做到就好了,而OOP则重视封裝,注重全面性的定义,以目标为管理中心,将目标的內部机构与环境因素区别起来。以前见到过一个很切合的表述,时尚博主把他们画成一幅图以下:
在这儿大家姑且把程序设计形容为房屋的布局,一间房屋的合理布局中,必须各种各样作用的家俱和卫浴洁具(相近方式),如坐便器、浴盆、天然气灶,床、餐桌等,针对面向对象方法的程序设计更重视的是作用的完成(即作用方式的完成),实际效果合乎预估就行,因而面向对象方法的程序设计会更趋向图1设定构造,各种各样作用早已完成,房屋也就可以一切正常定居了。
但针对面向对象编程的程序设计则是难以忍受的,那样的设定使房屋内的各种各样家俱和卫浴洁具间放置较为散乱而且互相曝露的概率大大增加,各种各样味道互相掺杂,显而易见是很槽糕的,因此为了更好地更雅致地设定房子的合理布局,面向对象编程的程序设计便选用了图2的合理布局,针对面向对象编程程序设计而言那样设定益处是不言而喻的,房屋中的每一个屋子都是有分别的名字和相对作用(在Java程序设计中一般把相近那样的屋子称之为类,每一个类意味着着一种屋子的抽象性体),如洗手间是尺寸解和冼澡梳洗用的,卧房是歇息用的,餐厅厨房则是煮饭用的,每一个小屋子都各尽其责而且不用時刻向外部曝露內部的构造,全部屋子构造清楚,外部只必须了解这一屋子并应用屋子里出示的各类作用就可以(方式启用),另外也更有益于中后期的扩展了,终究哪一个屋子必须加上这些作用,其范畴也拥有限定,也就使岗位职责更为确立了(单一义务标准)。
OOP的出現对POP的确存有许多 颠覆性创新的,但并不能说POP已沒有使用价值了,终究仅仅不一样时期的物质,从科学方法论而言,更喜欢将面向对象方法与面向对象编程看作是事情的2个层面–部分与总体(你务必要注意到部分与总体是相对性的),因而在具体运用中,二者方式都一样关键。
掌握完OOP和POP分别的特性,然后看java程序设计全过程中OOP运用,在java程序设计全过程中,大家基本上坐享了OOP设计方案观念产生的好处,以致于在这个一切皆目标,众生平等的全球里,欢乐不己,而OOP的确也遵照本身的服务宗旨将要数据信息及对数据信息的操作个人行为放到一起,做为一个相互依赖、不可缺少的总体,这一总体美名其曰:目标,运用该界定针对同样种类的目标开展归类、抽象性后,得到相互的特点,进而产生了类,在java程序设计中这种类便是class,因为类(目标)基础全是现实世界存有的事情定义(如前边的不一样的小屋子)因而更贴近大家对客观现实的了解,另外把数据信息和方式(优化算法)封裝在一个类(目标)中,那样更有益于数据信息的安全性,一般状况下特性和优化算法只独立归属于某一类,进而使程序设计更简易,也更便于维护保养。
根据这套基础理论观念,在具体的开发软件中,全部系统软件客观事实也是由系列产品相互依存的目标所构成,而这种目标也是被抽象性出去的类。坚信大伙儿在具体开发设计中是有一定的感受的(这篇文档假设阅读者已具有面向对象编程的开发设计观念包含封裝、承继、多态的知识要点)。但伴随着手机软件经营规模的扩大,运用的慢慢升級,渐渐地,OOP也刚开始显现出一些难题,如今不用急切了解他们,根据实例,大家渐渐地体会:
A类:
public class A { public void executeA(){ //别的业务操作省去...... recordLog(); } public void recordLog(){ //....纪录系统日志并汇报日志系统 }}
B类:
public class B { public void executeB(){ //别的业务操作省去...... recordLog(); } public void recordLog(){ //....纪录系统日志并汇报日志系统 }}
C类:
public class C { public void executeC(){ //别的业务操作省去...... recordLog(); } public void recordLog(){ //....纪录系统日志并汇报日志系统 }}
假定存有A、B、C三个类,必须对他们的方式浏览开展系统日志纪录,在代码中各种各样存有recordLog方式开展系统日志纪录并汇报,也许对如今的技术工程师而言基本上不太可能写成这般槽糕的代码,但在OOP那样的书写是容许的,并且在OOP刚开始环节那样的代码的确并很多存有着,直至技术工程师确实承受不上一次改动,四处挖墓时(改动recordLog內容),才下决心处理该难题,为了更好地处理程序流程间过少沉余代码的难题,技术工程师便刚开始应用下边的编码方法
//A类public class A { public void executeA(){ //别的业务操作省去...... args 主要参数,一般会传送类名,方式名字 或信息内容(那样的信息内容一般不随便修改) Report.recordLog(args ...); }}//B类public class B { public void executeB(){ //别的业务操作省去...... Report.recordLog(args ...); }}//C类public class C { public void executeC(){ //别的业务操作省去...... Report.recordLog(args ...); }}//recordpublic class Report { public static void recordLog(args ...){ //....纪录系统日志并汇报日志系统 }}
那样操作后,大家喜悦地发现问题好像获得了处理,当汇报信息内容內部方式必须调节时,只需调节Report类中recordLog方式体,也就防止了到处挖墓的难题,大幅度降低了手机软件中后期维护保养的复杂性。的确这般,并且除开所述的解决方法,还存有一种根据承继来处理的方法,选用这类方法,只需把互通的代码放进一个类(一般是别的类的父类)中,别的的类(子类)根据承继父类获得互通的代码,以下:
//通用性父类public class Dparent { public void commond(){ //通用性代码 }}//A 承继 Dparent public class A extends Dparent { public void executeA(){ //别的业务操作省去...... commond(); }}//B 承继 Dparent public class B extends Dparent{ public void executeB(){ //别的业务操作省去...... commond(); }}//C 承继 Dparent public class C extends Dparent{ public void executeC(){ //别的业务操作省去...... commond(); }}
显而易见代码沉余也获得了处理,这类根据承继提取通用性代码的方法也称之为竖向扩展,与之相匹配的也有横着扩展(如今不需急切搞清楚,后边的剖析中它将经常可以看到)。实际上拥有所述二种解决方法后,在绝大多数业务情景的代码沉余难题也获得了切切实实的处理,基本原理如下图
可是伴随着开发软件的系统软件愈来愈繁杂,技术工程师了解到,传统式的OOP程序流程常常主要表现出一些不当然的状况,核心业务中总夹杂着一些不关联的独特业务,如系统日志纪录,管理权限认证,事务管理操纵,特性检验,错误报告检验这些,这种独特业务可以说和核心业务沒有压根上的关系并且核心业务都不关注他们,例如在用户管理系统模块中,该模块自身只关注与客户有关的业务信息资源管理,对于别的的业务彻底可以不理睬,大家看一个简易事例帮助了解这个问题
/** * Created by zejian on 2017/2/15. * Blog : [全文详细地址,请重视原創] */public interface IUserService { void saveUser(); void deleteUser(); void findAllUser();}//完成类public class UserServiceImpl implements IUserService { //核心数据信息组员 //系统日志操作目标 //管理权限目标 //事务管理操纵目标 @Override public void saveUser() { //管理权限认证(假定管理权限认证丢在这儿) //事务管理操纵 //系统日志操作 //开展Dao层操作 userDao.saveUser(); } @Override public void deleteUser() { } @Override public void findAllUser() { }}
所述代码中大家注意到一些难题,管理权限,系统日志,事务管理都并不是用户管理系统的核心业务,换句话说用户管理系统模块除开要解决本身的核心业务外,还必须解决管理权限,系统日志,事务管理等候这种杂七杂八的无关紧要业务的外场操作,并且这种外场操作一样会在别的业务模块中出現,那样便会导致以下难题
代码错乱:核心业务模块很有可能必须兼具解决别的无关紧要的业务外场操作,这种外场操作很有可能会错乱核心操作的代码,并且当外场模块有重特大改动时也会危害到核心模块,这显而易见是不科学的。
代码分散化和沉余:一样的作用代码,在别的的模块基本上经常可以看到,造成 代码分散化而且信息冗余高。
代码品质低拓展难:因为不太有关的业务代码掺杂在一起,没法潜心核心业务代码,当开展相近不相干业务拓展时又会立即牵涉到核心业务的代码,造成 扩展性低。
显而易见前边剖析的二种解决方法已无计可施了,那麼该如何解决呢?实际上我们知道例如系统日志,管理权限,事务管理,特性检测等业务基本上涉及到来到全部的核心模块,假如把这种独特的业务代码立即到核心业务模块的代码中便会导致所述的难题,而技术工程师更期待的是这种模块能够完成热插拔特点并且不用把外场的代码侵入到核心模块中,那样在今后的维护保养和拓展也将会出现更优的主要表现,假定如今大家把系统日志、管理权限、事务管理、特性检测等外场业务当作独立的侧重点(还可以了解为独立的模块),每一个侧重点都能够在必须他们的時刻立即被应用并且不用提早融合到核心模块中,这类方式非常下面的图:
从图能够看得出,每一个侧重点与核心业务模块分离出来,做为独立的 功能,横切几个核心业务模块,这样的做的好处是显而易见的,每份功能代码不再单独入侵到核心业务类的代码中,即核心模块只需关注自己相关的业务,当需要外围业务(日志,权限,性能监测、事务控制)时,这些外围业务会通过一种特殊的技术自动应用到核心模块中,这些关注点有个特殊的名称,叫做“横切关注点”,上图也很好的表现出这个概念,另外这种抽象级别的技术也叫AOP(面向切面编程),正如上图所展示的横切核心模块的整面,因此AOP的概念就出现了,而所谓的特殊技术也就面向切面编程的实现技术,AOP的实现技术有多种,其中与Java无缝对接的是一种称为AspectJ的技术。
那么这种切面技术(AspectJ)是如何在Java中的应用呢?不必担心,也不必全面了解AspectJ,本篇博文也不会这样进行,对于AspectJ,我们只会进行简单的了解,从而为理解spring中的AOP打下良好的基础(Spring AOP 与AspectJ 实现原理上并不完全一致,但功能上是相似的,这点后面会分析),毕竟Spring中已实现AOP主要功能,开发中直接使用Spring中提供的AOP功能即可,除非我们想单独使用AspectJ的其他功能。
这里还需要注意的是,AOP的出现确实解决外围业务代码与核心业务代码分离的问题,但它并不会替代OOP,如果说OOP的出现是把编码问题进行模块化,那么AOP就是把涉及到众多模块的某一类问题进行统一管理,因此在实际开发中AOP和OOP同时存在并不奇怪,后面将会慢慢体会带这点,好的,已迫不及待了,让我们开始了解神一样的AspectJ吧。
这里先进行一个简单案例的演示,然后引出AOP中一些晦涩难懂的抽象概念,放心,通过本篇博客,我们将会非常轻松地理解并掌握它们。编写一个HelloWord的类,然后利用AspectJ技术切入该类的执行过程。
/** * Created by zejian on 2017/2/15. * Blog : [原文地址,请尊重原创] */public class HelloWord { public void sayHello(){ System.out.println("hello world !"); } public static void main(String args[]){ HelloWord helloWord =new HelloWord(); helloWord.sayHello(); }}
编写AspectJ类,注意关键字为aspect(MyAspectJDemo.aj,其中aj为AspectJ的后缀),含义与class相同,即定义一个AspectJ的类
/** * Created by zejian on 2017/2/15. * Blog : [原文地址,请尊重原创] * 切面类 */public aspect MyAspectJDemo { /** * 定义切点,日志记录切点 */ pointcut recordLog():call(* HelloWord.sayHello(..)); /** * 定义切点,权限验证(实际开发中日志和权限一般会放在不同的切面中,这里仅为方便演示) */ pointcut authCheck():call(* HelloWord.sayHello(..)); /** * 定义前置通知! */ before():authCheck(){ System.out.println("sayHello方法执行前验证权限"); } /** * 定义后置通知 */ after():recordLog(){ System.out.println("sayHello方法执行后记录日志"); }}
ok~,运行helloworld的main函数:
对于结果不必太惊讶,完全是意料之中。我们发现,明明只运行了main函数,却在sayHello函数运行前后分别进行了权限验证和日志记录,事实上这就是AspectJ的功劳了。对aspectJ有了感性的认识后,再来聊聊aspectJ到底是什么?
AspectJ是一个java实现的AOP框架,它能够对java代码进行AOP编译(一般在编译期进行),让java代码具有AspectJ的AOP功能(当然需要特殊的编译器),可以这样说AspectJ是目前实现AOP框架中最成熟,功能最丰富的语言,更幸运的是,AspectJ与java程序完全兼容,几乎是无缝关联,因此对于有java编程基础的工程师,上手和使用都非常容易。
在案例中,我们使用aspect关键字定义了一个类,这个类就是一个切面,它可以是单独的日志切面(功能),也可以是权限切面或者其他,在切面内部使用了pointcut定义了两个切点,一个用于权限验证,一个用于日志记录,而所谓的切点就是那些需要应用切面的方法,如需要在sayHello方法执行前后进行权限验证和日志记录,那么就需要捕捉该方法,而pointcut就是定义这些需要捕捉的方法(常常是不止一个方法的),这些方法也称为目标方法,最后还定义了两个通知,通知就是那些需要在目标方法前后执行的函数,如before()即前置通知在目标方法之前执行,即在sayHello()方法执行前进行权限验证,另一个是after()即后置通知,在sayHello()之后执行,如进行日志记录。到这里也就可以确定,切面就是切点和通知的组合体,组成一个单独的结构供后续使用,下图协助理解。
这里简单说明一下切点的定义语法:关键字为pointcut,定义切点,后面跟着函数名称,最后编写匹配表达式,此时函数一般使用call()或者execution()进行匹配,这里我们统一使用call()
pointcut 函数名 : 匹配表达式
案例:recordLog()是函数名称,自定义的,* 表示任意返回值,接着就是需要拦截的目标函数,sayHello(..)的..,表示任意参数类型。这里理解即可,后面Spring AOP会有关于切点表达式的分析,整行代码的意思是使用pointcut定义一个名为recordLog的切点函数,其需要拦截的(切入)的目标方法是HelloWord类下的sayHello方法,参数不限。
pointcut recordLog():call(* HelloWord.sayHello(..));
关于定义通知的语法:首先通知有5种类型分别如下:
语法:
[返回值类型] 通知函数名称(参数) [returning/throwing 表达式]:连接点函数(切点函数){ 函数体 }
案例如下,其中要注意around通知即环绕通知,可以通过proceed()方法控制目标函数是否执行。
/** * 定义前置通知 * * before(参数):连接点函数{ * 函数体 * } */ before():authCheck(){ System.out.println("sayHello方法执行前验证权限"); } /** * 定义后置通知 * after(参数):连接点函数{ * 函数体 * } */ after():recordLog(){ System.out.println("sayHello方法执行后记录日志"); } /** * 定义后置通知带返回值 * after(参数)returning(返回值类型):连接点函数{ * 函数体 * } */ after()returning(int x): get(){ System.out.println("返回值为:"+x); } /** * 异常通知 * after(参数) throwing(返回值类型):连接点函数{ * 函数体 * } */ after() throwing(Exception e):sayHello2(){ System.out.println("抛出异常:"+e.toString()); } /** * 环绕通知 可通过proceed()控制目标函数是否执行 * Object around(参数):连接点函数{ * 函数体 * Object result=proceed();//执行目标函数 * return result; * } */ Object around():aroundAdvice(){ System.out.println("sayAround 执行前执行"); Object result=proceed();//执行目标函数 System.out.println("sayAround 执行后执行"); return result; }
切入点(pointcut)和通知(advice)的概念已比较清晰,而切面则是定义切入点和通知的组合如上述使用aspect关键字定义的MyAspectJDemo,把切面应用到目标函数的过程称为织入(weaving)。在前面定义的HelloWord类中除了sayHello函数外,还有main函数,以后可能还会定义其他函数,而这些函数都可以称为目标函数,也就是说这些函数执行前后也都可以切入通知的代码,这些目标函数统称为连接点,切入点(pointcut)的定义正是从这些连接点中过滤出来的,下图协助理解。
经过前面的简单介绍,我们已初步掌握了AspectJ的一些语法和概念,但这样仍然是不够的,我们仍需要了解AspectJ应用到java代码的过程(这个过程称为织入),对于织入这个概念,可以简单理解为aspect(切面)应用到目标函数(类)的过程。
对于这个过程,一般分为动态织入和静态织入,动态织入的方式是在运行时动态将要增强的代码织入到目标类中,这样往往是通过动态代理技术完成的,如Java JDK的动态代理(Proxy,底层通过反射实现)或者CGLIB的动态代理(底层通过继承实现),Spring AOP采用的就是基于运行时增强的代理技术,这点后面会分析,这里主要重点分析一下静态织入,ApectJ采用的就是静态织入的方式。ApectJ主要采用的是编译期织入,在这个期间使用AspectJ的acj编译器(类似javac)把aspect类编译成class字节码后,在java目标类编译时织入,即先编译aspect类再编译目标类。
关于ajc编译器,是一种能够识别aspect语法的编译器,它是采用java语言编写的,由于javac并不能识别aspect语法,便有了ajc编译器,注意ajc编译器也可编译java文件。为了更直观了解aspect的织入方式,我们打开前面案例中已编译完成的HelloWord.class文件,反编译后的java代码如下:
//// Source code recreated from a .class file by IntelliJ IDEA// (powered by Fernflower decompiler)//package com.zejian.demo;import com.zejian.demo.MyAspectJDemo;//编译后织入aspect类的HelloWord字节码反编译类public class HelloWord { public HelloWord() { } public void sayHello() { System.out.println("hello world !"); } public static void main(String[] args) { HelloWord helloWord = new HelloWord(); HelloWord va/span> = helloWord; try { //MyAspectJDemo 切面类的前置通知织入 MyAspectJDemo.aspectOf().ajc$before$com_zejian_demo_MyAspectJDemo$1$22c5541(); //目标类函数的调用 va/span>.sayHello(); } catch (Throwable var3) { MyAspectJDemo.aspectOf().ajc$after$com_zejian_demo_MyAspectJDemo$2$4/span>(); throw var3; } //MyAspectJDemo 切面类的后置通知织入 MyAspectJDemo.aspectOf().ajc$after$com_zejian_demo_MyAspectJDemo$2$4/span>(); }}
显然AspectJ的织入原理已很明朗了,当然除了编译期织入,还存在链接期(编译后)织入,即将aspect类和java目标类同时编译成字节码文件后,再进行织入处理,这种方式比较有助于已编译好的第三方jar和Class文件进行织入操作,由于这不是本篇的重点,暂且不过多分析,掌握以上AspectJ知识点就足以协助理解Spring AOP了。有些同学可能想自己编写aspect程序进行测试练习,博主在这简单介绍运行环境的搭建,首先博主使用的idea的IDE,因此只对idea进行介绍(eclipse,呵呵)。首先通过maven仓库下载工具包aspectjtools-1.8.9.jar,该工具包包含ajc核心编译器,然后打开idea检查是否已安装aspectJ的插件:
配置项目使用ajc编译器(替换javac)如下图:
如果使用maven开发(否则在libs目录自行引入jar)则在pom文件中添加aspectJ的核心依赖包,包含了AspectJ运行时的核心库文件:
<dependency> <groupId>aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.5.4</version></dependency>
新建文件处创建aspectJ文件,然后就可以像运行java文件一样,操作aspect文件了。
ok~,关于AspectJ就简单介绍到这。
Spring AOP 与ApectJ 的目的一致,都是为了统一处理横切业务,但与AspectJ不同的是,Spring AOP 并不尝试提供完整的AOP功能(即使它完全可以实现),Spring AOP 更注重的是与Spring IOC容器的结合,并结合该优势来解决横切业务的问题,因此在AOP的功能完善方面,相对来说AspectJ具有更大的优势。
同时,Spring注意到AspectJ在AOP的实现方式上依赖于特殊编译器(ajc编译器),因此Spring很机智回避了这点,转向采用动态代理技术的实现原理来构建Spring AOP的内部机制(动态织入),这是与AspectJ(静态织入)最根本的区别。在AspectJ 1.5后,引入@Aspect形式的注解风格的开发,Spring也非常快地跟进了这种方式,因此Spring 2.0后便使用了与AspectJ一样的注解。
请注意,Spring 只是使用了与 AspectJ 5 一样的注解,但仍然没有使用 AspectJ 的编译器,底层依是动态代理技术的实现,因此并不依赖于 AspectJ 的编译器。下面我们先通过一个简单的案例来演示Spring AOP的入门程序
定义目标类接口和实现类
/** * Created by zejian on 2017/2/19. * Blog : [原文地址,请尊重原创] */ //接口类public interface UserDao { int addUser(); void updateUser(); void deleteUser(); void findUser();}//实现类import com.zejian.spring.springAop.dao.UserDao;import org.springframework.stereotype.Repository;/** * Created by zejian on 2017/2/19. * Blog : [原文地址,请尊重原创] */@Repositorypublic class UserDaoImp implements UserDao { @Override public int addUser() { System.out.println("add user ......"); return 6666; } @Override public void updateUser() { System.out.println("update user ......"); } @Override public void deleteUser() { System.out.println("delete user ......"); } @Override public void findUser() { System.out.println("find user ......"); }}
使用Spring 2.0引入的注解方式,编写Spring AOP的aspect 类:
@Aspectpublic class MyAspect { /** * 前置通知 */ @Before("execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))") public void before(){ System.out.println("前置通知...."); } /** * 后置通知 * returnVal,切点方法执行后的返回值 */ @AfterReturning(value="execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))",returning = "returnVal") public void AfterReturning(Object returnVal){ System.out.println("后置通知...."+returnVal); } /** * 环绕通知 * @param joinPoint 可用于执行切点的类 * @return * @throws Throwable */ @Around("execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("环绕通知前...."); Object obj= (Object) joinPoint.proceed(); System.out.println("环绕通知后...."); return obj; } /** * 抛出通知 * @param e */ @AfterThrowing(value="execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))",throwing = "e") public void afterThrowable(Throwable e){ System.out.println("出现异常:msg="+e.getMessage()); } /** * 无论什么情况下都会执行的方法 */ @After(value="execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))") public void after(){ System.out.println("最终通知...."); }}
编写配置文件交由Spring IOC容器管理
<beans xmlns="; xmlns:xsi="; xmlns:aop="; xmlns:context="; xsi:schemaLocation=" ;> <!-- 启动@aspectj的自动代理支持--> <aop:aspectj-autoproxy /> <!-- 定义目标对象 --> <bean id="userDaos" class="com.zejian.spring.springAop.dao.daoimp.UserDaoImp" /> <!-- 定义aspect类 --> <bean name="myAspectJ" class="com.zejian.spring.springAop.AspectJ.MyAspect"/></beans>
编写测试类
/** * Created by zejian on 2017/2/19. * Blog : [原文地址,请尊重原创] */@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations= ";)public class UserDaoAspectJ { @Autowired UserDao userDao; @Test public void aspectJTest(){ userDao.addUser(); }}
简单说明一下,定义了一个目标类UserDaoImpl,利用Spring2.0引入的aspect注解开发功能定义aspect类即MyAspect,在该aspect类中,编写了5种注解类型的通知函数,分别是前置通知@Before、后置通知@AfterReturning、环绕通知@Around、异常通知@AfterThrowing、最终通知@After,这5种通知与前面分析AspectJ的通知类型几乎是一样的,并注解通知上使用execution关键字定义的切点表达式,即指明该通知要应用的目标函数,当只有一个execution参数时,value属性可以省略,当含两个以上的参数,value必须注明,如存在返回值时。当然除了把切点表达式直接传递给通知注解类型外,还可以使用@pointcut来定义切点匹配表达式,这个与AspectJ使用关键字pointcut是一样的,后面分析。目标类和aspect类定义完成后,最后需要在xml配置文件中进行配置,同样的所有类的创建都交由SpringIOC容器处理,注意,使用Spring AOP 的aspectJ功能时,需要使用以下代码启动aspect的注解功能支持:
< />
ok~,运行程序,结果符合预期:
通过简单案例的分析,也就很容易知道,Spring AOP 的实现是遵循AOP规范的,特别是以AspectJ(与java无缝整合)为参考,因此在AOP的术语概念上与前面分析的AspectJ的AOP术语是一样的,如切点(pointcut)定义需要应用通知的目标函数,通知则是那些需要应用到目标函数而编写的函数体,切面(Aspect)则是通知与切点的结合。织入(weaving),将aspect类应用到目标函数(类)的过程,只不过Spring AOP底层是通过动态代理技术实现罢了。
在案例中,定义过滤切入点函数时,是直接把execution已定义匹配表达式作为值传递给通知类型的如下:
@After(value="execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))") public void after(){ System.out.println("最终通知...."); }
除了上述方式外,还可采用与ApectJ中使用pointcut关键字类似的方式定义切入点表达式如下,使用@Pointcut注解:
/** * 使用Pointcut定义切点 */@Pointcut("execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))")private void myPointcut(){}/** * 应用切入点函数 */@After(value="myPointcut()")public void afterDemo(){ System.out.println("最终通知....");}
使用@Pointcut注解进行定义,应用到通知函数afterDemo()时直接传递切点表达式的函数名称myPointcut()即可,比较简单,下面接着介绍切点指示符。
为了方法通知应用到相应过滤的目标方法上,SpringAOP提供了匹配表达式,这些表达式也叫切入点指示符,在前面的案例中,它们已多次出现。
通配符
在定义匹配表达式时,通配符几乎随处可见,如*、.. 、+ ,它们的含义如下:
.. :匹配方法定义中的任意数量的参数,此外还匹配类定义中的任意数量包
//任意返回值,任意名称,任意参数的公共方法execution(public * *(..))//匹配com.zejian.dao包及其子包中所有类中的所有方法within(com.zejian.dao..*)
//匹配实现了DaoUser接口的所有子类的方法within(com.zejian.dao.DaoUser+)
//匹配com.zejian.service包及其子包中所有类的所有方法within(com.zejian.service..*)//匹配以set开头,参数为int类型,任意返回值的方法execution(* set*(int))
类型签名表达式
为了方便类型(如接口、类名、包名)过滤方法,Spring AOP 提供了within关键字。其语法格式如下:
within(<type name>)
type name 则使用包名或者类名替换即可,来点案例吧。
//匹配com.zejian.dao包及其子包中所有类中的所有方法@Pointcut("within(com.zejian.dao..*)")//匹配UserDaoImpl类中所有方法@Pointcut("within(com.zejian.dao.UserDaoImpl)")//匹配UserDaoImpl类及其子类中所有方法@Pointcut("within(com.zejian.dao.UserDaoImpl+)")//匹配所有实现UserDao接口的类的所有方法@Pointcut("within(com.zejian.dao.UserDao+)")
方法签名表达式
如果想根据方法签名进行过滤,关键字execution可以帮到我们,语法表达式如下
//scope :方法作用域,如public,private,protect//returnt-type:方法返回值类型//fully-qualified-class-name:方法所在类的完全限定名称//parameters 方法参数execution(<scope> <return-type> <fully-qualified-class-name>.*(parameters))
对于给定的作用域、返回值类型、完全限定类名以及参数匹配的方法将会应用切点函数指定的通知,这里给出模型案例:
//匹配UserDaoImpl类中的所有方法@Pointcut("execution(* com.zejian.dao.UserDaoImpl.*(..))")//匹配UserDaoImpl类中的所有公共的方法@Pointcut("execution(public * com.zejian.dao.UserDaoImpl.*(..))")//匹配UserDaoImpl类中的所有公共方法并且返回值为int类型@Pointcut("execution(public int com.zejian.dao.UserDaoImpl.*(..))")//匹配UserDaoImpl类中第一个参数为int类型的所有公共的方法@Pointcut("execution(public * com.zejian.dao.UserDaoImpl.*(int , ..))")
其他指示符
bean:Spring AOP扩展的,AspectJ没有对于指示符,用于匹配特定名称的Bean对象的执行方法;
//匹配名称中带有后缀Service的Bean。@Pointcut("bean(*Service)")private void myPointcut1(){}
//匹配了任意实现了UserDao接口的代理对象的方法进行过滤@Pointcut("this(com.zejian.spring.springAop.dao.UserDao)")private void myPointcut2(){}
//匹配了任意实现了UserDao接口的目标对象的方法进行过滤@Pointcut("target(com.zejian.spring.springAop.dao.UserDao)")private void myPointcut3(){}
//匹配使用了MarkerAnnotation注解的类(注意是类)@Pointcut("@within(com.zejian.spring.annotation.MarkerAnnotation)")private void myPointcut4(){}
//匹配使用了MarkerAnnotation注解的方法(注意是方法)@Pointcut("@annotation(com.zejian.spring.annotation.MarkerAnnotation)")private void myPointcut5(){}
ok~,关于表达式指示符就介绍到这,我们主要关心前面几个常用的即可,不常用过印象即可。这里最后说明一点,切点指示符可以使用运算符语法进行表达式的混编,如and、or、not(或者&&、||、!),如下一个简单例子:
//匹配了任意实现了UserDao接口的目标对象的方法并且该接口不在com.zejian.dao包及其子包下@Pointcut("target(com.zejian.spring.springAop.dao.UserDao) !within(com.zejian.dao..*)")private void myPointcut6(){}//匹配了任意实现了UserDao接口的目标对象的方法并且该方法名称为addUser@Pointcut("target(com.zejian.spring.springAop.dao.UserDao)&&execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))")private void myPointcut7(){}
5种通知函数
通知在前面的aspectJ和Spring AOP的入门案例已见过面,在Spring中与AspectJ一样,通知主要分5种类型,分别是前置通知、后置通知、异常通知、最终通知以及环绕通知,下面分别介绍。
前置通知@Before
前置通知通过@Before注解进行标注,并可直接传入切点表达式的值,该通知在目标函数执行前执行,注意JoinPoint,是Spring提供的静态变量,通过joinPoint 参数,可以获取目标对象的信息,如类名称,方法参数,方法名称等,,该参数是可选的。
/** * 前置通知 * @param joinPoint 该参数可以获取目标对象的信息,如类名称,方法参数,方法名称等 */@Before("execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))")public void before(JoinPoint joinPoint){ System.out.println("我是前置通知");}
通过@AfterReturning注解进行标注,该函数在目标函数执行完成后执行,并可以获取到目标函数最终的返回值returnVal,当目标函数没有返回值时,returnVal将返回null,必须通过returning = “returnVal”注明参数的名称而且必须与通知函数的参数名称相同。请注意,在任何通知中这些参数都是可选的,需要使用时直接填写即可,不需要使用时,可以完成不用声明出来。如下
/*** 后置通知,不需要参数时可以不提供*/@AfterReturning(value="execution(* com.zejian.spring.springAop.dao.UserDao.*User(..))")public void AfterReturning(){ System.out.println("我是后置通知...");}
/*** 后置通知* returnVal,切点方法执行后的返回值*/@AfterReturning(value="execution(* com.zejian.spring.springAop.dao.UserDao.*User(..))",returning = "returnVal")public void AfterReturning(JoinPoint joinPoint,Object returnVal){ System.out.println("我是后置通知...returnVal+"+returnVal);}
该通知只有在异常时才会被触发,并由throwing来声明一个接收异常信息的变量,同样异常通知也用于Joinpoint参数,需要时加上即可,如下:
/*** 抛出通知* @param e 抛出异常的信息*/@AfterThrowing(value="execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))",throwing = "e")public void afterThrowable(Throwable e){ System.out.println("出现异常:msg="+e.getMessage());}
该通知有点类似于finally代码块,只要应用了无论什么情况下都会执行。
/** * 无论什么情况下都会执行的方法 * joinPoint 参数 */@After("execution(* com.zejian.spring.springAop.dao.UserDao.*User(..))")public void after(JoinPoint joinPoint) { System.out.println("最终通知....");}
环绕通知既可以在目标方法前执行也可在目标方法之后执行,更重要的是环绕通知可以控制目标方法是否指向执行,但即使如此,我们应该尽量以最简单的方式满足需求,在仅需在目标方法前执行时,应该采用前置通知而非环绕通知。案例代码如下第一个参数必须是ProceedingJoinPoint,通过该对象的proceed()方法来执行目标函数,proceed()的返回值就是环绕通知的返回值。同样的,ProceedingJoinPoint对象也是可以获取目标对象的信息,如类名称,方法参数,方法名称等等。
@Around("execution(* com.zejian.spring.springAop.dao.UserDao.*User(..))")public Object around(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("我是环绕通知前...."); //执行目标函数 Object obj= (Object) joinPoint.proceed(); System.out.println("我是环绕通知后...."); return obj;}
通知传递参数
在Spring AOP中,除了execution和bean指示符不能传递参数给通知方法,其他指示符都可以将匹配的方法相应参数或对象自动传递给通知方法。获取到匹配的方法参数后通过”argNames”属性指定参数名。如下,需要注意的是args(指示符)、argNames的参数名与before()方法中参数名 必须保持一致即param。
@Before(value="args(param)", argNames="param") //明确指定了 public void before(int param) { System.out.println("param:" + param); }
当然也可以直接使用args指示符不带argNames声明参数,如下:
@Before("execution(public * com.zejian..*.addUser(..)) && args(userId,..)") public void before(int userId) { //调用addUser的方法时如果与addUser的参数匹配则会传递进来会传递进来 System.out.println("userId:" + userId); }
args(userId,..)该表达式会保证只匹配那些至少接收一个参数而且传入的类型必须与userId一致的方法,记住传递的参数可以简单类型或者对象,而且只有参数和目标方法也匹配时才有会有值传递进来。来个
在不同的切面中,如果有多个通知需要在同一个切点函数指定的过滤目标方法上执行,那些在目标方法前执行(”进入”)的通知函数,最高优先级的通知将会先执行,在执行在目标方法后执行(“退出”)的通知函数,最高优先级会最后执行。而对于在同一个切面定义的通知函数将会根据在类中的声明顺序执行。如下:
package com.zejian.spring.springAop.AspectJ;import org.aspectj.lang.annotation.AfterReturning;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;/** * Created by zejian on 2017/2/20. * Blog : [原文地址,请尊重原创] */@Aspectpublic class AspectOne { /** * Pointcut定义切点函数 */ @Pointcut("execution(* com.zejian.spring.springAop.dao.UserDao.deleteUser(..))") private void myPointcut(){} @Before("myPointcut()") public void beforeOne(){ System.out.println("前置通知....执行顺序1"); } @Before("myPointcut()") public void beforeTwo(){ System.out.println("前置通知....执行顺序2"); } @AfterReturning(value = "myPointcut()") public void AfterReturningThree(){ System.out.println("后置通知....执行顺序3"); } @AfterReturning(value = "myPointcut()") public void AfterReturningFour(){ System.out.println("后置通知....执行顺序4"); }}
在同一个切面中定义多个通知响应同一个切点函数,执行顺序为声明顺序:
如果在不同的切面中定义多个通知响应同一个切点,进入时则优先级高的切面类中的通知函数优先执行,退出时则最后执行,如下定义AspectOne类和AspectTwo类并实现org.springframework.core.Ordered 接口,该接口用于控制切面类的优先级,同时重写getOrder方法,定制返回值,返回值(int 类型)越小优先级越大。其中AspectOne返回值为0,AspectTwo的返回值为3,显然AspectOne优先级高于AspectTwo。
/** * Created by zejian on 2017/2/20. * Blog : [原文地址,请尊重原创] */@Aspectpublic class AspectOne implements Ordered { /** * Pointcut定义切点函数 */ @Pointcut("execution(* com.zejian.spring.springAop.dao.UserDao.deleteUser(..))") private void myPointcut(){} @Before("myPointcut()") public void beforeOne(){ System.out.println("前置通知..AspectOne..执行顺序1"); } @Before("myPointcut()") public void beforeTwo(){ System.out.println("前置通知..AspectOne..执行顺序2"); } @AfterReturning(value = "myPointcut()") public void AfterReturningThree(){ System.out.println("后置通知..AspectOne..执行顺序3"); } @AfterReturning(value = "myPointcut()") public void AfterReturningFour(){ System.out.println("后置通知..AspectOne..执行顺序4"); } /** * 定义优先级,值越低,优先级越高 * @return */ @Override public int getOrder() { return 0; }}//切面类 AspectTwo.java@Aspectpublic class AspectTwo implements Ordered { /** * Pointcut定义切点函数 */ @Pointcut("execution(* com.zejian.spring.springAop.dao.UserDao.deleteUser(..))") private void myPointcut(){} @Before("myPointcut()") public void beforeOne(){ System.out.println("前置通知....执行顺序1--AspectTwo"); } @Before("myPointcut()") public void beforeTwo(){ System.out.println("前置通知....执行顺序2--AspectTwo"); } @AfterReturning(value = "myPointcut()") public void AfterReturningThree(){ System.out.println("后置通知....执行顺序3--AspectTwo"); } @AfterReturning(value = "myPointcut()") public void AfterReturningFour(){ System.out.println("后置通知....执行顺序4--AspectTwo"); } /** * 定义优先级,值越低,优先级越高 * @return */ @Override public int getOrder() { return 2; }}
运行结果如下:
案例中虽然只演示了前置通知和后置通知,但其他通知也遵循相同的规则,有兴趣可自行测试。到此基于注解的Spring AOP 分析就结束了,但请注意,在配置文件中启动@Aspect支持后,Spring容器只会尝试自动识别带@Aspect的Bean,前提是任何定义的切面类都必须已在配置文件以Bean的形式声明。
<beans xmlns="; xmlns:xsi="; xmlns:aop="; xmlns:context="; xsi:schemaLocation=" ;> <!--<context:component-scan base-package=""--> <!-- 启动@aspectj的自动代理支持--> <aop:aspectj-autoproxy /> <!-- 定义目标对象 --> <bean id="userDaos" class="com.zejian.spring.springAop.dao.daoimp.UserDaoImp" /> <!-- 定义aspect类 --> <bean name="myAspectJ" class="com.zejian.spring.springAop.AspectJ.MyAspect"/> <bean name="aspectOne" class="com.zejian.spring.springAop.AspectJ.AspectOne" /> <bean name="aspectTwo" class="com.zejian.spring.springAop.AspectJ.AspectTwo" /></beans>
前面分析完基于注解支持的开发是日常应用中最常见的,即使如此我们还是有必要了解一下基于xml形式的Spring AOP开发,这里会以一个案例的形式对xml的开发形式进行简要分析,定义一个切面类
/** * Created by zejian on 2017/2/20. * Blog : [原文地址,请尊重原创] */public class MyAspectXML { public void before(){ System.out.println("MyAspectXML====前置通知"); } public void afterReturn(Object returnVal){ System.out.println("后置通知-->返回值:"+returnVal); } public Object around(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("MyAspectXML=====环绕通知前"); Object object= joinPoint.proceed(); System.out.println("MyAspectXML=====环绕通知后"); return object; } public void afterThrowing(Throwable throwable){ System.out.println("MyAspectXML======异常通知:"+ throwable.getMessage()); } public void after(){ System.out.println("MyAspectXML=====最终通知..来了"); }}
通过配置文件的方式声明如下(spring-aspectj-xml.xml):
<beans xmlns="; xmlns:xsi="; xmlns:aop="; xmlns:context="; xsi:schemaLocation=" ;> <!--<context:component-scan base-package=""--> <!-- 定义目标对象 --> <bean name="productDao" class="com.zejian.spring.springAop.dao.daoimp.ProductDaoImpl" /> <!-- 定义切面 --> <bean name="myAspectXML" class="com.zejian.spring.springAop.AspectJ.MyAspectXML" /> <!-- 配置AOP 切面 --> <aop:config> <!-- 定义切点函数 --> <aop:pointcut id="pointcut" expression="execution(* com.zejian.spring.springAop.dao.ProductDao.add(..))" /> <!-- 定义其他切点函数 --> <aop:pointcut id="delPointcut" expression="execution(* com.zejian.spring.springAop.dao.ProductDao.delete(..))" /> <!-- 定义通知 order 定义优先级,值越小优先级越大--> <aop:aspect ref="myAspectXML" order="0"> <!-- 定义通知 method 指定通知方法名,必须与MyAspectXML中的相同 pointcut 指定切点函数 --> <aop:before method="before" pointcut-ref="pointcut" /> <!-- 后置通知 returning="returnVal" 定义返回值 必须与类中声明的名称一样--> <aop:after-returning method="afterReturn" pointcut-ref="pointcut" returning="returnVal" /> <!-- 环绕通知 --> <aop:around method="around" pointcut-ref="pointcut" /> <!--异常通知 throwing="throwable" 指定异常通知错误信息变量,必须与类中声明的名称一样--> <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="throwable"/> <!-- method : 通知的方法(最终通知) pointcut-ref : 通知应用到的切点方法 --> <aop:after method="after" pointcut-ref="pointcut"/> </aop:aspect> </aop:config></beans>
声明方式和定义方式在代码中已很清晰了,了解一下即可,在实际开发中,会更倾向与使用注解的方式开发,毕竟更简单更简洁。
接下来的Spring AOP应用案例将会加深我们对于其的熟悉程度,这里将会模拟两种开发中常用的场景,注意这些场景的代码并不一定完全适合所有情况,但它们确实是可运用到实际软件开发中的,同时案例中的代码并不会以完整的项目演示而更倾向于模拟AOP的应用代码,那就由此开始吧。性能监测对于成熟的系统是不可或缺的,以下的代码段将程序消耗时间方面给我们带来直观的数据(消耗时间的大小):
// 计算消耗的时间long start = System.currentTimeMillis();//其他代码操作long time = System.currentTimeMillis() - start;
可预知这样的代码将系统中多次出现而且横跨各个模块,显然可以抽取横切关注点,使用AOP来完成,下面的代码将会模拟监测api接口访问的性能并记录每个接口的名称包含类名以及消耗时间等,简单定义一个监控信息记录类:
/** * Created by wuzejian on 2017/2/20. * 性能监控信息类,简单模拟 */public class MonitorTime { private String className; private String methodName; private Date logTime; private long comsumeTime; //......其他属性 //省略set 和 get }
定义一个监控的切面类:
/** * Created by wuzejian on 2017/2/20. * API接口性能分析 * 这里只是简单模拟,可以根据业务需求自行定义 */@Aspectpublic class TimerAspect { /** * 定义切点函数,过滤controller包下的名称以Controller结尾的类所有方法 */ @Pointcut("execution(* com.zejian.spring.controller.*Controller.*(..))") void timer() { } @Around("timer()") public Object logTimer(ProceedingJoinPoint thisJoinPoint) throws Throwable { MonitorTime monitorTime=new MonitorTime(); //获取目标类名称 String clazzName = thisJoinPoint.getTarget().getClass().getName(); //获取目标类方法名称 String methodName = thisJoinPoint.getSignature().getName(); monitorTime.setClassName(clazzName);//记录类名称 monitorTime.setMethodName(methodName);//记录对应方法名称 monitorTime.setLogTime(new Date());//记录时间 // 计时并调用目标函数 long start = System.currentTimeMillis(); Object result = thisJoinPoint.proceed(); long time = System.currentTimeMillis() - start; //设置消耗时间 monitorTime.setComsumeTime(time); //把monitorTime记录的信息上传给监控系统,并没有实现,需要自行实现即可 //MonitoruUtils.report(monitorTime) return result; }}
然后在xml文件声明该切面:
<beans xmlns="; xmlns:xsi="; xmlns:aop="; xmlns:context="; xsi:schemaLocation=" ;> <!--<context:component-scan base-package=""--> <!-- 启动@aspectj的自动代理支持--> <aop:aspectj-autoproxy /> <!-- 定义目标对象 --> <bean id="userController" class="com.zejian.spring.controller.UserController" /> <!-- 定义aspect类 --> <bean name="timerAspect" class="com.zejian.spring.SpringAopAction.TimerAspect"/></beans>
在每次访问切点函数定义过滤的方法时就计算出每个接口消耗的时间,以此来作为访问接口的性能指标。除了性能监控,可能还有比这更重要的异常监控,而异常的处理在各个模块中都是不可避免的,为了避免到处编写try/catch,统一处理异常是个非常不错的主意,Spring AOP 显然可以胜任 ,我们完全可以在切面类中使用环绕通知统一处理异常信息,并把异常信息封装上报给后台日志系统,以下的代码将体现出这一机智的操作。首先编写一个异常封装类(仅简单模拟):
/** * Created by wuzejian on 2017/2/20. * 封装异常信息的类 */public class ExceptionInfo { private String className; private String methodName; private Date logTime;//异常记录时间 private String message;//异常信息 //.....其他属性 //省略set get }
统一处理异常的切面类如下:
/** * Created by wuzejian on 2017/2/20. * 异常监测切面类 */@Aspectpublic class ExceptionMonitor { /** * 定义异常监控类 */ @Pointcut("execution(com.zejian.spring *(..))") void exceptionMethod() { } @Around("exceptionMethod()") public Object monitorMethods(ProceedingJoinPoint thisJoinPoint) { try { return thisJoinPoint.proceed(); } catch (Throwable e) { ExceptionInfo info=new ExceptionInfo(); //异常类记录 info.setClassName(thisJoinPoint.getTarget().getClass().getName()); info.setMethodName(thisJoinPoint.getSignature().getName()); info.setLogTime(new Date()); info.setMessage(e.toString()); //上传日志系统,自行完善 //ExceptionReportUtils.report(info); return null; } }}
关于异常监控ExceptionMonitor中的通知函数体,这里仅简单演示代码实现,实际开发中根据业务需求修改即可。AOP的应用远不止这两种,诸如缓存,权限验证、内容处理、事务控制等都可以使用AOP实现,其中事务控制Spring中提供了专门的处理方式,限于篇幅就先聊到这。
前面的分析中,我们谈到Spring AOP的实现原理是基于动态织入的动态代理技术,而AspectJ则是静态织入,而动态代理技术又分为Java JDK动态代理和CGLIB动态代理,前者是基于反射技术的实现,后者是基于继承的机制实现,下面通过一个简单的例子来分析这两种技术的代码实现。
先看一个简单的例子,声明一个A类并实现ExInterface接口,利用JDK动态代理技术在execute()执行前后织入权限验证和日志记录,注意这里仅是演示代码并不代表实际应用。
/** * Created by zejian on 2017/2/11. * Blog : [原文地址,请尊重原创] *///自定义的接口类,JDK动态代理的实现必须有对应的接口类public interface ExInterface { void execute();}//A类,实现了ExInterface接口类public class A implements ExInterface{ public void execute(){ System.out.println("执行A的execute方法..."); }}//代理类的实现public class JDKProxy implements InvocationHandler{ /** * 要被代理的目标对象 */ private A target; public JDKProxy(A target){ this.target=target; } /** * 创建代理类 * @return */ public ExInterface createProxy(){ return (ExInterface) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this); } /** * 调用被代理类(目标对象)的任意方法都会触发invoke方法 * @param proxy 代理类 * @param method 被代理类的方法 * @param args 被代理类的方法参数 * @return * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //过滤不需要该业务的方法 if("execute".equals(method.getName())) { //调用前验证权限 AuthCheck.authCheck(); //调用目标对象的方法 Object result = method.invoke(target, args); //记录日志数据 Report.recordLog(); return result; }eles if("delete".equals(method.getName())){ //..... } //如果不需要增强直接执行原方法 return method.invoke(target,args); }} //测试验证 public static void main(String args[]){ A a=new A(); //创建JDK代理 JDKProxy jdkProxy=new JDKProxy(a); //创建代理对象 ExInterface proxy=jdkProxy.createProxy(); //执行代理对象方法 proxy.execute(); }
运行结果:
在A的execute方法里面并没有调用任何权限和日志的代码,也没有直接操作a对象,相反地只是调用了proxy代理对象的方法,最终结果却是预期的,这就是动态代理技术,是不是跟Spring AOP似曾相识?实际上动态代理的底层是通过反射技术来实现,只要拿到A类的class文件和A类的实现接口,很自然就可以生成相同接口的代理类并调用a对象的方法了,关于底层反射技术的实现,暂且不过多讨论,请注意实现java的动态代理是有先决条件的,该条件是目标对象必须带接口,如A类的接口是ExInterface,通过ExInterface接口动态代理技术便可以创建与A类类型相同的代理对象,如下代码演示了创建并调用时利用多态生成的proxy对象:
A a=new A(); //创建JDK代理类 JDKProxy jdkProxy=new JDKProxy(a); //创建代理对象,代理对象也实现了ExInterface,通过Proxy实现 ExInterface proxy=jdkProxy.createProxy(); //执行代理对象方法 proxy.execute();
代理对象的创建是通过Proxy类达到的,Proxy类由Java JDK提供,利用Proxy#newProxyInstance方法便可以动态生成代理对象(proxy),底层通过反射实现的,该方法需要3个参数
/*** @param loader 类加载器,一般传递目标对象(A类即被代理的对象)的类加载器* @param interfaces 目标对象(A)的实现接口* @param h 回调处理句柄(后面会分析到)*/public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
创建代理类proxy的代码如下:
public ExInterface createProxy(){ return (ExInterface) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this); }
到此并没结束,因为有接口还是远远不够,代理类(Demo中的JDKProxy)还需要实现InvocationHandler接口,也是由JDK提供,代理类必须实现的并重写invoke方法,完全可以把InvocationHandler看成一个回调函数(Callback),Proxy方法创建代理对象proxy后,当调用execute方法(代理对象也实现ExInterface)时,将会回调InvocationHandler#invoke方法,因此我们可以在invoke方法中来控制被代理对象(目标对象)的方法执行,从而在该方法前后动态增加其他需要执行的业务,Demo中的代码很好体现了这点:
@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //过滤不需要该业务的方法 if("execute".equals(method.getName())) { //调用前验证权限(动态添加其他要执行业务) AuthCheck.authCheck(); //调用目标对象的方法(执行A对象即被代理对象的execute方法) Object result = method.invoke(target, args); //记录日志数据(动态添加其他要执行业务) Report.recordLog(); return result; }eles if("delete".equals(method.getName())){ //..... return method.invoke(target, args); } //如果不需要增强直接执行原方法 return method.invoke(target,args);
invoke方法有三个参数:
这就是Java JDK动态代理的代码实现过程,小结一下,运用JDK动态代理,被代理类(目标对象,如A类),必须已有实现接口如(ExInterface),因为JDK提供的Proxy类将通过目标对象的类加载器ClassLoader和Interface,以及句柄(Callback)创建与A类拥有相同接口的代理对象proxy,该代理对象将拥有接口ExInterface中的所有方法,同时代理类必须实现一个类似回调函数的InvocationHandler接口并重写该接口中的invoke方法,当调用proxy的每个方法(如案例中的proxy#execute())时,invoke方法将被调用,利用该特性,可以在invoke方法中对目标对象(被代理对象如A)方法执行的前后动态添加其他外围业务操作,此时无需触及目标对象的任何代码,也就实现了外围业务的操作与目标对象(被代理对象如A)完全解耦合的目的。当然缺点也很明显需要拥有接口,这也就有了后来的CGLIB动态代理了
通过CGLIB动态代理实现上述功能并不要求目标对象拥有接口类,实际上CGLIB动态代理是通过继承的方式实现的,因此可以减少没必要的接口,下面直接通过简单案例协助理解(CGLIB是一个开源项目,github网址是:GitHub - cglib/cglib: cglib - Byte Code Generation Library is high level API to generate and transform Java byte code. It is used by AOP, testing, data access frameworks to generate dynamic proxy objects and intercept field access.)。
//被代理的类即目标对象public class A { public void execute(){ System.out.println("执行A的execute方法..."); }}//代理类public class CGLibProxy implements MethodInterceptor { /** * 被代理的目标类 */ private A target; public CGLibProxy(A target) { super(); this.target = target; } /** * 创建代理对象 * @return */ public A createProxy(){ // 使用CGLIB生成代理: // 1.声明增强类实例,用于生产代理类 Enhancer enhancer = new Enhancer(); // 2.设置被代理类字节码,CGLIB根据字节码生成被代理类的子类 enhancer.setSuperclass(target.getClass()); // 3.//设置回调函数,即一个方法拦截 enhancer.setCallback(this); // 4.创建代理: return (A) enhancer.create(); } /** * 回调函数 * @param proxy 代理对象 * @param method 委托类方法 * @param args 方法参数 * @param methodProxy 每个被代理的方法都对应一个MethodProxy对象, * methodProxy.invokeSuper方法最终调用委托类(目标类)的原始方法 * @return * @throws Throwable */ @Override public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { //过滤不需要该业务的方法 if("execute".equals(method.getName())) { //调用前验证权限(动态添加其他要执行业务) AuthCheck.authCheck(); //调用目标对象的方法(执行A对象即被代理对象的execute方法) Object result = methodProxy.invokeSuper(proxy, args); //记录日志数据(动态添加其他要执行业务) Report.recordLog(); return result; }else if("delete".equals(method.getName())){ //..... return methodProxy.invokeSuper(proxy, args); } //如果不需要增强直接执行原方法 return methodProxy.invokeSuper(proxy, args); }}
从代码看被代理的类无需接口即可实现动态代理,而CGLibProxy代理类需要实现一个方法拦截器接口MethodInterceptor并重写intercept方法,类似JDK动态代理的InvocationHandler接口,也是理解为回调函数,同理每次调用代理对象的方法时,intercept方法都会被调用,利用该方法便可以在运行时对方法执行前后进行动态增强。关于代理对象创建则通过Enhancer类来设置的,Enhancer是一个用于产生代理对象的类,作用类似JDK的Proxy类,因为CGLib底层是通过继承实现的动态代理,因此需要传递目标对象(如A)的Class,同时需要设置一个回调函数对调用方法进行拦截并进行相应处理,最后通过create()创建目标对象(如A)的代理对象,运行结果与前面的JDK动态代理效果相同。
public A createProxy(){ // 1.声明增强类实例,用于生产代理类 Enhancer enhancer = new Enhancer(); // 2.设置被代理类字节码,CGLIB根据字节码生成被代理类的子类 enhancer.setSuperclass(target.getClass()); // 3.设置回调函数,即一个方法拦截 enhancer.setCallback(this); // 4.创建代理: return (A) enhancer.create(); }
关于JDK代理技术 和 CGLIB代理技术到这就讲完了,我们也应该明白 Spring AOP 确实是通过 CGLIB或者JDK代理 来动态地生成代理对象,这个代理对象指的就是 AOP 代理累,而 AOP 代理类的方法则通过在目标对象的切入点动态地织入增强处理,从而完成了对目标方法的增强。这里并没有非常深入去分析这两种技术,更倾向于向大家演示Spring AOP 底层实现的最简化的模型代码,Spring AOP内部已都实现了这两种技术,Spring AOP 在使用时机上也进行自动化调整,当有接口时会自动选择JDK动态代理技术,如果没有则选择CGLIB技术,当然Spring AOP的底层实现并没有这么简单,为更简便生成代理对象,Spring AOP 内部实现了一个专注于生成代理对象的工厂类,这样就避免了大量的手动编码,这点也是十分人性化的,但最核心的还是动态代理技术。从性能上来说,Spring AOP 虽然无需特殊编译器协助,但性能上并不优于AspectJ的静态织入,这点了解一下即可。最后给出Spring AOP简化的原理模型如下图,附属简要源码,【GITHUB源码下载】