33-AOP通知类型
前面的案例中,有涉及到如下内容
@Before("pt()")
它所代表的含义是将通知添加到切入点方法执行的前面。
除了这个注解外,还有没有其他的注解,换个问题就是除了可以在前面加,能不能在其他的地方加?
类型介绍
我们先来回顾下AOP通知:
AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置
那么具体可以将通知添加到哪里呢?一共提供了5种通知类型
- 前置通知
- 后置通知
- 环绕通知(重点)
- 返回后通知(了解)
- 抛出异常后通知(了解)
为了更好的理解这几种通知类型,我们来看一张图
- 前置通知,追加功能到方法执行前,类似于在代码1或者代码2添加内容
- 后置通知,追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码5添加内容
- 返回后通知,追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码3添加内容,如果方法执行抛出异常,返回后通知将不会被添加
- 抛出异常后通知,追加功能到方法抛出异常后,只有方法执行出异常才进行,类似于在代码4添加内容,只有方法抛出异常后才会被添加
- 环绕通知,环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式,它可以实现其他四种通知类型的功能,具体是如何实现的,需要我们往下学习。
环境准备
- 创建一个Maven项目
- pom.xml添加Spring依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
- 添加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 ...");
return 100;
}
}
- 创建Spring的配置类
@Configuration
@ComponentScan("com.yolo")
@EnableAspectJAutoProxy
public class SpringConfig {
}
通知类型的使用
前置通知
修改MyAdvice,在before方法上添加@Before
注解
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.yolo.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.yolo.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.yolo.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 ...");
}
@Around("pt()")
public void around(){
System.out.println("around before advice ...");
System.out.println("around after advice ...");
}
}
运行程序,输出如下
运行结果中,通知的内容打印出来,但是原始方法的内容却没有被执行。
因为环绕通知需要在原始方法的前后进行增强,所以环绕通知就必须要能对原始操作进行调用,具体如何实现?
- 在方法参数中添加
ProceedingJoinPoint
,同时在需要的位置使用proceed()
调用原始操作,注意抛出异常
@Around("pt()")
public void around(ProceedingJoinPoint point) throws Throwable {
System.out.println("around before advice ...");
// 表示对原操作的调用
point.proceed();
System.out.println("around after advice ...");
}
运行程序,输出如下
- 注意事项
- 当原始方法中有返回值时
- 修改MyAdvice,对BookDao中的select方法添加环绕通知
- 修改App类,调用select方法
- 当原始方法中有返回值时
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(int com.yolo.dao.BookDao.select())")
private void pt2() {}
@Around("pt2()")
public void aroundSelect(ProceedingJoinPoint point) throws Throwable {
System.out.println("around before advice ...");
// 表示对原操作的调用
point.proceed();
System.out.println("around after advice ...");
}
}
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.select());
}
}
运行程序,报错:org.springframework.aop.AopInvocationException: Null return value from advice does not match primitive return type for: public abstract int com.yolo.dao.BookDao.select()
错误大概的意思是:空的返回不匹配原始方法的int返回
- void就是返回Null
- 原始方法是BookDao下的select方法,返回值是int
所以如果我们使用环绕通知的话,要根据原始方法的返回值来设置环绕通知的返回值,具体解决方案为:
@Around("pt2()")
public Object aroundSelect(ProceedingJoinPoint point) throws Throwable {
System.out.println("around before advice ...");
// 表示对原操作的调用
Object res = point.proceed();
System.out.println("around after advice ...");
return res;
}
- 运行程序,结果如下
返回后通知
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(int com.yolo.dao.BookDao.select())")
private void pt2() {
}
@AfterReturning("pt2()")
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
}
运行程序,结果如下
注意:
返回后通知是需要在原始方法select正常执行后才会被执行,如果select()方法执行的过程中出现了异常,那么返回后通知是不会被执行。后置通知是不管原始方法有没有抛出异常都会被执行。
现在我们在select()方法中加一个异常
public int select() {
System.out.println("book dao select ...");
int i = 1/0;
return 100;
}
运行程序,输出如下,没有输出afterReturning advice …
我们再换成后置输出,
@After("pt2()")
public void after(){
System.out.println("after advice ...");
}
运行程序,结果如下,输出了after advice
异常后通知
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(int com.yolo.dao.BookDao.select())")
private void pt2() {
}
@AfterThrowing("pt2()")
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
}
运行程序,输出如下
去掉原方法中的异常
public int select() {
System.out.println("book dao select ...");
// int i = 1/0;
return 100;
}
再运行,结果如下
学习完这5种通知类型,我们来思考下环绕通知是如何实现其他通知类型的功能的?
因为环绕通知是可以控制原始方法执行的,所以我们把增强的代码写在调用原始方法的不同位置就可以实现不同的通知类型的功能,如
通知类型总结
知识点1:@After
知识点2:@AfterReturning
知识点3:@AfterThrowing
知识点4:@Around
知识点5:@Before
环绕通知注意事项
- 环绕通知必须依赖形参
ProceedingJoinPoint
才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知 - 通知中如果未使用
ProceedingJoinPoint
对原始方法进行调用将跳过原始方法的执行 - 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为
Object
类型 - 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
- 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理
Throwable
异常