AOP与动态代理
1)AOP
【1】核心概念
- 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
- 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
- 切入点: PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
【2】代码例子
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* 编写AOP面向切面/方向编程,为了解决一些类或者方法需要共同处理的事务
* 比如:以下是计算某包下所有方法的运行时间
*/
@Aspect//表示该类是AOP类
@Component//把该类交给IOC容器管理
@Slf4j
public class TimeAspect {
@Around("execution(* com.itheima.service.*.*(..))")//切入点表达式,表示要作用到的哪些方法
public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//1、记录开始时间
long begin=System.currentTimeMillis();
//2、调用原始方法运行
Object result= proceedingJoinPoint.proceed();
//3、记录结束时间
long end=System.currentTimeMillis();
log.info(proceedingJoinPoint.getSignature()+"方法执行耗时:{}ms",end-begin);//得到原始方法的签名:proceedingJoinPoint.getSignature()
//4、返回运行原始方法的返回值
return result;
}
}
【3】上述代码AOP执行流程讲解
- 当调用目标类中的方法执行时
- 会去执行AOP类中的代码
- AOP类中的代码就包括原目标的方法,只不过在该方法的前后可以进行逻辑代码的编写,以实现某个目标。
内部解析:当调用目标类中的方法执行时,会去执行AOP类中的代码,这个时候会生成一个动态代理对象,这个动态代理对象通俗来讲,就是在目标类方法的基础上进行功能的扩充。以减少代码的复用性。方便大范围修改,统一管理。
【4】通知类型
-
@Around:环绕通知:此注解标注的通知方法在目标方法前、后都被执行
-
@Before:前置通知:此注解标注的通知方法在目标方法前被执行
-
@After :后置通知:此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行-
-
@AfterReturning :返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
-
@AfterThrowing :异常后通知,此注解标注的通知方法发生异常后执行
简单代码演示:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class MyAspect {
@Around("execution(* com.example.MyService.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕通知 - 方法执行前");
Object result;
try {
result = joinPoint.proceed();
System.out.println("环绕通知 - 方法执行后");
} catch (Exception e) {
System.out.println("环绕通知 - 方法抛出异常");
throw e;
}
return result;
}
@Before("execution(* com.example.MyService.*(..))")
public void beforeAdvice() {
System.out.println("前置通知 - 方法执行前");
}
@After("execution(* com.example.MyService.*(..))")
public void afterAdvice() {
System.out.println("后置通知 - 方法执行后");
}
@AfterReturning("execution(* com.example.MyService.*(..))")
public void afterReturningAdvice() {
System.out.println("返回后通知 - 方法正常返回");
}
@AfterThrowing(pointcut = "execution(* com.example.MyService.*(..))", throwing = "ex")
//pointcut 属性用于指定哪些方法会触发异常后通知,而 throwing 属性用于定义接收异常对象的参数名。
public void afterThrowingAdvice(Exception ex) {
System.out.println("异常后通知 - 方法抛出异常:" + ex.getMessage());
}
}
注意事项:
- @Around环绕通知需要自己调用 ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行
- @Around环绕通知方法的返回值,必须指定为object,来接收原始方法的返回值。
【5】@PointCut
【6】通知的执行顺序
1.不同切面类中,默认按照切面类的类名字母排序
- 目标方法前的通知方法:字母排名靠前的先执行
- 目标方法后的通知方法:字母排名靠前的后执行
2.用 @Order(数字) 加在切面类上来控制顺序
- 目标方法前的通知方法:数字小的先执行
- 目标方法后的通知方法:数字小的后执行
【7】切入点表达式
定义
:切入点表达式:描述切入点方法的一种表达式
作用
:主要用来决定项目中的哪些方法需要加入通知
常见形式:
execution(.....):
根据方法的签名来匹配
execution(* com.*.service.*.update*(*))
- 第一个*表示通配任何返回值
- 第二个*表示通配第二级所有的包
- 第三个*表示通配service包下的所有接口
- 第四个*表示通配方法名前缀包含update的所有方法
- 第五个*表示通配任意类型的一个参数
注意事项
根据业务需要,可以使用 且 (&&)、或()、非(!) 来组合比较复杂的切入点表达式。
书写建议
- 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如: 查询类方法都是 find 开头,更新类方法都是 update开头
- 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。
- 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 …,使用“匹配单个包。
@annotation(....)
:根据注解匹配
- 第一步:定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解
*/
@Retention(RetentionPolicy.RUNTIME)// 注解,指定该注解在运行时可被反射读取和使用;
@Target(ElementType.METHOD) //注解,指定该注解可以应用在方法上。
public @interface MyLog {
// 可以在这里定义注解的属性,用于进一步配置日志记录的行为
// 例如:记录级别、日志消息模板等
}
- 第二部:定义一个AOP类
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
/**
* 自定义切面,实现AOP功能
*/
@Aspect // 表示该类是AOP类
@Component // 把该类交给IOC容器管理
@Slf4j // 引入日志注解,方便打印日志
public class MyAspect {
/**
* 定义切点,该切点用于匹配被 "@全类名" 注解标记的方法
*/
@Pointcut("@annotation(自定义注解的全类名)")
private void pt() {
// 此处为空方法体,仅作为一个切点的定义
}
/**
* 前置通知,拦截使用指定注解标记的方法,并在方法执行前进行处理
*/
@Before("pt()")
public void before() {
log.info("MyAspect......before");
}
}
- 第三步:在service接口的对应的实现类加上自定义的@MyLog注解
例如
@MyLog
@Override
public List<Dept> list() {
List<Dept> deptList = deptMapper.list();
return deptList;
}
@MyLog
@Override
public void delete(Integer id) {
//1. 删除部门
deptMapper.delete(id);
}
【8】连接点
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等
- 对于
@Around
通知,获取连接点信息只能使用ProceedingJoinPoint
- 对于其他四种通知,获取连接点信息只能使用
JinPoint
,它是ProceedingJoinPoint
的父类型
2)AOP案例–记录操作日志
步骤
准备:
- 在案例工程中引入AOP的起步依赖
- 准备好的数据库表结构,并引入对应的实体类
编码:
- 自定义注解 @Log
- 定义切面类,完成记录操作日志的逻辑
- 定义插入日志信息的mapper接口
- 需要作用的方法加上自定义注解@Log
3)动态代理
动态代理是一种实现代理模式的技术,在运行时生成代理对象,可以在代理对象的方法调用前后插入额外的逻辑。通过使用动态代理,可以对原始对象进行增强,而无需修改原始对象的代码。
4)对比
AOP和动态代理都可以用于实现类似的功能,但它们之间存在一些差异:
-
关注点的分离:AOP通过将横切关注点与核心业务逻辑分离,提供了更好的代码模块化和可重用性。而动态代理主要用于在方法调用前后添加额外逻辑。
-
代码修改:AOP不需要修改原始对象的代码,它通过将切面织入到应用程序中来实现功能增强。而动态代理需要使用代理类包装原始对象并添加额外的逻辑。
-
支持的目标类型:AOP可以用于任何对象(包括类、接口、甚至其他切面),并且可以对整个类或类中的特定方法进行增强。而动态代理通常用于接口实现类,通过生成实现了目标接口的代理对象来实现代理功能。