AOP是在不改原有代码的前提下对其进行增强
AOP(Aspect Oriented Programming)面向切面编程,在不惊动原始设计的基础上为其进行功能增强,前面咱们有技术就可以实现这样的功能即代理模式。Java设计模式——代理模式-CSDN博客
基础概念
连接点(Join Point): 连接点是在应用程序执行期间可以插入切面的点。在Spring中,连接点通常是方法调用,尽管它也可以是某些特定的程序执行点,比如异常处理或字段访问。
切入点(Pointcut): 切入点是一组连接点的集合,其中每个连接点都符合特定的条件。切入点定义了在应用程序中哪些连接点应该被拦截,并应用相应的通知。
通知(Advice): 通知是在切面的特定连接点上执行的代码。在Spring AOP中,有五种类型的通知:
- 前置通知(Before Advice):在连接点之前执行。
- 后置通知(After Advice):在连接点之后执行。
- 返回通知(After Returning Advice):在连接点正常完成后执行。
- 异常通知(After Throwing Advice):在连接点抛出异常后执行。
- 环绕通知(Around Advice):在连接点前后都执行。
通知类(Advisor): 通知类是将通知和切入点组合在一起的对象。在Spring中,通知类通常是由一个切面定义的,它包含一个或多个通知和一个切入点。
切面(Aspect): 切面是通知和切入点的组合。它描述了在何处以及何时应用通知。在Spring中,切面通常是一个带有通知方法的普通类,并且使用注解或XML配置来定义切入点和通知。
AOP实现步骤
步骤1:添加依赖:
aspectjweaver、springframework
步骤2:定义接口与实现类
步骤3:定义通知类和通知
public class MyAdvice {
public void method(){
System.out.println(System.currentTimeMillis());
}
}
步骤4:定义切入点
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
public void method(){
System.out.println(System.currentTimeMillis());
}
}
步骤5:制作切面
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}切面是用来描述通知和切入点之间的关系
步骤6:将通知类配给容器并标识其为切面类
@Aspect
public class MyAdvice {
}
步骤7:开启注解格式AOP功能
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}
AOP工作流程
由于AOP是基于Spring容器管理的bean做的增强,所以整个工作过程需要从Spring加载bean说起:
流程1:Spring容器启动
- * 容器启动就需要去加载bean,哪些类需要被加载呢?
- * 需要被增强的类,如:BookServiceImpl
- * 通知类,如:MyAdvice
- * 注意此时bean对象还没有创建成功
流程2:读取所有切面配置中的切入点
流程3:初始化bean
判定bean对应的类中的方法是否匹配到任意切入点
- * 注意第1步在容器启动的时候,bean对象还没有被创建成功。
- * 要被实例化bean对象的类中的方法和切入点进行匹配
匹配失败,创建原始对象,如`UserDao`
- 匹配失败说明不需要增强,直接调用原始对象的方法即可。
匹配成功,创建原始对象的代理对象,如:`BookDao`
- * 匹配成功说明需要对其进行增强
- * 对哪个类做增强,这个类对应的对象就叫做目标对象
- * 因为要对目标对象进行功能增强,而采用的技术是动态代理,会为其创建一个代理对象
- * 最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强
流程4:获取bean执行方法
- * 获取的bean是原始对象时,调用方法并执行,完成操作
- * 获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
验证容器中是否为代理对象
为了验证IOC容器中创建的对象和我们刚才所说的结论是否一致,首先先把结论理出来:
- * 如果目标对象中的方法会被增强,那么容器中将存入的是目标对象的代理对象
- * 如果目标对象中的方法不被增强,那么容器中将存入的是目标对象本身。
- 当前类的getClass()方法
AOP配置管理
切入点表达式:
要进行增强的方法的描述方式
execution(public User com.itheima.service.UserService.findById(int))
- * execution:动作关键字,描述切入点的行为动作,execution表示执行到指定切入点
- * public:访问修饰符,还可以是public,private等,可以省略
- * User:返回值,写返回值类型
- * com.itheima.service:包名,多级包使用点连接
- * UserService:类/接口名称
- * findById:方法名
- * int:参数,直接写参数的类型,多个类型用逗号隔开
- * 异常名:方法定义中抛出指定异常,可以省略
通配符
`*`:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
execution(public * com.itheima.*.UserService.find*(*))匹配com.itheima包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法
`..`:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
execution(public User com..UserService.findById(..))匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法
`+`:专用于匹配子类类型
execution(* *..*Service+.*(..))这个使用率较低,描述子类的,咱们做JavaEE开发,继承机会就一次,使用都很慎重,所以很少用它。*Service+,表示所有以Service结尾的接口的子类。
AOP通知类型
-
前置通知
-
后置通知
-
==环绕通知(重点)==
-
返回后通知(了解)
-
抛出异常后通知(了解)
环绕通知
@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 ...");
}
}
ProceedingJoinPoint pjp
获取原方法、设置返回值类型
@Around("pt2()")
public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
//表示对原始操作的调用
Object ret = pjp.proceed();
System.out.println("around after advice ...");
return ret;
}
获取方法名、接口名和签名信息
@Component
@Aspect
public class ProjectAdvice {
//配置业务层的所有方法
@Pointcut("execution(* com.itheima.service.*Service.*(..))")
private void servicePt(){}
//@Around("ProjectAdvice.servicePt()") 可以简写为下面的方式
@Around("servicePt()")
public void runSpeed(ProceedingJoinPoint pjp){
//获取执行签名信息
Signature signature = pjp.getSignature();
//通过签名获取执行操作名称(接口名)
String className = signature.getDeclaringTypeName();
//通过签名获取执行操作名称(方法名)
String methodName = signature.getName();
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
pjp.proceed();
}
long end = System.currentTimeMillis();
System.out.println("万次执行:"+ className+"."+methodName+"---->" +(end-start) + "ms");
}
}
通知获取数据
* 获取切入点方法的参数,所有的通知类型都可以获取参数
- * JoinPoint:适用于前置、后置、返回后、抛出异常后通知
- * ProceedingJoinPoint:适用于环绕通知
@Before("pt()") public void before(JoinPoint jp) Object[] args = jp.getArgs(); System.out.println(Arrays.toString(args)); System.out.println("before advice ..." ); }
@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; }
* 获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究
- * 返回后通知
- * 环绕通知
@AfterReturning(value = "pt()",returning = "ret") public void afterReturning(Object ret) { System.out.println("afterReturning advice ..."+ret); }
@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; }
* 获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究
- * 抛出异常后通知
- * 环绕通知
@AfterThrowing(value = "pt()",throwing = "t") public void afterThrowing(Throwable t) { System.out.println("afterThrowing advice ..."+t); }
@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; }
环绕通知注意事项
- 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
- 通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行
- 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为Object类型
- 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
- 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常