详解Spring的AOP切面编程
一 、基本理解
AOP,面向切面编程,作为Spring的核心思想之一,度娘上有太多的教程啊、解释啊,但博主还是要自己按照自己的思路和理解再来阐释一下。原因很简单,别人的思想终究是别人的,自己的理解才是自己的,尤其当用文字、代码来阐述一遍过后,理解层面上又似乎变得不一样了。
博主就不概念化解释AOP了,这里只简单说下为啥要使用这样一种编程思想和相关的AOP技术。其实很简单,就是为了业务模块间的解耦,尤其在现代的软件设计中强调高内聚、低耦合,要求我们的业务模块化,各个功能模块只关注自己的逻辑实现,而不用关注与主业务逻辑不相关的功能。然而,在面向对象的系统设计中,系统中不可或缺的一些功能如日志、事务是散布在应用各处与主逻辑代码高度耦合的,这让主业务代码变得相当冗余、难以复用。而在面向切面的编程思想中,我们是考虑将那些散布在应用多处的重复性代码抽离出来封装成模块化的功能类,一来让主业务逻辑更加专注、简单,二来模块化的日志、事务也便于复用和移植,这就是解耦的思想。但是,解耦并不等于断耦,抽离的功能最终还是要以某种方式"还"(qie)回去,否则应用的功能就不完善了。这里,"还"(qie)回去的技术就是AOP技术,而这种解耦的编程思想就是AOP的编程思想。在Java的生态中,提供AOP技术的框架也有不少,主要的运用就是Spring的AOP和Spring"借鉴"并包含进了自己的生态体系的 AspectJ的AOP。
二 、核心概念
为便于理解阐述,博主先唠叨几句。上面的基本阐述中,我们知道,AOP要干的事情其实也很简单,就是要将对象编程中,抽离出来的模块代码(权限、日志、事务)还(qie)回去,但肯定不能是对象思维中的代码冗杂的组合,而是应该更加高明一些,最好能在原来的业务代码执行的过程中不知不觉的还(qie)回去——也就是说要在主业务逻辑执行的流程里,动态的添加(权限、日志、事务)代码抽离前干的那些事情。怎么能做到呢?用代理啊,亲!想想,我们对一个目标对象采用代理不就是为了在目标对象逻辑执行时候通过在代理对象中干点额外的事情吗?这样,虽然,原目标对象并没有增加任何额外的功能,通过代理的一番暗中骚操作,展示给调用者的就好像目标对象有了代理对象中的那些额外的功能一样。于是你也很好理解,为什么Spring的AOP中要用到动态代理了。好了,经过一番唠叨,我们再来看AOP的相关术语就要好理解得多——
1、横切关注点
如上描述,我们把日志、事务、权限等代码重复性极高却散布在应用程序各个地方的功能称为横切关注点。
2、连接点(Join Point)
被代理的目标对象在业务逻辑执行的过程中,可以被代理对象动态切入代理功能的一些时机节点,比如方法执行前、后,异常时,成功返回时等等。当然,这只是针对Spring来说的,因为Spring基于动态代理,只支持方法级别的AOP切入,实际上,AspectJ、JBoss等框架的AOP还能提供构造器以及更细粒度字段等的连接点支持。
3、通知(Advice)
如上描述,就是代理对象在什么时机要为目标对象额外增加的功能代码,因而很多教程资料上称之为 增强。请注意博主对通知的描述里有提到什么时机,这很好理解,你的代理对象要给目标对象增加额外功能,总得清楚要增加在哪些时机吧,所以,我们的通知按照功能切入的时机分为以下5个类型:
- 前置通知(Before):被代理对象目标方法被调用之前执行通知代码;
- 后置通知(After):被代理对象目标方法执行完成之后执行通知代码,不管方法是否成功执行(这相当于异常捕获中的finally块,总是会执行的意思,所以博主觉得如果将其命名为最终通知要更好理解些);
- 异常通知(After-throwing):被代理对象目标方法抛出异常后执行通知代码;
- 返回通知(After-returning):被代理对象目标方法成功执行后执行通知代码;
- 环绕通知(Around) :包裹被代理对象的目标方法,相当于结合了以上的所有通知类型。
4、切点(Pointcut)
被代理对象目标方法执行过程中真正的要执行通知代码的一个或多个连接点,这会通过切点表达式语言进行匹配。
## 6、切面(Aspect)
通知和切点的结合,切面完整的包含了代理对象对目标对象进行通知的三个基本要素:何时(前、后、异常、环绕、返回等),何地(切点),干什么(通知切入的功能)。
## 7、织入(Weaving)
将切面应用到被代理对象并创建代理对象的的过程。切面会在指定的连接点(切点)被织入到被代理对象的执行方法中。其实,被代理对象的生命周期中有多个时机(编译、类加载、运行)都可以进行织入,就 Spring 而言,是在被代理对象运行期进行代理对象的创建,织入切面逻辑的。
注:以上描述都是基于Spring 方法级别的AOP 来进行阐述
三、基础代码示例
说了那么多,还是上代码最简单直接。准备工作:
① 测试依赖的包及其版本(注:很多教程中都提到需要 aopalliance包,但是博主测试过程中并没有确认此包存在的必要性)
aspectjweaver-1.9.2.jar commons-logging-1.2.jar spring-aop-4.3.18.RELEASE.jar spring-beans-4.3.18.RELEASE.jar spring-context-4.3.18.RELEASE.jar spring-core-4.3.18.RELEASE.jar spring-expression-4.3.18.RELEASE.jar spring-test-4.3.18.RELEASE.jar
② 定义两个基础模型类(如下),业务是:给只有打电话功能的手机动态的添加 拍照、玩游戏这样的非主业务功能。
//主业务功能 public class HuaWeiPhone { public void ring() { System.out.println("华为手机,产销第一"); } } //额外添加的功能 public class Photograph { public void takePictures(){ System.out.println("华为手机,拍照牛批"); } public void playGames(){ System.out.println("华为手机,游戏玩得也这么畅快"); } }
1、XML配置的方式
根据以上Java代码,进行非常简单的配置,就能看到动态的为手机增加了拍照功能的效果了——
<bean class="main.java.model.HuaWeiPhone"/> <bean id="photograph" class="main.model.Photograph"/> <aop:config> <aop:pointcut id="ring" expression="execution(* main.model.HuaWeiPhone.ring(..))"/> <aop:aspect ref="photograph"> <aop:before method="takePictures" pointcut-ref="ring"/> <aop:after method="playGames" pointcut-ref="ring"/> </aop:aspect> </aop:config>
在Spring环境下测试类XML配置——
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:main/resource/applicationContext.xml") public class SpringTest { @Autowired HuaWeiPhone huaWeiPhone; @Test public void testXml(){ huaWeiPhone.ring(); } }
输出结果
2、Java注解的方式
需要先说明的是,Spring的基于注解的 AOP 实际上是借鉴吸收了AspectJ的功能,所以你会看到很多类似 AspectJ 框架的注解。在之前的模型类上通过添加相应的注解改造成一个切面——
@Aspect //将该类标注为一个AOP切面 @Component public class Photograph { @Pointcut("execution(* main.model.HuaWeiPhone.ring(..))") public void chenbenbuyi (){ } @Before("chenbenbuyi()") public void takePictures(){ System.out.println("华为手机,拍照牛批"); } @After("chenbenbuyi()") public void playGames(){ System.out.println("华为手机,游戏玩得也这么畅快"); } }
同样的,目标类(HuaWeiPhone)上也要添加@Componet注解将其交给Spring 容器管理。然后,如果是纯注解的话,还要一个配置类——
//配置注解扫描 @ComponentScan(basePackages = "main") //启用AspectJ的自动代理功能 @EnableAspectJAutoProxy public class JavaConfig { }
最后,在Spring的环境下测试——
@RunWith(SpringJUnit4ClassRunner.class) //@ContextConfiguration(locations = "classpath:main/resource/applicationContext.xml") @ContextConfiguration(classes = JavaConfig.class) public class SpringTest { @Autowired HuaWeiPhone huaWeiPhone; @Test public void testAnno(){ huaWeiPhone.ring(); } }
结果同上,这里就不展示了。不过需要注意的是,不管什么配置方式,基于Spring 的AOP编程实现的前提都是要将通知对象和被通知方法交给Spring IOC容器管理,也就是要声明为Spring 容器中的Bean。
四、需求升级
在第三部分中,博主只是展示了最最简单的AOP功能实现,还有稍微复杂的技能点没有列出。比如,5种通知类型中的环绕通知呢?再比如,我的切面代码如果要传参数怎么办呢?接下来博主依次讲解。
① 关于环绕通知的运用
基于 二 中的阐述,5 种通知类型中 环绕通知 是功能最为强大,实际上,我们可以在环绕通知中个性化的定制出前置 、后置、异常和返回的通知类型,而如果单独的采用前置、后置等通知类型,如果业务涉及多线程对成员变量的修改,可能出现并发问题,所以环绕要比单独的使用另外的几种通知类型更加的安全。我们对上面的切面基于环绕通知进行修改,使之包含所有的通知类型的功能——
@Aspect @Component public class Photograph { @Pointcut("execution(* main.model.HuaWeiPhone.ring(..))") public void chenbenbuyi (){ } @Around("chenbenbuyi()") public void surround(ProceedingJoinPoint joinPoint){ try { System.out.println("目标方法执行前执行,我就是前置通知"); joinPoint.proceed(); // ① // int i =1/0; // ② 制造异常 System.out.println("正常返回,我就是返回通知"); } catch (Throwable e) { System.out.println("出异常了,我就是异常通知"); } finally { System.out.println("后置通知,我就是最终要执行的通知"); } } }
XML的配置和上面的其它通知类型一样,只不过元素标签为 <aop:around />而已。上面的打印语句的位置就对应了其它几种通知类型执行切面逻辑的时机。这里注意,环绕通知方法体中需要有 ProceedingJoinPoint 接口作为参数,在环绕通知中,通过执行该参数的 proceed() 方法来调用通知需要切入的目标方法。如果不执行 ① 处的调用,被通知方法实际上会被阻塞掉,所以你会看到,明明测试中执行了被通知的方法,实际却没有执行。该参数对象还可以获取方法签名、代理对象、目标对象等信息,可以自己测试着玩。
② 关于通知的传参问题
切面虽然是通用逻辑,但实际在切入不同的目标方的时候,可能还是希望通知方法根据被通知方法的不同(比如参数不同)而执行不一样的逻辑,这就要求我们的通知也能获取到被通知方法传入的参数。通过切点表达式,这也很容易办到。首先我们修改被通知的方法可以传参:
public void ring(String str) { System.out.println("华为手机,产销第一"); int i =1/0; }
然后切面中切点表达式和切面方法也做对应的修改——
@Aspect @Component public class Photograph { /** * Spring 借助于 AspectJ的切点表达式语言中的arg()表达式执行参数的传递工作 */ @Pointcut("execution(* main.model.HuaWeiPhone.ring(String))&&args(name)") public void chenbenbuyi (String name){ } /** * ① 在引用空标方法的切点表达式时同时也就要传入相应的参数 * ② 传入的参数形参名字必须和切点表达式中的相同 */ @Before("chenbenbuyi(name)") public void takePictures(String name){ System.out.println("喂喂,你好我是 "+ name); } /** * 对于异常通知,有专门的异常参数可以直接获取到被通知方法出现异常后信息的 */ @AfterThrowing(pointcut = "chenbenbuyi(name)",throwing = "e") public void excep(String name,Throwable e){ System.out.println("出异常了,异常信息是:"+e.getMessage()); } }
XML中配置参数传递
<bean class="main.java.model.HuaWeiPhone"/> <bean id="photograph" class="main.java.model.Photograph"/> <aop:config> <aop:pointcut id="ring" expression="execution(* main.java.model.HuaWeiPhone.ring(..)) and args(name)"/> <aop:aspect ref="photograph"> <aop:before method="takePictures" pointcut-ref="ring" arg-names="name" /> <aop:after-throwing method="excep" throwing="e" arg-names="name,e" pointcut-ref="ring"/> </aop:aspect> </aop:config>
测试代码——
@RunWith(SpringJUnit4ClassRunner.class) //@ContextConfiguration(locations = "classpath:main/resource/applicationContext.xml") @ContextConfiguration(classes = JavaConfig.class) public class SpringTest { @Autowired HuaWeiPhone huaWeiPhone; @Test public void testAnno(){ huaWeiPhone.ring("Java高级架构狮"); } }
注意点:
- XML配置中由于 &符号有特殊含义,所以 切点表达式中 连接形参名的时候就不能再使用注解中的 && ,而应该使用 and 代替,同样的如果有 或(|| )非 (!)操作,分别使用 or 和 not 代替。
- 注解和XML配置中切点表达式描述形参类型的地方博主采用了不同的方式,因为 .. 就表示任意类型,可以不用指明。
五、切点表达式常用图解
读者福利:
分享免费学习资料
针对于Java程序员,我这边准备免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)
为什么某些人会一直比你优秀,是因为他本身就很优秀还一直在持续努力变得更优秀,而你是不是还在满足于现状内心在窃喜!希望读到这的您能点个小赞和关注下我,以后还会更新技术干货,谢谢您的支持!
资料领取方式:加入Java技术交流群963944895
,点击加入群聊,私信管理员即可免费领取