工作流程
-
Spring容器启动
-
读取所有切面配置中的切入点
-
初始化bean
- 匹配成功,创建原始对象(目标对象)的代理对象。
- 匹配成功说明需要对其进行增强
- 对哪个类做增强,这个类对应的对象就叫做目标对象
- 因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象
- 最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强
- 匹配失败,创建对象
- 匹配失败说明不需要增强,直接调用原始对象的方法即可。
- 匹配成功,创建原始对象(目标对象)的代理对象。
-
获取bean执行方法
- 获取的bean是原始对象时,调用方法并执行,完成操作
- 获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
核心概念
目标对象(Target)
原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
代理(Proxy)
目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现
SpringAOP本质
代理模式
AOP切入点表达式
语法格式
概念
- 切入点:要进行增强的方法
- 切入点表达式:要进行增强的方法的描述方式
切入点的描述
其实有两种方式的,先来看下例子
- 描述方式一:执行com.itheima.dao包下的BookDao接口中的无参数update方法
- execution(void com.itheima.dao.BookDao.update())
- 描述方式二:执行com.itheima.dao.impl包下的BookDaoImpl类中的无参数update方法
- execution(void com.itheima.dao.impl.BookDaoImpl.update())
切入点表达式标准格式:
动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)
execution(public User com.itheima.service.UserService.findById(int))
- execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
- public:访问修饰符,还可以是public,private等,可以省略
- User:返回值,写返回值类型
- com.itheima.service:包名,多级包使用点连接
- UserService:类/接口名称
- findById:方法名
- int:参数,直接写参数的类型,多个类型用逗号隔开
- 异常名:方法定义中抛出指定异常,可以省略
通配符
- `*`:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
- execution(public * com.itheima.*.UserService.find*(*))
- 匹配com.itheima包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法
- `..`:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
- execution(public User com..UserService.findById(..))
- 匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法
- `+`:专用于匹配子类类型
- execution(* *..*Service+.*(..))
书写技巧
- 所有代码按照标准规范开发,否则以下技巧全部失效
- 描述切入点通常描述接口,而不描述实现类
- 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)
- 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用\*通配快速描述
- 包名书写尽量不使用..匹配,效率过低,常用\*做单个包描述匹配,或精准匹配
- 接口名/类名书写名称与模块相关的采用\*匹配,例如UserService书写成\*Service,绑定业务层接口名
- 方法名书写以动词进行精准匹配,名词采用*匹配,例如getById书写成getBy*,selectAll书写成selectAll
- 参数规则较为复杂,根据业务方法灵活调整
- 通常不使用异常作为匹配规则
AOP通知类型
类型介绍
AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置。
共提供了5种通知类型:
-
前置通知
- 名称:@Before
- 类型 :方法注解
- 位置 :通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行
- 范例:
-
@Before("pt()") public void Before() { System.out.println("Before advice ..."); }
- 相关属性:value(默认):切入点方法名,格式为类名。方法名()
-
后置通知
- 名称:@After
- 类型 :方法注解
- 位置 :通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行
- 范例:
-
@After("pt()") public void after() { System.out.println("after advice ..."); }
- 相关属性:value(默认):切入点方法名,格式为类名。方法名()
-
环绕通知(重点)
-
名称:@Around
-
类型:方法注解
-
位置:通知方法定义上方
-
作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行
-
范例:
-
@Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable{ System.out.println("around before advice ..."); //表示对原始操作的调用 Object ret =pjp.proceed(); System.out.println("around after advice ..."); return ret; }
- 注意事项:
1. 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
2. 通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行
3. 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为Object类型
4. 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
5. 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常
-
-
返回后通知(了解)
- 名称:@AfterReturning
- 类型 :方法注解
- 位置 :通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行
- 范例:
-
@AfterReturning("pt2()") public void AfterReturning() { System.out.println("AfterReturning advice ..."); }
- 相关属性:value(默认):切入点方法名,格式为类名。方法名()
-
抛出异常后通知(了解)
- 名称:@AfterThrowing
- 类型 :方法注解
- 位置 :通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行
- 范例:
-
@AfterThrowing("pt2()") public void AfterThrowing() { System.out.println("AfterThrowing advice ..."); }
- 相关属性:value(默认):切入点方法名,格式为类名。方法名()
为了更好的理解这几种通知类型,我们来看一张图
- 前置通知,追加功能到方法执行前,类似于在代码1或者代码2添加内容
- 后置通知,追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码5添加内容
- 返回后通知,追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码3添加内容,如果方法执行抛出异常,返回后通知将不会被添加
- 抛出异常后通知,追加功能到方法抛出异常后,只有方法执行出异常才进行,类似于在代码4添加内容,只有方法抛出异常后才会被添加
- 环绕通知,环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式,它可以实现其他四种通知类型的功能,具体是如何实现的,需要我们往下学习。
AOP通知获取数据
获取数据
- 获取切入点方法的参数
- JoinPoint:适用于前置、后置、返回后、抛出异常后通知
- JoinPoint对象描述了连接点方法的运行状态,可以获取原始方法的调用参数
-
@Before("pt()") public void before(JoinPoint jp) Object[] args = jp.getArgs(); System.out.println(Arrays.toString(args)); System.out.println("before advice ..." ); }
- ProceedingJoinPoint:适用于环绕通知
- ProceedingJoinPoint是JoinPoint的子类
-
@Around("pt()") public Object around(ProceedingJoinPoint pjp)throws Throwable { Object[] args = pjp.getArgs(); System.out.println(Arrays.toString(args)); Object ret = pjp.proceed(); return ret; }
获取返回值
- 获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究
- 返回后通知
- 抛出异常后通知获取切入点方法方法中出现的异常信息,使用形参可以接收对应的异常对象
-
@AfterReturning(value = "pt()",returning = "ret") public void afterReturning(Object ret) { System.out.println("afterReturning advice ..."+ret); }
- 环绕通知
- 环绕通知中可以手工书写对原始方法的调用,得到的结果即为原始方法的返回值
-
@Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable{ Object ret= pjp.proceed(); return ret; }
获取异常
- 获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究
- 抛出异常后通知
- 抛出异常后通知获取切入点方法方法中出现的异常信息,使用形参可以接收对应的异常对象
-
@AfterThrowing(value = "pt()",throwing = "t") public void afterThrowing(Throwable t) { System.out.println("afterThrowing advice ..."+t); }
- 环绕通知
- 抛出异常后通知获取切入点方法运行的异常信息,使用形参可以接收运行时抛出的异常对象
-
@Around("pt()") public Object around(ProceedingJoinPoint pjp){ Object ret = null; try{ ret = pjp.proceed(); }catch(Throwable throwable){ t.printStackTrace(); } return ret; }