大家好,我是你们天天跟 BUG 斗智斗勇的后端博主~不知道你们有没有过这种经历:写了 10 个接口,每个接口都要加 "记录请求参数""统计耗时""打印返回值" 的代码,复制粘贴到第 5 个时,手都快按出火星子了 ------ 这时候要是有人告诉你,有个叫 AOP 的东西能帮你把这些重复活 "自动化",是不是瞬间想原地拜师?
今天咱就把 AOP 扒得明明白白,从 "它是啥" 到 "怎么用它写日志",全程无废话,还带实操代码,看完直接能上手!
一、先搞懂:AOP 到底是个啥 "偷懒工具"?
- AOP:是Spring的2大核心(IoC和AOP)之一。
- AOP:Aspect Oriented Programming,面向切面编程。是通过预编译方式(aspectj)或者运行期动态代理(Spring)实现程序功能的统一维护的技术。
- AOP是OOP(Object Oriented Programming)的技术延续,是软件开发中的一个热点,也是Spring中的一个重要内容。利用AOP可以实现对业务逻辑各个部分之间的隔离,从而使得业务逻辑各部分之间的耦合性降低,提高程序的可重用性,同时提高了开发效率。
1. 一句话介绍:不碰业务代码,还能给它加功能
AOP(面向切面编程)本质是个 "后端特工"------ 比如你写了个用户登录接口(核心业务),想加 "记录登录日志""校验 token" 功能,不用在登录代码里改一行,AOP 能偷偷在接口执行前 / 后把这些活干了,主打一个 "润物细无声"。
2. 它的核心作用:专治 "重复代码绝症"
咱后端常见的 "重复病症":
- 每个接口都要写日志(谁调用、传了啥、返回啥)
- 每个方法都要加事务(开始 / 提交 / 回滚)
- 多个接口要做权限校验(没登录就拦截)
AOP 能把这些 "重复操作" 抽成一个独立的 "切面",然后告诉它:"你去盯着所有 Controller 接口,执行前先校验权限,执行后记录日志"------ 从此告别复制粘贴,改需求时只改一个地方就行!
3. 本质 & 优势:动态代理是它的 "隐身衣"
AOP 底层靠动态代理实现(简单说就是给目标方法套个 "壳",壳里装增强逻辑),优势简直戳中后端痛点:
-
作用:不修改源码的情况下,进行功能增强,通过动态代理实现的
💡有2种常用的动态代理技术:
- JDK的动态代理,基于接口实现代理
- Cglib动态代理,基于继承实现代理,不要求有接口
所以SpringBoot的AOP采用哪种动态代理呢?
- SpringBoot从2.0开始,默认采用cglib动态
-
优势:减少重复代码,提高开发效率,降低耦合度方便维护
-
比如:给功能增加日志输出, 事务管理的功能
优势 | 大白话解释 |
---|---|
解耦 | 业务代码(登录)和增强代码(日志)彻底分开,不用混在一起堆成 "屎山" |
复用 | 一个切面能给 100 个方法用,不用写 100 遍重复代码 |
灵活 | 想给哪个方法加功能、想加啥功能,改个配置就行,不用动业务逻辑 |
少 bug | 重复代码少了,复制粘贴时手抖少写个分号的概率也低了! |
二、AOP 的 "黑话" 翻译:别被术语吓住!
刚学 AOP 时,总被 "切面""通知" 这些词搞懵,其实用 "餐厅服务" 类比一下,秒懂!
术语 | 黑话翻译 | 餐厅类比 |
---|---|---|
切面(Aspect) | 要做的 "增强任务集合" | 服务员的 "工作清单"(端菜、收碗、找零) |
切入点(Pointcut) | 要增强的 "目标方法"(比如所有 Controller 接口) | 服务员要服务的 "特定餐桌"(比如 1 号桌、VIP 区) |
通知(Advice) | 具体的 "增强动作"(比如方法执行前校验) | 清单里的 "单个任务"(给 1 号桌端菜、收 3 号桌碗) |
目标对象(Target) | 被增强的 "原始业务对象"(比如 UserController) | 被服务的 "顾客" |
代理对象(Proxy) | AOP 生成的 "带增强功能的对象" | 穿了工作服、会按清单做事的 "服务员" |
- 目标对象(Target):要代理的/要增强的目标对象。
- 代理对象(Proxy):目标对象被AOP织入增强后,就得到一个代理对象
- 连接点(JoinPoint):能够被拦截到的点。在Spring里指的是方法,指能增强的方法
- 切入点(PointCut) :要对哪些连接点进行拦截的定义。要增强的方法
- 通知/增强(Advice) :拦截到连接点之后要做的事情。如何增强,额外添加上去的功能和能力
- 切面(Aspect) :是切入点和通知的结合。 告诉Spring的AOP:要对哪个方法,做什么样的增强
- 织入(Weaving):把增强/通知 应用到 目标对象来创建代理对象的过程
三、5 种通知类型:该在 "什么时候" 干活?
通知就是 AOP 的 "动作指令",关键是选对 "执行时机",比如日志要在方法执行后记,权限校验要在执行前做:
通知类型 | 执行时机 | 经典场景 |
---|---|---|
前置通知(Before) | 目标方法执行前 | 权限校验、参数合法性检查 |
后置通知(AfterReturning) | 目标方法正常执行后 | 记录返回值、统计正常耗时 |
异常通知(AfterThrowing) | 目标方法抛异常时 | 记录异常日志(比如保存报错信息到数据库) |
最终通知(After) | 目标方法无论成功 / 失败都执行 | 释放资源(比如关闭数据库连接) |
环绕通知(Around) | 目标方法执行前后都能插手 | 全流程控制(比如统计总耗时、手动控制方法是否执行) |
划重点:环绕通知是 "全能选手",能拿到目标方法的入参、返回值,还能决定要不要执行目标方法,日志功能首选它!
四、实操:SpringBoot 里用 AOP,3 步搞定!
光说不练假把式,咱用 SpringBoot 搭个 AOP 环境,先跑通基础流程:
步骤 1:加依赖(Maven)
xml
<!-- AOP核心依赖,SpringBoot已经封装好了 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
步骤 2:写切面类(核心!)
用@Aspect标注重庆,用@Component让 Spring 扫描到,再用通知注解写增强逻辑:
java
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
// 1. 标为切面类 + 交给Spring管理
@Aspect
@Component
public class LogAspect {
// 2. 定义切入点:要增强哪些方法?(这里先写个简单的,后面细讲表达式)
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
public void logPointcut() {} // 这个方法只是个"标记",内容不用写
// 3. 写通知:在切入点方法执行前做什么
@Before("logPointcut()")
public void doBefore() {
System.out.println("前置通知:方法要执行啦,我先校验下权限~");
}
// 后置通知:方法正常执行后
@AfterReturning("logPointcut()")
public void doAfterReturning() {
System.out.println("后置通知:方法执行完啦,我记录下返回值~");
}
}
步骤 3:写个测试接口
less
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/login")
public String login(String username) {
return username + "登录成功!";
}
}
启动项目,访问http://localhost:8080/user/login?username=zhangsan,控制台会打印:
前置通知:方法要执行啦,我先校验下权限~
后置通知:方法执行完啦,我记录下返回值~
成了!这就是 AOP 的基本用法,不用改 Controller 一行代码,就加了增强功能~
五、切入点:怎么精准 "瞄准" 要增强的方法?
刚才的execution(* com.example.demo.controller..(..))就是切入点表达式,它是 AOP 的 "导航仪",决定了增强逻辑要加到哪个方法上。
1. 获取切入点的 2 种方式
咱后端常用的就这俩,注解式更灵活,XML 式老项目可能会用:
方式 | 写法 | 适用场景 |
---|---|---|
注解式(推荐) | 用@Pointcut+ 表达式,比如上面的例子 | 大多数 SpringBoot 项目,配置简单,一目了然 |
XML 式 | 在 applicationContext.xml 里配(现在很少用了) | 老 Spring 项目,需要 XML 配置的场景 |
2. 切入点表达式:学会这 3 个,够用 90% 场景!
最常用的是execution表达式,格式记牢:
scss
execution(访问修饰符 返回值 包名.类名.方法名(参数) 异常类型)
其中*代表 "任意",..代表 "任意参数 / 子包",举几个实战例子:
表达式 | 含义 | 实战场景 |
---|---|---|
execution(* com.example.demo.controller..(..)) | 匹配 controller 包下所有类的所有方法 | 给所有接口加日志 |
execution(* com.example.demo.service.UserService.*(String)) | 匹配 UserService 里所有参数为 String 的方法 | 只增强用户相关的特定方法 |
execution(public * com.example.demo...(..)) | 匹配 demo 包及其子包下所有 public 方法 | 给整个项目的 public 方法加权限校验 |
小技巧:如果想更灵活(比如给加了@Log注解的方法增强),可以用@annotation表达式:
java
@Pointcut("@annotation(com.example.demo.annotation.Log)")
public void logPointcut() {}
这样只要在方法上加@Log,AOP 就会自动增强,比如:
less
@Log // 加了这个注解,就会被上面的切入点匹配
@GetMapping("/login")
public String login(String username) { ... }
六、终极实战:用 AOP 实现 "全自动日志功能"
咱来写个实用的日志功能,需求是:记录每个接口的「请求 URL、请求方法、参数、返回值、耗时」,还要存到数据库(这里用打印日志代替,实际项目改存库就行)。
1. 明确 2 个关键问题
- 增强谁:所有 Controller 接口(用execution表达式匹配)
- 怎么增强:用环绕通知(因为要统计耗时,还要拿入参和返回值)
2. 完整代码实现
java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
@Aspect
@Component
public class LogAspect {
// 切入点:匹配所有Controller接口
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
public void logPointcut() {}
// 环绕通知:核心逻辑
@Around("logPointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 记录开始时间(统计耗时用)
long startTime = System.currentTimeMillis();
// 2. 获取请求信息(URL、请求方法)
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String requestUrl = request.getRequestURL().toString(); // 请求URL
String httpMethod = request.getMethod(); // 请求方法(GET/POST)
// 3. 获取目标方法的参数和方法名
Object[] args = joinPoint.getArgs(); // 方法参数
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod(); // 目标方法
String methodName = method.getDeclaringClass().getName() + "." + method.getName(); // 类名.方法名
// 4. 执行目标方法(就是原本的Controller方法)
Object result = joinPoint.proceed(); // 这行必须有,否则目标方法不执行!
// 5. 记录结束时间,计算耗时
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
// 6. 打印日志(实际项目里可以存到数据库,比如用MyBatis插入log表)
System.out.println("===== 接口日志 =====");
System.out.println("请求URL:" + requestUrl);
System.out.println("请求方法:" + httpMethod);
System.out.println("目标方法:" + methodName);
System.out.println("请求参数:" + args[0]); // 这里简化,实际要遍历args
System.out.println("返回值:" + result);
System.out.println("耗时:" + costTime + "ms");
System.out.println("===================");
// 返回目标方法的返回值(否则接口调用者拿不到返回值)
return result;
}
}
3. 测试效果
访问http://localhost:8080/user/login?username=zhangsan,控制台会输出:
markdown
===== 接口日志 =====
请求URL:http://localhost:8080/user/login
请求方法:GET
目标方法:com.example.demo.controller.UserController.login
请求参数:zhangsan
返回值:zhangsan登录成功!
耗时:5ms
===================
完美!以后不管加多少接口,都不用再写日志代码,AOP 会自动帮你记录 ------ 这就是 "一次编写,处处生效" 的快乐~
七、总结:AOP 到底帮我们解决了什么?
其实 AOP 的核心就是 "分离关注点":让业务代码只关心 "业务逻辑"(比如登录、下单),让增强代码只关心 "通用功能"(比如日志、事务)。
想象一下:没有 AOP 时,你写 100 个接口要加 100 遍日志代码,改日志格式时要改 100 个地方;有了 AOP,写 1 次切面,改 1 次配置,所有接口都能用上 ------ 这不就是后端开发者梦寐以求的 "偷懒自由" 吗?
最后留个小问题:你们在项目里用 AOP 踩过哪些坑?比如环绕通知忘了写joinPoint.proceed()导致方法不执行?欢迎在评论区分享,咱们一起避坑~