1. AOP简介
1.1. 什么是AOP
AOP:全称是Aspect Oriented Programming 即:面向切面编程
简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强
1.2. AOP的作用及优势
作用:在程序运行期间,不修改源码对已有方法进行增强
优势:减少重复代码、提高开发效率、维护方便
1.3. AOP的实现原理
动态代理。Spring会根据目标类是否实现了接口来决定采用哪种动态代理的方式。
1.4. AOP相关术语
Joinpoint(连接点):被拦截到的点。Spring只支持方法类型的连接点,所以连接点就是要拦截的方法。简单地讲,ServiceImpl中所有的方法,都是连接点
Pointcut(切入点):被切面增强的连接点
Advice(通知/增强):所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。通知的类型:前置通知(before),后置通知(after),异常通知(afterThrowing),返回通知(afterReturning),环绕通知(around)
Introduction(引介):引介是一种特殊的通知在不修改类代码的前提下,Introduction可以在运行期为类动态地添加一些方法或Field
Target(目标对象):代理的目标对象,即被代理对象,比如ServiceImpl
Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。
Proxy(代理):一个类被AOP织入增强后,就产生一个结果代理类
Aspect(切面):是切入点和通知(引介)的结合
连接点和切入点的区别:
- 连接点不一定是切入点,切入点一定是连接点
- 连接点是被拦截的方法,该方法可能会被增强,也可能不会,如果被切面增强,就是切入点
1.5. 学习Spring中的AOP要明确的事
开发阶段(我们做的)
- 编写核心业务代码(开发主线):大部分程序员来做,要求熟悉业务需求。
- 把公用代码抽取出来,制作成通知。(开发阶段最后再做)
- 在配置文件中,声明切入点与通知间的关系,即切面。
运行阶段(Spring框架完成的)
- Spring框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。
1.6. 实际开发中切入点表达式的写法
切到业务层实现类ServiceImpl下所有的方法
2. 切入点表达式
切入点表达式指定要对哪些方法进行增强,即指定切入点
表达式形式:返回值 返回类型 包名.类名.方法名(形参类型列表)
2.1. public修饰符可省略
切入点表达式的访问权限默认是public,所以以下两个表达式是等价的1
2public void demo.spring.service.UserServiceImpl.addUser()
void demo.spring.service.UserServiceImpl.addUser() // 省略public
2.2. 形参类型列表
参数是基本类型,直接写名称。如果是引用类型,则写全限定名1
2* *..*.*(int)
* *..*.*(int, long, java.lang.String)
2.3. 使用通配符
返回值类型可以使用通配符。1
2void demo.spring.service.UserServiceImpl.addUser() // 返回类型是void
* demo.spring.service.UserServiceImpl.addUser() // 返回类型可以是任意类型
方法名可以使用通配符1
2* demo.spring.service.UserServiceImpl.addUser() // 匹配addUser()方法
* demo.spring.service.UserServiceImpl.*() // 匹配所有空参方法
参数类型列表可以使用通配符,注意通配符是两个点.1
2* demo.spring.service.UserServiceImpl.*() // 匹配所有空参方法
* demo.spring.service.UserServiceImpl.*(..) // 匹配所有方法(参数列表任意)
类名可以使用通配符1
2
3* demo.spring.service.UserServiceImpl.*(..) // 匹配UserServiceImpl类
* demo.spring.service.*ServiceImpl.*(..) // 匹配以ServiceImpl名称结尾的类
* demo.spring.service.*.*(..) // 匹配demo.spring.service包下的所有类
包名分隔符可以写作两个点.,表示当前包及其子包的通配1
2* demo.spring.service.*.*(..) // 匹配demo.spring.service包下的所有类
* demo.spring.service..*.*(..) // 匹配demo.spring.service包及其子包下的所有类(递归匹配)
全通配写法 * *..*.*(..)
3. AOP各通知执行顺序
AOP各通知执行逻辑如下:
1 | try { |
执行分两种情况:
- 执行正常:before、目标方法、after、afterReturning
- 执行异常:before、目标方法、after、afterThrowing
也就是说,before、目标方法、after这3个步骤是一定会执行的,最后一步是异常通知还是返回通知,取决于是否发生异常
4. AOP案例环境搭建
4.1. 依赖
1 | <!-- Spring核心 --> |
4.2. 目标对象Target
1 | public interface UserService { |
1 | /** |
4.3. 自定义切面类
1 | package demo.spring.springlearning.aspect; |
4.4. 案例:前/后/异常/返回 通知
编辑applicationContext.xml
1 | <!-- 定义目标Bean --> |
测试1
2
3
4
5
6
7
8
public void test() {
ApplicationContext ioc = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
UserService userService = ioc.getBean(UserService.class);
userService.addUser();
System.out.println("============================");
userService.deleteUser();
}
4.5. 案例:环绕通知
环绕通知与其它通知,一般单独使用。环绕通知具体要做什么,由程序员决定,而不是由Spring来决定,所以灵活性最好。
实现方式是:在环绕通知切入点方法中,传入ProceedingJoinPoint参数。ProceedingJoinPoint是一个接口。你可以通过pjp.getArgs()获取目标方法的执行参数,通过pjp.proceed(args)执行目标方法。最后你要返回执行目标方法的返回值。
1 | public Object around(ProceedingJoinPoint pjp) { |
1 | <!-- 定义目标Bean --> |
5. AOP配置详解
5.1. 切入点的几种配置方式
5.1.1. 通过pointcut属性设置切入点表达式
1 | <!-- 定义目标Bean --> |
5.1.2. 通过pointcut-ref引用已配置的切入点
多个通知的切入点表达式可能会重复,显得冗余。这时候可以用<aop:pointcut>单独定义一个切入点,在配置通知时,使用pointcut-ref来引用对应的切入点
1 | <!-- 定义目标Bean --> |
AOP可以配置多个<aop:aspect>切面,以上示例的<aop:pointcut>是配置在<aop:aspect>内部的,只有该切面内部的通知才能引用该切入点。如果想定义好的切入点可以被所有的通知引用,就把<aop:pointcut>放到外面。注意DTD约束要求<aop:pointcut>必须写在<aop:aspect>之后,否则会出错
1 | <!-- 定义目标Bean --> |
6. AOP基于注解配置
6.1. 注解定义Bean
开启包扫描
1 | <context:component-scan base-package="demo.spring.springlearning"/> |
使用@Service标识Target
1 |
|
使用@Component标识自定义切面
1 |
|
6.2. AOP注解配置
开启AOP注解支持
1 | <aop:aspectj-autoproxy/> |
使用@Aspect声明切面类。使用@Before等等定义对应的通知,value是切入点表达式
1 |
|
觉得切入点表达式重复冗余,可以使用@Pointcut定义一个单独的切入点
1 |
|
6.3. AOP完全注解配置(不使用XML)
定义Spring配置类
1 |
|
测试
1 |
|