aspect初识
Aspect初识
概述
Spring有两个核心的概念,一个是IOC/DI,一个是AOP,通过这篇文章我们来认识下AOP。
一、什么是AOP?
AOP的全称是Aspect Oriented Programming,即面向切面编程。是实现功能统一维护的一种技术,它将业务逻辑的各个部分进行隔离,使开发人员在编写业务逻辑时可以专心于核心业务,从而提高了开发效率。
作用:在不修改源码的基础上,对已有方法进行增强。
实现原理:动态代理技术
优势:减少重复代码、提高开发效率、维护方便
应用场景:事务处理、日志管理、权限控制、异常处理等方面。
二、AOP和OOP的区别?
我们都知道OOP是一种编程思想,那么AOP也是一种编程思想,编程思想主要的内容就是指导程序员该如何编写程序,所以它们两个是不同的编程范式。
AOP 是 OOP(Object Oriented Programming-面向对象编程)的一种延续,二者互补,并不对立。
AOP 的目的是将横切关注点(如日志记录、事务管理、权限控制、接口限流、接口幂等等)从核心业务逻辑中分离出来,通过动态代理、字节码操作等技术,实现代码的复用和解耦,提高代码的可维护性和可扩展性。
OOP 的目的是将业务逻辑按照对象的属性和行为进行封装,通过类、对象、继承、多态等概念,实现代码的模块化和层次化(也能实现代码的复用),提高代码的可读性和可维护性。
三、AOP的核心概念
涉及到:连接点、切点、通知、通知类、切面
1. 连接点(JoinPoint):指能被拦截到的点,在Spring中只有方法能被拦截
2. 切点(Pointcut):指要对那些连接点进行拦截,即被增强的方法
3. 通知(Advice):指拦截后要做的事情,即切点被拦截后执行的方法
4. 通知类:定义通知的类
5. 切面(Aspect):描述通知与切点的对应关系。它定义了在切点上应用的具体行为。
6. 织入(Weaving):织入是将增强功能添加到目标类具体连接点上的过程
AOP有三种织入方式:
① 编译期织入:需要特殊的Java编译器(例如AspectJ的ajc);
② 装载期织入:要求使用特殊的类加载器,在装载类的时候对类进行增强;
③ 运行时织入:在运行时为目标类生成代理实现增强。Spring采用了动态代理的方式实现了运行时织入,而AspectJ采用了编译期织入和装载期织入的方式
四、AOP实现步骤
1. 添加依赖
pom.xml
1 <dependency> 2 <groupId>org.aspectj</groupId> 3 <artifactId>aspectjweaver</artifactId> 4 <version>1.9.4</version> 5 </dependency>
2. 定义接口与实现类
环境准备的时候,BookDaoImpl已经准备好,不需要做任何修改
3. 定义通知类和通知
通知就是切点被拦截后执行的方法,这里我们举例时涉及的拦截后执行的方法就是当前系统时间的打印。
1 public class MyAdvice { 2 public void method(){ 3 System.out.println(System.currentTimeMillis()); 4 } 5 }
类名和方法名没有要求,可以任意。
4. 定义切点
示例代码如下:
1 public class MyAdvice { 2 @Pointcut("execution(void com.itheima.dao.BookDao.update())") 3 private void pt(){} 4 public void method(){ 5 System.out.println(System.currentTimeMillis()); 6 } 7 }
说明:
1)切点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑。
2)这里的 @Pointcut 注解定义了一个切点,名为 pt。它使用表达式 execution(void com.itheima.dao.BookDao.update()) 来匹配 com.itheima.dao.BookDao 类中的 update() 方法的执行
5. 制作切面
切面是用来描述通知和切点之间的关系,如何进行关系的绑定?
1 public class MyAdvice { 2 @Pointcut("execution(void com.itheima.dao.BookDao.update())") 3 private void pt(){} 4 5 @Before("pt()") 6 public void method(){ 7 System.out.println(System.currentTimeMillis()); 8 } 9 }
绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置
说明:
1)在 Spring AOP 中,切面通常是一个带有切点和通知定义的类。这里,MyAdvice
类本身就是一个切面,它使用了 @Before
注解来定义在切点 pt()
匹配的连接点之前要执行的通知。
2)@Before翻译过来是之前,也就是说通知会在切点方法执行之前执行,除此之前还有其他四种类型,后面会讲。
6. 将通知类配给容器并标识其为切面类
@Aspect 是 AspectJ 的一个注解,用于定义切面(Aspect)。在 Spring AOP 中,虽然也使用了这个注解,但它实际上是来自于 AspectJ 的库。但在 Spring AOP 中也被广泛使用,以便于开发者使用 AspectJ 的切面定义功能。
1 @Component 2 @Aspect 3 public class MyAdvice { 4 @Pointcut("execution(void com.itheima.dao.BookDao.update())") 5 private void pt(){} 6 7 @Before("pt()") 8 public void method(){ 9 System.out.println(System.currentTimeMillis()); 10 } 11 }
7. 开启注解格式AOP功能
@EnableAspectJAutoProxy 是 Spring 框架中的一个注解,用于启用 AspectJ 的自动代理功能。这意味着 Spring 将支持基于切面的 AOP 功能。
1 @Configuration 2 @ComponentScan("com.itheima") 3 @EnableAspectJAutoProxy 4 public class SpringConfig { 5 6 }
8. 运行程序
1 public class App { 2 public static void main(String[] args) { 3 ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); 4 BookDao bookDao = ctx.getBean(BookDao.class); 5 bookDao.update(); 6 } 7 }
看到在执行update方法之前打印了系统时间戳,说明对原始方法进行了增强,AOP编程成功。
五、语法表达式
1. 切点表达式
切点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数列表) 异常名)
对于这个格式,我们不需要硬记,通过一个例子,理解它:
execution(public User com.itheima.service.UserService.findById(int))
1) 动作关键字: 描述切入点的行为动作,例如 execution 表示执行到指定切入点
2)访问修饰符:可以是public,private等,可以省略
3)返回值:比如这里的User,*表示任意类型
4)包名
说明:多级包使用点连接,比如这里的com.itheima.service。包名使用*表示任意包,多级包结构要写多个*,使用*..表示任意包结构。
5)类/接口名称:比如这里的UserService
6)方法名:比如这里的findById
7)参数列表
- 基本数据类型直接写类型
- 引用类型写包名.类名 比如java.lang.String
- *表示匹配一个任意类型参数
- .. 表示匹配任意类型任意个数的参数
8)异常名:方法定义中抛出指定异常,可以省略
9) 全通配:* *..*(..)
2. 通配符
我们使用通配符描述切入点,主要的目的就是简化之前的配置,具体都有哪些通配符可以使用?
*:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
execution(public * com.itheima.*.UserService.find*(*))
匹配com.itheima包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法
..:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
execution(public User com..UserService.findById(..))
匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法
+:专用于匹配子类类型
execution(* *..*Service+.*(..))
1 匹配接口,能匹配到 2 execution(void com.itheima.dao.impl.BookDaoImpl.update()) 3 匹配实现类,能匹配到 4 execution(* com.itheima.dao.impl.BookDaoImpl.update()) 5 返回值任意,能匹配到 6 execution(* com.itheima.dao.impl.BookDaoImpl.update(*)) 7 返回值任意,但是update方法必须要有一个参数,无法匹配,要想匹配需要在update接口和实现类添加参数 8 execution(void com.*.*.*.*.update()) 9 返回值为void,com包下的任意包三层包下的任意类的update方法,匹配到的是实现类,能匹配 10 execution(void com.*.*.*.update()) 11 返回值为void,com包下的任意两层包下的任意类的update方法,匹配到的是接口,能匹配 12 execution(void *..update()) 13 返回值为void,方法名是update的任意包下的任意类,能匹配 14 execution(* *..*(..)) 15 匹配项目中任意类的任意方法,能匹配,但是不建议使用这种方式,影响范围广 16 execution(* *..u*(..)) 17 匹配项目中任意包任意类下只要以u开头的方法,update方法能满足,能匹配 18 execution(* *..*e(..)) 19 匹配项目中任意包任意类下只要以e结尾的方法,update和save方法能满足,能匹配 20 execution(void com..*()) 21 返回值为void,com包下的任意包任意类任意方法,能匹配,*代表的是方法 22 execution(* com.itheima.*.*Service.find*(..)) 23 将项目中所有业务层方法的以find开头的方法匹配 24 execution(* com.itheima.*.*Service.save*(..)) 25 将项目中所有业务层方法的以save开头的方法匹配
六、通知类型
AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置。
那么,通知具体要添加到切入点的哪里?
1. AOP有以下5种通知类型
1. 前置通知(Before): 在方法执行前添加功能
2. 后置通知(After): 无论方法是否抛出异常,都会执行该通知
3. 异常通知(AfterThrowing):在方法抛出异常后添加功能
4. 返回通知(AfterReturning):在方法返回结果之后执行(因此该通知方法在方法抛出异常时,不能执行)
5. 环绕通知(Around):在方法执行前后添加功能
2. 注意事项
1)返回通知和异常通知,只有一个会执行
2)环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式,它可以实现其他四种通知类型的功能