前言
本文主要涉及如下知识点
- SpringAOP的基本概念
- 基于XML配置的SpringAOP实战
- 基于注解配置的SpringAOP实战
- 环绕通知
AOP的概念
· AOP(Aspect Oriented Programming)面向切面编程,是OOP面向对象编程的升级,AOP是基于OOP的。OOP比较好理解,生活中万物都是对象,但是AOP乍一看不是很好理解。不过没关系,我稍微一点拨,肯定就能懂。
· 说到底,其实就是“切面”这个概念我们不熟悉,我的理解是这样的:切面是某种对象的共同特征。什么意思呢?我举个简单的例子:我们一生中会玩很多的游戏,每个游戏都在不同的平台,那我们每次玩游戏,都要登录某个平台。面向对象编程的思维是这样的:我们这个人,就是一个对象,而我们玩游戏,就是这个对象使用了某种方法,所以在编程时我们把“人”这个概念抽取成了对象,把“玩游戏”这个概念抽取成一个个不同的方法。而在这个基础上,面向切面的编程方式 被提出了,因为我们发现,我们有无数种对应玩游戏的方法,但是每一种方法我们都需要进行登录的操作,所以我们把“登录”这个操作抽取出来,称作是所有”玩游戏”方法的切面,而每个玩游戏的方法,就是一个个切入点。
· 可见,“登录”是所有“玩游戏”方法的共同特征,所以切面这个概念不如对象一般直观,因为,所有的切面都是人为抽取出来的,具有一定的思考性。而面向切面编程最重要的两个优势在于:1.可以简化代码量;2.可以让业务代码仅关注业务逻辑,使代码结构更清晰。
· 不管是哪一种优势,对于开发来说,都是至关重要的。
切面的一些专业术语
· SpringAOP的实现是基于动态代理的,本人写过一遍有关动态代理的教程:Java两种动态代理实战+动态代理死循环的解释。
· 要会看的懂SpringAOP的相关文档就必须懂有关切面的相关术语,首先Aspect就是切面的意思。这个要是不知道,就快先去把6级考了再说。**
术语如下:
术语 |
含义 |
joinpoint 连接点 |
可以被拦截到的方法,但不一定会被增强 |
pointcut 切入点 |
被增强的方法(可见切入点一定是连接点,而反之则不然) |
advice 通知(增强) |
指在拦截到切入点后,如何增强某个方法 |
target |
指被代理的对象 |
weaving 织入 |
指一个过程:增强某个类并形成代理类的过程 |
proxy 代理 |
一个类被织入后,就生成了一个代理类 |
aspect 切面 |
指切入点和通知的结合 |
· 如果你能把上述玩游戏的例子和这里的术语对应上,那你术语部分就过关啦!
其中advice术语是实现aop的关键,其有五种通知类型:
- 1.前置通知(before):只在原方法之前调用的增强
- 2.后置通知(after-returning):原方法完成之后调用的增强
- 3.异常通知(after-throwing):原方法出异常后调用的增强
- 4.最终通知(after):不管怎么样都会调用的增强,最后才会调用
- 5.环绕通知(around):通过写代码来实现对切入点的管理(以上一般都是用配置,来决定顺序),环绕通知一般用于注解开发。
SpringAOP实战
· 本来想搞利用aop实现事务的管理的,不过既然在开头用了玩游戏的例子,那么索性,这里的案例就用玩游戏了。
· 需求如下:玩家只需要关注玩游戏就行。代理类的任务在于:在玩游戏之前实现登录操作,游戏结束后执行退出操作,如果玩游戏途中出现了游戏BUG,就执行回档操作(异常),最终不管怎么样,玩游戏是要钱的,最后要执行扣钱操作。
前期准备
· 引入jar包,aop需要的jar包是aspectjweaver,用于解析切入点表达式(下文会讲)以及spring-aop(这个一般引入spring-context依赖就会自己引入了),然后什么ioc的包等等,我就不说了,这次案例会用到一点点的IOC的知识,如果不会,可以参考我的IOC教程SpringIOC实战(xml+注解)。
1 2 3 4 5 6 7 8 9 10 11 12
| <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.2</version> </dependency> </dependencies>
|
- 配置玩家类(由Spring进行管理)
可见我们希望切面能增强所有玩游戏的方法,而不增强吃饭这个方法,其中我们看见玩LOL时会出现异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package com.memoforward.player;
import org.springframework.stereotype.Component;
@Component public class Player {
public void playLOL(){ System.out.println("玩英雄联盟..."); throw new RuntimeException("LOL崩溃了...."); } public void playDota2(){ System.out.println("玩Dota2..."); } public void playWOW(){ System.out.println("玩魔兽世界..."); } public void playTaiWu(){ System.out.println("玩太吾绘卷..."); } public void eat(){ System.out.println("游戏玩累了,吃饭..."); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package com.memoforward;
import org.springframework.stereotype.Component;
@Component("playerAdvice") public class PlayerAdvice { public void login(){ System.out.println("---1.游戏登录---"); } public void quit(){ System.out.println("---2.游戏退出---"); } public void rollback(){ System.out.println("---3.游戏回档---"); } public void loseMoney(){ System.out.println("---4.游戏扣费---"); } }
|
用XML配置切面
· 使用SpringAOP需要在xml文件中引入aop的约束,这里就不贴了。
· 关于XML配置,共有如下的几个标签需要用到。
标签名 |
属性 |
作用 |
层级 |
aop:config |
无 |
开启aop的控制 |
1 |
aop:pointcut |
id:指切入点表达式的id;expression:写切入点表达式 |
告诉切面将增强哪些方法(切入点) |
2或3 |
aop:aspect |
id:该切面的id;ref:该切面对应的通知类 |
声明一个切面 |
2 |
aop : before |
method:该前置通知对应在通知类中的方法;pointcut-ref:切入点表达式id / pointcut:切入点表达式 |
声明一个前置通知 |
3 |
aop : after-returning |
和上面类似 |
声明一个后置通知 |
3 |
aop : after-throwing |
和上面类似 |
声明一个异常通知 |
3 |
aop : after |
和上面类似 |
声明一个最终通知 |
3 |
- 有关切入点表示的补充
· 切入点表达式有关键字:execution。在关键字内部写表达式,规则是:(访问修饰符 返回值 全限定类名.方法名),且不同的execution之间可以用and、or、!等关键字来增强表达式的逻辑。
举个例子:如果要切入点要选Player类中的playLOL方法,则表达式可以这么写:
1
| execution(public void com.memoforward.player.Player.playLOL())
|
· 显然这么写太复杂了,因此有如下的简化措施:
- 访问修饰符可省略
- 返回值可以用通配符 * 表示任意返回值类型
- 包名可以用 * 表示任意一个包;用 *. 表示当前包及其所有子包
- 类名和方法名都可以用 * 表示任意类和任意方法
- 可用 (..) 表示任意参数和任意参数类型(如果不想用用任意类型,基础类型可以直接写,引用类型用 ‘包名.类名’ 的方式)
` 因此有全通配写法:该项目下所有包的所有方法(不推荐使用)
- 一般情况下:我们只需要切到业务层实现类下的所有方法就可以了。
xml配置文件如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <context:component-scan base-package="com.memoforward"/>
<aop:config> <aop:aspect id="playerAdvice" ref="playerAdvice"> <aop:pointcut id="pt1" expression="execution(* com.memoforward.player.*.*(..)) and !execution(* com.memoforward.player.*.eat(..))"/> <aop:before method="login" pointcut-ref="pt1"/> <aop:after-returning method="quit" pointcut-ref="pt1"/> <aop:after-throwing method="rollback" pointcut-ref="pt1"/> <aop:after method="loseMoney" pointcut-ref="pt1"/> </aop:aspect> </aop:config>
</beans>
|
- 注意事项:
- 用了两个execution语句实现了增强 除了eat方法外的所有方法。
- 注意切入点表达式的位置:如果在< aop: aspect >标签内,则这个表达式只对这个切面生效;如果在切面标签外,则对所有切面生效,但其必须要声明在切面之前。
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import ...
@ContextConfiguration(locations = "classpath:beans.xml") public class testSpringAOP extends AbstractTestNGSpringContextTests { @Autowired Player player;
@Test public void testAOP01(){ player.eat(); System.out.println("*************"); player.playDota2(); System.out.println("*************"); player.playTaiWu(); System.out.println("*************"); player.playWOW(); System.out.println("*************"); player.playLOL(); } }
|
- 测试结果如下:可见除了eat方法,其他方法都被增强了,而且出异常的LOL也成功进行了游戏回档。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 游戏玩累了,吃饭... ************* ---1.游戏登录--- 玩Dota2... ---2.游戏退出--- ---4.游戏扣费--- ************* ---1.游戏登录--- 玩太吾绘卷... ---2.游戏退出--- ---4.游戏扣费--- ************* ---1.游戏登录--- 玩魔兽世界... ---2.游戏退出--- ---4.游戏扣费--- ************* ---1.游戏登录--- 玩英雄联盟... ---3.游戏回档--- ---4.游戏扣费---
java.lang.RuntimeException: LOL崩溃了....
|
基于注解的AOP开发
- 在配置文件中开启aop自动代理权限
- 配置切面的通知类
1
| <aop:aspectj-autoproxy/>
|
- 配置切面通知类:@Aspect;@Pointcut;以及各种通知注解,很简单。
注意:切入点表达式需要把 and 换成 &&
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| package com.memoforward;
import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component;
@Component("playerAdvice02") @Aspect public class PlayerAdvice02 { @Pointcut("execution(* com.memoforward.player.*.*(..)) && !execution(* com.memoforward.player.*.eat(..))") private void pt1(){} @Before("pt1()") public void login(){ System.out.println("---游戏登录---"); } @AfterReturning("pt1()") public void quit(){ System.out.println("---游戏退出---"); } @AfterThrowing("pt1()") public void rollback(){ System.out.println("---游戏回档---"); } @After("pt1()") public void loseMoney(){ System.out.println("---游戏扣费---"); } }
|
- 测试如下:此时已经换成了PlayerAdvice02切面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import ...;
@ContextConfiguration(locations = "classpath:beans.xml") public class testSpringAOP extends AbstractTestNGSpringContextTests { @Autowired Player player;
@Test public void testAOP(){ player.eat(); System.out.println("*************"); player.playTaiWu(); System.out.println("*************"); player.playLOL(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| 游戏玩累了,吃饭... ************* ---1.游戏登录--- 玩太吾绘卷... ---4.游戏扣费--- ---2.游戏退出--- ************* ---1.游戏登录--- 玩英雄联盟... ---4.游戏扣费--- ---3.游戏回档---
java.lang.RuntimeException: LOL崩溃了....
|
- 问题(该问题已经被修复,注解可以放心使用)
· 仔细一点就能发现,最终通知和后置通知的顺序反了,这是注解开发的一个大问题,目前还没有被修复,因此如果要用注解开发的话,一般使用环绕通知的方式,所谓环绕通知和动态代理的实现方法基本没什么区别。下面将简单介绍一下:
环绕通知
· 环绕通知和动态代理的内部几乎是一样的写法,不同的点在于:动态代理的参数包含了被代理类的字节码对象;而在环绕通知中,因为Spring已经管理的被代理的类,因此就不必我们手动提供了,取而代之的,是Spring提供的的一个接口:ProceedingJoinPoint,此接口有两个方法,一个是获取被代理类方法的参数,一个是调用被代理类的方法。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Around("pt1()") public Object playerAdvice(ProceedingJoinPoint pjp){ Object obj = null; try{ Object[] args = pjp.getArgs(); login(); obj = pjp.proceed(args); quit(); return obj; } catch (Throwable throwable) { rollback(); throw new RuntimeException(throwable); } finally { loseMoney(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| 游戏玩累了,吃饭... ************* ---1.游戏登录--- 玩太吾绘卷... ---2.游戏退出--- ---4.游戏扣费--- ************* ---1.游戏登录--- 玩英雄联盟... ---3.游戏回档--- ---4.游戏扣费---
java.lang.RuntimeException: java.lang.RuntimeException: LOL崩溃了....
|
总结
· SpringAOP说难不难,但是重要的是这种面向切面的编程思想以及动态代理。Spring还剩最后一项事务管理。我会在下一次博客把它补上。
交流
请联系邮箱:chenxingyu@bupt.edu.cn