文章目录
- 📚AOP切点表达式
- 🐇语法格式
- 🐇通配符
- 📚AOP通知类型
- 🐇环境准备
- 🐇通知类型的使用
- 📚AOP通知获取数据
- 🐇环境准备
- 🐇获取参数
- 🐇获取返回值
- 🐇获取异常
- 📚小结
学习来源:黑马程序员SSM框架教程_Spring+SpringMVC+Maven高级+SpringBoot+MyBatisPlus企业实用开发技术
📚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())
- 【针对方式一】:执行com.itheima.dao包下的BookDao接口中的无参数update方法。
-
【标准格式】:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名),例:
execution(public User com.itheima.service.UserService.findById(int))
🐇通配符
*
——单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现。例:匹配com.itheima包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法。execution(public * com.itheima.*.UserService.find*(*))
..
——多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写。例:匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法。execution(public User com..UserService.findById(..))
+
:专用于匹配子类类型,例:*Service+,表示所有以Service结尾的接口的子类。(用的很少)execution(* *..*Service+.*(..))
- 匹配接口:
execution(void com.itheima.dao.BookDao.update())
。 - 匹配实现类:
execution(void com.itheima.dao.impl.BookDaoImpl.update())
。 - 匹配项目中所有业务层方法的以find开头的方法:
execution(* com.itheima.*.*Service.find*(..))
。 - 匹配项目中所有业务层方法的以save开头的方法:
execution(* com.itheima.*.*Service.save*(..))
。
- 匹配接口:
- 描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就出现紧耦合了。
- 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)。
- 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用
*
通配快速描述。 - 包名书写尽量不使用
..
匹配,效率过低,常用*
做单个包描述匹配,或精准匹配。 - 【接口名/类名书写】:名称与模块相关的采用
*
匹配,例如UserService书写成*Service
,绑定业务层接口名。 - 【方法名书写】:以动词进行精准匹配,名词采用
*
匹配,例如getById书写成getBy*
。 - 通常不使用异常作为匹配规则。
📚AOP通知类型
🐇环境准备
-
创建一个Maven项目。
-
pom.xml添加Spring依赖。
<dependencies> <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> </dependencies>
-
添加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(); } }
🐇通知类型的使用
-
前置通知:追加功能到方法执行前,类似于在代码1或者代码2添加内容。修改MyAdvice,在before方法上添加
@Before注解
。@Component @Aspect public class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} @Before("pt()") //此处也可以写成 @Before("MyAdvice.pt()"),但不建议 public void before() { System.out.println("before advice ..."); } }
-
后置通知,追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码5添加内容。
@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 ..."); } }
-
返回后通知,追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码3添加内容,如果方法执行抛出异常,返回后通知将不会被添加。
@Component @Aspect public class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} @Pointcut("execution(int com.itheima.dao.BookDao.select())") private void pt2(){} @AfterReturning("pt2()") public void afterReturning() { System.out.println("afterReturning advice ..."); } }
- 返回后通知是需要在原始方法
select
正常执行后才会被执行,如果select()
方法执行的过程中出现了异常,那么返回后通知是不会被执行。 - 后置通知是不管原始方法有没有抛出异常都会被执行。
- 返回后通知是需要在原始方法
-
抛出异常后通知,追加功能到方法抛出异常后,只有方法执行出异常才进行,类似于在代码4添加内容,只有方法抛出异常后才会被添加。
@Component @Aspect public class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} @Pointcut("execution(int com.itheima.dao.BookDao.select())") private void pt2(){} @AfterThrowing("pt2()") public void afterThrowing() { System.out.println("afterThrowing advice ..."); } }
- 异常后通知是需要原始方法抛出异常,可以在
select()
方法中添加一行代码int i = 1/0
即可。如果没有抛异常,异常后通知将不会被执行。
- 异常后通知是需要原始方法抛出异常,可以在
-
环绕通知,环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式,它可以实现其他四种通知类型的功能。
@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 ..."); } }
-
因为环绕通知是可以控制原始方法执行的,所以我们把增强的代码写在调用原始方法的不同位置就可以实现不同的通知类型的功能。
-
环绕通知原始方法有返回值的处理↓
- 修改MyAdvice,对BookDao中的select方法添加环绕通知。
@Component @Aspect public class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} @Pointcut("execution(int com.itheima.dao.BookDao.select())") private void pt2(){} @Around("pt2()") public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable { System.out.println("around before advice ..."); //表示对原始操作的调用,根据原始方法的返回值来设置环绕通知的返回值 //为什么返回的是Object而不是int的主要原因是Object类型更通用。 Object ret = pjp.proceed(); System.out.println("around after advice ..."); return ret; } }
- 修改App类,调用select方法
public class App { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); BookDao bookDao = ctx.getBean(BookDao.class); int num = bookDao.select(); System.out.println(num); } }
- 修改MyAdvice,对BookDao中的select方法添加环绕通知。
-
环绕通知注意事项↓
- 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知。
- 通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行。
- 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为Object类型。
- 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object。
- 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常。
-
环绕通知案例:测量业务层接口万次执行效率。
-
📚AOP通知获取数据
之前我们写AOP仅仅是在原始方法前后追加一些操作,接下来探讨AOP中数据相关的内容,从
获取参数
、获取返回值
和获取异常
三个方面切入。
🐇环境准备
-
创建一个Maven项目
-
pom.xml添加Spring依赖
<dependencies> <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> </dependencies>
-
添加BookDao和BookDaoImpl类
public interface BookDao { public String findName(int id); } @Repository public class BookDaoImpl implements BookDao { public String findName(int id) { System.out.println("id:"+id); return "itcast"; } }
-
创建Spring的配置类
@Configuration @ComponentScan("com.itheima") @EnableAspectJAutoProxy public class SpringConfig { }
-
编写通知类
@Component @Aspect public class MyAdvice { @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))") 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 Object around() throws Throwable{ Object ret = pjp.proceed(); return ret; } @AfterReturning("pt()") public void afterReturning() { System.out.println("afterReturning advice ..."); } @AfterThrowing("pt()") 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); String name = bookDao.findName(100); System.out.println(name); } }
🐇获取参数
-
非环绕通知获取方式
-
在方法上添加JoinPoint,通过JoinPoint来获取参数。
@Component @Aspect public class MyAdvice { @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))") private void pt(){} @Before("pt()") public void before(JoinPoint jp) Object[] args = jp.getArgs(); System.out.println(Arrays.toString(args)); System.out.println("before advice ..." ); } //...其他的略 }
-
运行App类,可以获取如下内容,说明参数100已经被获取。
-
使用JoinPoint的方式获取参数适用于
前置
、后置
、返回后
、抛出异常后
通知。
-
-
环绕通知获取方式
-
环绕通知使用的是ProceedingJoinPoint。
@Component @Aspect public class MyAdvice { @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))") private void pt(){} @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; } //其他的略 }
-
pjp.proceed()
方法是有两个构造方法,分别是:
-
调用无参数的proceed,当原始方法有参数,会在调用的过程中自动传入参数。
-
但当需要修改原始方法的参数时,就只能采用带有参数的方法,如下:
@Component @Aspect public class MyAdvice { @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))") private void pt(){} @Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable{ Object[] args = pjp.getArgs(); System.out.println(Arrays.toString(args)); args[0] = 666; Object ret = pjp.proceed(args); return ret; } //其他的略 }
-
有了这个特性后,我们就可以在环绕通知中对原始方法的参数进行拦截过滤,避免由于参数的问题导致程序无法正确运行,保证代码的健壮性。
-
-
🐇获取返回值
-
环绕通知获取返回值
@Component @Aspect public class MyAdvice { @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))") private void pt(){} @Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable{ Object[] args = pjp.getArgs(); System.out.println(Arrays.toString(args)); args[0] = 666; Object ret = pjp.proceed(args); return ret; } //其他的略 }
ret
就是方法的返回值,我们可以直接获取,如果需要还可以进行修改。
-
返回后通知获取返回值
@Component @Aspect public class MyAdvice { @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))") private void pt(){} @AfterReturning(value = "pt()",returning = "ret") public void afterReturning(Object ret) { System.out.println("afterReturning advice ..."+ret); } //其他的略 }
-
参数名的问题↓
-
afterReturning方法参数类型的问题:参数类型可以写成String,但是为了能匹配更多的参数类型,建议写成Object类型
-
afterReturning方法参数的顺序问题↓
-
🐇获取异常
-
环绕通知获取异常
@Component @Aspect public class MyAdvice { @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))") private void pt(){} @Around("pt()") public Object around(ProceedingJoinPoint pjp){ Object[] args = pjp.getArgs(); System.out.println(Arrays.toString(args)); args[0] = 666; Object ret = null; try{ ret = pjp.proceed(args); }catch(Throwable throwable){ t.printStackTrace(); } return ret; } //其他的略 }
- 在catch方法中就可以获取到异常,至于获取到异常以后该如何处理,这个就和业务需求有关了。
-
抛出异常后通知获取异常
@Component @Aspect public class MyAdvice { @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))") private void pt(){} @AfterThrowing(value = "pt()",throwing = "t") public void afterThrowing(Throwable t) { System.out.println("afterThrowing advice ..."+t); } //其他的略 }
-
如何让原始方法抛出异常,方式有很多,
@Repository public class BookDaoImpl implements BookDao { public String findName(int id,String password) { System.out.println("id:"+id); if(true){ throw new NullPointerException(); } return "itcast"; } }
-
运行App后,查看控制台,就能看到异常信息被打印到控制台。
-
📚小结
-
知识点1:@Before
名称 @Before 类型 方法注解 位置 通知方法定义上方 作用 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行 -
知识点2:@After
名称 @After 类型 方法注解 位置 通知方法定义上方 作用 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行 -
知识点3:@AfterReturning
名称 @AfterReturning 类型 方法注解 位置 通知方法定义上方 作用 设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法正常执行完毕后执行 -
知识点4:@AfterThrowing
名称 @AfterThrowing 类型 方法注解 位置 通知方法定义上方 作用 设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行 -
知识点5:@Around
名称 @Around 类型 方法注解 位置 通知方法定义上方 作用 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行