前言
AOP:Aspect Oriented Programming(面向切面编程),一种编程范式,指导开发者如何组织程序结构。
OOP(Object Oriented Programming)面向对象编程
AOP和OOP一样都是一种编程思想,而编程思想的主要内容便是指导程序员如何编写程序,所以它们两个是不同的编程范式
1.AOP的作用
作用:在不惊动原始设计的基础上为其进行功能增强,类似于代理模式技术;这也是Spring的理念,对代码做到无侵入式
2.AOP核心概念
首先先看一个案例,代码如下:
@Repository
public class BookDaoImpl implements BookDao {
public void save() {
//记录程序当前执行执行(开始时间)
Long startTime = System.currentTimeMillis();
//业务执行万次
for (int i = 0;i<10000;i++) {
System.out.println("book dao save ...");
}
//记录程序当前执行时间(结束时间)
Long endTime = System.currentTimeMillis();
//计算时间差
Long totalTime = endTime-startTime;
//输出信息
System.out.println("执行万次消耗时间:" + totalTime + "ms");
}
public void update(){
System.out.println("book dao update ...");
}
public void delete(){
System.out.println("book dao delete ...");
}
public void select(){
System.out.println("book dao select ...");
}
}
那么上面的代码依次执行save(),delete(),update()和select()后,其控制台的各种打印结果如下图:
那么奇怪的来了,计算万次消耗时间只在save()中有,为何delete()和update()也会有,那么select()为何又没有?
上述代码中就使用了Spring的AOP,实现了无侵入式的代码设计
Spring实现AOP
1.Spring的AOP是对一个类的方法不进行任何修改的前提下实现增强。对于上面的案例将save、update、delete和select方法看做一个个“连接点”;
2.上述的update和delete方法中,没有关于万次消耗时间的语句,但在执行后却已经有此功能,也就是说update和delete方法已经被加强过了,那么对加强过的方法取名为“切入点”;
3.由于update和delete在执行后都实现了万次消耗时间的方法,所有一个方法存放共性的功能,将这个方法取名为“通知”;
4.“通知”就是要增强的内容,会有多个;切入点是需要被增强的方法,也会有多个;哪个切入点需要添加哪个通知,就需要提前将它们之间的关系描述清除,那么对于通知和切入点之间的关系描述称为“切面”;
5.通知是一个方法,所以存放通知方法的类叫做“通知类”;
小结:
-
连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
-
在SpringAOP中,理解为方法的执行
-
-
切入点(Pointcut):匹配连接点的式子
-
在SpringAOP中,一个切入点可以描述一个具体方法,也可也匹配多个方法
-
一个具体的方法:如com.itheima.dao包下的BookDao接口中的无形参无返回值的save方法
-
匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,所有带有一个参数的方法
-
-
连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一定要被增强,所以可能不是切入点。
-
-
通知(Advice):在切入点处执行的操作,也就是共性功能
-
在SpringAOP中,功能最终以方法的形式呈现
-
-
通知类:定义通知的类
-
切面(Aspect):描述通知与切入点的对应关系。
3.AOP工作流程
流程1:Spring容器启动
-
容器启动就需要去加载bean,哪些类需要被加载呢?
-
需要被增强的类,如:BookServiceImpl
-
通知类,如:MyAdvice
-
注意此时bean对象还没有创建成功
流程2:读取所有切面配置中的切入点
如上图中,但是由于ptx()并未使用,所以并不会被读取。
流程3:初始化bean
判定bean对应的类中的方法是否匹配到任意切入点
-
注意第1步在容器启动的时候,bean对象还没有被创建成功。
-
要被实例化bean对象的类中的方法和切入点进行匹配
-
匹配失败,创建原始对象,如
UserDao
-
匹配失败说明不需要增强,直接调用原始对象的方法即可。
-
-
匹配成功,创建原始对象(==目标对象==)的==代理==对象,如:
BookDao
-
匹配成功说明需要对其进行增强
-
对哪个类做增强,这个类对应的对象就叫做目标对象
-
因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象
-
最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强
-
流程4:获取bean执行方法
-
获取的bean是原始对象时,调用方法并执行,完成操作
-
获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
验证容器中是否为dialing对象
-
如果目标对象中的方法会被增强,那么容器中将存入的是目标对象的代理对象
-
如果目标对象中的方法不被增强,那么容器中将存入的是目标对象本身。
验证思路
1.要执行的方法,不被定义的切入点包含,即不要增强,打印当前类的getClass()方法
2.要执行的方法,被定义的切入点包含,即要增强,打印出当前类的getClass()方法
3.观察两次打印的结果
步骤1:修改App类,获取类的类型
public class App { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); BookDao bookDao = ctx.getBean(BookDao.class); System.out.println(bookDao); System.out.println(bookDao.getClass()); } }
步骤2:修改MyAdvice类,不增强
因为定义的切入点中,被修改成
update1
,所以BookDao中的update方法在执行的时候,就不会被增强,所以容器中的对象应该是目标对象本身。
@Component @Aspect public class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update1())") private void pt(){} @Before("pt()") public void method(){ System.out.println(System.currentTimeMillis()); } }
步骤3:修改MyAdvice类,增强
因为定义的切入点中,被修改成
update
,所以BookDao中的update方法在执行的时候,就会被增强,所以容器中的对象应该是目标对象的代理对象
@Component @Aspect public class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} @Before("pt()") public void method(){ System.out.println(System.currentTimeMillis()); } }
4.AOP核心概念
-
目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
-
代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现
目标对象就是要增强的类[如:BookServiceImpl类]对应的对象,也叫原始对象,不能说它不能运行,只能说它在运行的过程中对于要增强的内容是缺失的。
SpringAOP是在不改变原有设计(代码)的前提下对其进行增强的,它的底层采用的是代理模式实现的,所以要对原始对象进行增强,就需要对原始对象创建代理对象,在代理对象中的方法把通知[如:MyAdvice中的method方法]内容加进去,就实现了增强,这就是我们所说的代理(Proxy)。
5.AOP通知类型
上述注解代表的含义就是将通知添加到切入点方法执行的“前面”;
那么除了这个注解,还有能加在其他地方的注解
5.1类型介绍
-
AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置
提供了5种通知类型:
-
前置通知
-
后置通知
-
环绕通知
-
返回后通知
-
抛出异常后通知
(1)前置通知,追加功能到方法执行前,类似于在代码1或者代码2添加内容
(2)后置通知,追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码5添加内容
(3)返回后通知,追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码3添加内容,如果方法执行抛出异常,返回后通知将不会被添加
(4)抛出异常后通知,追加功能到方法抛出异常后,只有方法执行出异常才进行,类似于在代码4添加内容,只有方法抛出异常后才会被添加
(5)环绕通知,环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式,它可以实现其他四种通知类型的功能
5.2通知类型的使用
环境准备
添加BookDao和BookDaoImpl类
public interface BookDao {
public void update();
public int select();
}
@Repository
public class BookDaoImpl implements BookDao {
public void update(){
System.out.println("book dao update ...");
}
public int select() {
System.out.println("book dao select is running ...");
return 100;
}
}
创建Spring的配置类
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}
创建通知类
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
public void before() {
System.out.println("before advice ...");
}
public void after() {
System.out.println("after advice ...");
}
public void around(){
System.out.println("around before advice ...");
System.out.println("around after advice ...");
}
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
}
编写App运行类
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.update();
}
}
前置通知
修改MyAdvice,在before方法上添加
@Before注解
@Component @Aspect public class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} @Before("pt()") public void before() { System.out.println("before advice ..."); } }
后置通知
@Component @Aspect public class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} @Before("pt()") public void before() { System.out.println("before advice ..."); } @After("pt()") public void after() { System.out.println("after advice ..."); } }
环绕通知
@Component @Aspect public class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} @Around("pt()") public void around(){ System.out.println("around before advice ..."); System.out.println("around after advice ..."); } }
运行结果中,通知的内容打印出来,但是原始方法的内容却没有被执行。
因为环绕通知需要在原始方法的前后进行增强,所以环绕通知就必须要能对原始操作进行调用
@Component @Aspect public class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} @Around("pt()") public void around(ProceedingJoinPoint pjp) throws Throwable{ System.out.println("around before advice ..."); //表示对原始操作的调用 pjp.proceed(); System.out.println("around after advice ..."); } }
这里的proceed()抛了一个异常,那么进入到源码中就可以直到原因了:
再次运行,就可以看到原始方法已经被执行了
因为环绕通知是可以控制原始方法执行的,所以我们把增强的代码写在调用原始方法的不同位置就可以实现不同的通知类型的功能,如:
通知类型总结:
名称 | @After |
---|---|
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行 |
名称 | @AfterReturning |
---|---|
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法正常执行完毕后执行 |
名称 | @Around |
---|---|
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行 |
名称 | @AfterThrowing |
---|---|
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行 |
环绕通知注意事项
-
环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
-
通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行
-
对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为Object类型
-
原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
-
由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常