AOP(Aspect Oriented Programming,面向切面/面向方法编程),是一种思想。
**场景:**想查看一个方法的执行耗时,是在方法执行前记录一次时间,在方法执行完再记录一次时间,而如果有多个方法需要统计时间,会出现很多重复性代码,而且修改时需要对原方法进行修改。
**AOP的优势:**减少重复代码、代码无入侵、提高效率、维护方便等
应用场景
- 前置:
- 权限检验
- 日志记录等
- 后置:
- 资源释放
- 性能监控等
- 环绕:
- 事务管理等
核心概念
- 连接点(JoinPoint):可被AOP控制的方法(一般所有public方法均可以);
- 通知(Advice):指那些重复的公共逻辑操作,即自定义的方法逻辑操作(比如方法的日志记录,对于日志记录的信息构造以及持久化等操作,就属于通知);
- 切入点(Pointcut):也就是被增强的连接点(即真正被AOP通知所执行的方法);
- 切面(Aspect):一个容器,包含多个切入点和通过(也就是被
@Aspect注解的AOP类,该类可以有多个方法,不同方法可以有不同的操作逻辑,可以针对不同的切入点); - 目标对象(Target):表示通知所应用的对象,也就是切入点所在方法的类的对象(即被代理的原始业务对象);
- 织入(Weaving):将通知嵌入目标对象并创建代理的过程。
AOP执行流程(底层实现:动态代理)

通知类型
@Around:环绕通知,在目标方法执行前后都会被执行,返回值为Object;@Before:前置通知,目标方法前执行,无论异常与否都执行,无返回值;@After:后置通知,目标方法后执行,无论异常与否都执行,无返回值;@AfterReturning:返回后通知,目标方法执行后被执行,有异常不执行,可以为void、Object或者自定义类型;@AfterThrowing:异常后通知,通知方法发生异常后执行,否则不执行,无返回值。
注意:
@Around需要自己在通知方法中声明ProceedingJoinPoint类型对象pjp,然后自己通过[res = ]pjp.proceed();方法来让原方法执行,其他通知则不需要考虑;@Around的方法的返回值必须指定为Object,用来接收原始方法的返回值。
通知顺序
当有多个切面的切入点都匹配到了目标方法,目标方法运行时,各个通知方法都会被执行。
-
不同切面类中:默认按切面类的类名字母排序;
-
目标方法前的通知方法:字母靠前先执行;
-
目标方法后的通知方法:字母靠前后执行;
//示例
@Component
@Aspect
public class AspectA {
@Before("com.test.PointCutConfig.cut()")
public void beforeA(){
System.out.println("切面A 前置");
}@After("com.test.PointCutConfig.cut()") public void afterA(){ System.out.println("切面A 后置"); }}
@Component
@Aspect
public class AspectB {
@Before("com.test.PointCutConfig.cut()")
public void beforeB(){
System.out.println("切面B 前置");
}@After("com.test.PointCutConfig.cut()") public void afterB(){ System.out.println("切面B 后置"); }}
/*
- 都是同一个切入点,则所有通知都会生效
- 因为AspectA的字母序列在AspectB之前,即A前B后。
则对于前置而言是A先执行,后才是B,对于后置而言,则是B先执行,后才是A
执行顺序:
切面A 前置
切面B 前置
=====目标方法执行=====
切面B 后置
切面A 后置
*/
-
-
@Order(num)加在切面类上:-
目标方法前的,数字小先执行;
-
目标方法后的,数字大先执行。
//示例
@Order(1) // 数值小
public class AspectA {
... //与上面示例内容相同
}@Order(2) // 数值大
public class AspectB {
...
}/*
因为使用Order注解标注,A的数字比B小,即A在前B在后,因此:
对于前置,数字小(在前)的先执行,对于后置,数字大(在后)的先执行执行顺序:
切面A 前置
切面B 前置
=====目标方法执行=====
切面B 后置
切面A 后置
*/
-
**总结:**前置在前先执行,后置在后先执行
切面类在前(字母排序在前/数字排序在前,即小在前)的前置方法先执行;切面类在后的后置方法先执行。
切点表达式
用来决定项目中哪些方法需要加入通知
execution([访问修饰符]? 返回值 [包名.类名.]?方法名(方法形参) [throws 异常]?):根据方法签名来匹配,可通过|| 或者 &&来连接多个execution(...)进行匹配。其中?表示前面[ ]块可省略。- 访问修饰符(可选)Spring AOP 仅支持代理
public方法,直接省略不写 - 返回值类型 (必填)匹配方法返回值:
void(无返回)、String(字符串)、*(任意返回值) - 包名/类名(可选)定位方法所在的包和类,支持通配符
- 方法名(必填)要匹配的方法名称,支持通配符
- 方法形参 (必填)
()= 无参方法;(..)= 任意参数;(String)= 仅 String 参数 - throws 异常(可选)几乎不用,直接忽略
- 访问修饰符(可选)Spring AOP 仅支持代理
@annotation(注解类的引用,eg:com.demo.anno.LogOperation):根据注解匹配,匹配标识有特定注解的方法。
通配符
*:任意位置都能用,表示单个独立的任意符合,可通配上面任意一个参数;..:只能出现在包或参数两个位置,表示多个连续任意符号,可通配任意层级包、任意类型、任意个数的参数。
简单示例
1、项目结构
com.example.demo
├── annotation/ 自定义注解(用于@annotation匹配)
├── service/ 业务类(目标方法,被增强的对象)
├── aspect/ 切面类(编写切点表达式+通知)
└── DemoApplicationTests 测试类
2、自定义注解(用于注解匹配)
package com.example.demo.annotation;
import java.lang.annotation.*;
// 自定义注解:贴在方法上,AOP就会匹配这个方法
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}
3、业务类(目标方法,被匹配的对象)
package com.example.demo.service;
import com.example.demo.annotation.MyLog;
import org.springframework.stereotype.Service;
@Service
public class UserService {
// 无参方法
public void addUser() {
System.out.println("执行:添加用户");
}
// 有参方法
public String getUserById(Integer id) {
System.out.println("执行:根据ID查询用户");
return "用户" + id;
}
// 贴了自定义注解的方法
@MyLog
public void deleteUser() {
System.out.println("执行:删除用户");
}
}
4、切面类(核心:所有切点表达式 + 通知)
package com.example.demo.aspect;
import com.example.demo.annotation.MyLog;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class TestAspect {
// ===================== 1. 基础 execution 表达式 =====================
// 匹配:UserService类中 无参、无返回值 的 addUser() 方法
@Before("execution(void com.example.demo.service.UserService.addUser())")
public void test1(JoinPoint jp) {
System.out.println("[匹配无参方法] 前置通知");
}
// ===================== 2. 通配符 * :匹配任意返回值 =====================
// 匹配:UserService中 任意返回值、getUserById(Integer) 方法
@Before("execution(* com.example.demo.service.UserService.getUserById(Integer))")
public void test2() {
System.out.println("[匹配指定参数方法] 前置通知");
}
// ===================== 3. 通配符 .. :匹配任意参数 =====================
// 匹配:UserService中 任意返回值、任意参数 的 所有方法
@Before("execution(* com.example.demo.service.UserService.*(..))")
public void test3() {
System.out.println("[匹配类下所有方法] 前置通知");
}
// ===================== 4. 通配符 .. :匹配任意层级包 =====================
// 匹配:com.example.demo 包及其子包下 所有类的所有方法
@Before("execution(* com.example.demo..*.*(..))")
public void test4() {
System.out.println("[匹配全包所有方法] 前置通知");
}
// ===================== 5. 通配符 * :匹配方法名前缀 =====================
// 匹配:UserService中 以 get 开头的任意方法
@Before("execution(* com.example.demo.service.UserService.get*())")
public void test5() {
System.out.println("[匹配方法名前缀] 前置通知");
}
// ===================== 6. @annotation :匹配注解 =====================
// 匹配:所有添加了 @MyLog 注解的方法
@Before("@annotation(com.example.demo.annotation.MyLog)")
public void test6() {
System.out.println("[匹配注解方法] 前置通知");
}
// ===================== 7. 组合匹配:&& 且 =====================
// 匹配:UserService中 以 delete 开头 + 任意参数 的方法,其中execution(* *(..))表示任意返回值类型+任意方法+任意参数类型的方法(所有方法恒成立)
@Before("execution(* com.example.demo.service.UserService.delete*(..)) && execution(* *(..))")
public void test7() {
System.out.println("[组合匹配] 前置通知");
}
}
注意
如果需要复用切点表达式,可以使用 @Pointcut(xxx) 将公共的切点表达式抽取出来,需要时直接引用即可
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class TestAspect {
//抽取需要重复使用的切点表达式
@Pointcut("execution(* com.example.demo.service.UserService.*(..))")
public void userServicePointcut(){}
//public:表示在其他外部切面类中也可以使用该表达式
//private:表示当前切点表达式只能在当前切面类中使用
//使用
@Around("userServicePointcut()")
public Object testFunc(){
...
}
}
连接点对象
父类为 JoinPoint 类型,子类为 ProceedingJoinPoint 类型,用它可以获得方法执行时的相关信息,比如目标类名、方法名、参数等。
对于环绕通知,只能使用 ProceedingJoinPoint ,而对于其他四种类型的通知,只能使用 JoinPoint (环绕通知是必须在形参中声明 ProceedingJoinPoint ,而其他通知如果需要获取相关信息,才需要在形参中声明 JoinPoint )
常见方法
getTarget():获取目标对象;getTarget().getClass().getName():获取目标类;getSignature().getName():获取目标方法名;getArgs():获取目标方法参数。
注解匹配使用示例-日志记录
1、引入依赖
<!--引入springAOP依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2、编写所需要的注解类
在anno包下编写所需要的注解类,并使用元注解标识
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解,用于记录管理员操作日志
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AdminLog {
}
3、编写AOP类对特定方法根据需要进行编程
如果是使用 @execution(xxx) ,则只需将通知类型中的内容更换即可
import com.hyltest.mapper.AdminMapper;
import com.hyltest.mapper.OperateLogMapper;
import com.hyltest.pojo.entity.OperateLog;
import com.hyltest.utils.CurrentHolder;
import lombok.RequiredArgsConstructor;
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;
import java.time.LocalDateTime;
import java.util.concurrent.CompletableFuture;
@RequiredArgsConstructor
@Slf4j
@Aspect
@Component
public class OperateLogAspect {
private final AdminMapper adminMapper;
private final OperateLogMapper operateLogMapper;
@Around("@annotation(com.hyltest.anno.AdminLog)") //通过注解的形式将方法操作作为注解的操作
public Object recordAdminOperateLog(ProceedingJoinPoint pjp) throws Throwable{
OperateLog operateLog = new OperateLog();
//-----**通过pjp获取想要的数据**------
//获取执行的方法名
String methodName = pjp.getSignature().getName();
//获取目标类
String className = pjp.getTarget().getClass().getSimpleName();
//获取当前操作时间
operateLog.setCreateTime(LocalDateTime.now());
//获取当前操作人id
Integer id = CurrentHolder.getCurrentId();
//根据id获取当前操作人名字
String name = adminMapper.getNameById(id);
//组装操作日志
operateLog.setAdminId(id);
operateLog.setMethod(name+"操作了"+className+"."+methodName+"方法");
Object result = null;
try {
// **当公共逻辑执行完,放行去执行原方法**
result = pjp.proceed(); //获得原方法的返回值
return result;
} catch (Exception e) {
// 继续抛出异常,不影响业务逻辑
throw e;
} finally {
// 异步保存日志,避免影响主业务流程
CompletableFuture.runAsync(() -> {
try {
//保存操作日志
operateLogMapper.insertNewOperateLog(operateLog);
} catch (Exception ex) {
// 记录日志保存失败,但不影响业务
log.error("保存操作日志失败: {}", ex.getMessage(), ex);
}
});
}
}
}
注意:
- AOP的方法逻辑就是将公共的操作代码提取出来;
- AOP类需要在类上加上
@Component与@Aspect注解,其中component表示将当前AOP类交给spring容器管理,aspect用于声明当前类是一个AOP类; - 通过
pjp.getSignature()获得的对象是连接点JointPoint对象,为什么是连接点对象而不是切入点对象?因为切入点是包含在连接点中的,有点类似于子类与父类的关系,编程时要依赖上层接口而不是具体实现类; - AOP类的方法的返回值必须为Object;
- Around注解的方法必须在方法形参上声明ProceedJoinPoint对象,显示调用proceed方法才能执行原方法;
4、在需要的类/方法上添加注解
以controller的某一个方法为例
@AdminLog //自定义注解
@PostMapping("/addAdmin")
public Result addAdmin(@RequestBody Admin admin) {
log.info("新增管理员信息:admin={}", admin);
adminService.addAdmin(admin);
return Result.success();
}