在软件开发中,我们经常会遇到一些横切性问题 ------ 比如日志打印、事务管理、权限校验、性能监控等,这些功能不直接对应业务逻辑,但又需要在多个业务方法中重复实现。如果直接在业务代码中嵌入这些逻辑,会导致代码冗余、耦合度高,后续维护困难。而 AOP(Aspect-Oriented Programming,面向切面编程) 正是解决这类问题的核心技术,它通过 "分离横切逻辑与业务逻辑",让代码更简洁、更易于维护。本文将从 AOP 的核心概念出发,逐步拆解其工作原理,再通过 Spring Boot 实战演示 AOP 的实际应用,帮你彻底掌握这一核心技术。
一、AOP 核心价值:为什么需要面向切面编程?
先看一个典型的业务场景:我们有一个员工管理系统,包含新增员工、删除员工、查询员工三个业务方法,现在需要为每个方法添加日志打印 (记录方法入参、出参、执行时间)和权限校验(验证用户是否有操作权限)功能。
1. 传统实现方式:代码冗余且耦合
如果不使用 AOP,我们需要在每个业务方法中手动嵌入日志和权限校验逻辑:
java
@Service
public class EmpService {
// 日志组件
private static final Logger logger = LoggerFactory.getLogger(EmpService.class);
public void addEmp(Emp emp) {
// 1. 权限校验(横切逻辑)
if (!hasPermission("EMP_ADD")) {
throw new RuntimeException("无新增员工权限");
}
// 2. 日志打印(横切逻辑)
long startTime = System.currentTimeMillis();
logger.info("addEmp方法入参:{}", emp);
// 3. 核心业务逻辑
System.out.println("新增员工:" + emp.getName());
// 2. 日志打印(横切逻辑)
long endTime = System.currentTimeMillis();
logger.info("addEmp方法执行完成,耗时:{}ms", endTime - startTime);
}
public void deleteEmp(Long empId) {
// 1. 权限校验(重复代码)
if (!hasPermission("EMP_DELETE")) {
throw new RuntimeException("无删除员工权限");
}
// 2. 日志打印(重复代码)
long startTime = System.currentTimeMillis();
logger.info("deleteEmp方法入参:{}", empId);
// 3. 核心业务逻辑
System.out.println("删除员工:" + empId);
// 2. 日志打印(重复代码)
long endTime = System.currentTimeMillis();
logger.info("deleteEmp方法执行完成,耗时:{}ms", endTime - startTime);
}
// 查询员工方法(同样嵌入重复的横切逻辑,此处省略)
public Emp getEmpById(Long empId) { /* ... */ }
// 权限校验工具方法
private boolean hasPermission(String permission) {
// 模拟权限校验逻辑
return "admin".equals(UserContext.getLoginUser());
}
}
这种实现方式的问题显而易见:
- 代码冗余:日志打印、权限校验的逻辑在每个业务方法中重复编写,增加了代码量;
- 耦合度高:横切逻辑与业务逻辑紧密绑定,后续修改日志格式或权限规则时,需要修改所有业务方法;
- 维护困难:横切逻辑分散在多个方法中,排查问题和迭代升级时效率低下。
2. AOP 实现方式:分离横切逻辑与业务逻辑
使用 AOP 后,我们可以将日志打印、权限校验等横切逻辑抽离出来,单独封装为 "切面",然后通过配置指定这些切面需要作用在哪些业务方法上,业务方法中只需保留核心业务逻辑:
java
// 1. 业务类:仅保留核心业务逻辑,简洁清晰
@Service
public class EmpService {
public void addEmp(Emp emp) {
System.out.println("新增员工:" + emp.getName());
}
public void deleteEmp(Long empId) {
System.out.println("删除员工:" + empId);
}
public Emp getEmpById(Long empId) { /* ... */ }
}
// 2. 切面类:封装日志打印横切逻辑(单独维护,可复用)
@Aspect
@Component
public class LogAspect { /* 日志逻辑封装 */ }
// 3. 切面类:封装权限校验横切逻辑(单独维护,可复用)
@Aspect
@Component
public class PermissionAspect { /* 权限逻辑封装 */ }
AOP 的核心价值就在于:分离横切逻辑与核心业务逻辑,实现横切逻辑的复用,降低代码耦合,提升开发效率和可维护性。
二、AOP 核心概念:读懂切面编程的术语
AOP 有一套专属的术语,理解这些术语是掌握 AOP 的基础,我们用 "业务方法 + 日志切面" 的场景来逐一解释:
| 术语 | 英文 | 核心定义 | 场景示例 |
|---|---|---|---|
| 切面 | Aspect | 封装横切逻辑的类(如日志切面、权限切面),是 AOP 的核心载体 | LogAspect(日志切面类,封装日志打印逻辑) |
| 连接点 | Join Point | 程序执行过程中的所有可拦截点(如方法执行、异常抛出、字段修改),在 Spring AOP 中仅支持方法级连接点 | EmpService的addEmp、deleteEmp、getEmpById方法 |
| 切入点 | Pointcut | 从所有连接点中筛选出的、需要执行切面逻辑的目标点(通过表达式指定) | 仅匹配EmpService中所有以emp结尾的方法,或所有加了@Log注解的方法 |
| 通知 | Advice | 切面中具体的横切逻辑实现,指定了 "在切入点的什么时机执行什么逻辑" | 日志切面中的 "方法执行前打印入参"、"方法执行后打印出参"、"方法执行异常打印堆栈" |
| 目标对象 | Target Object | 被切面拦截的对象(即业务对象) | EmpService的实例对象 |
| 代理对象 | Proxy Object | Spring AOP 通过动态代理为目标对象创建的包装对象,切面逻辑最终通过代理对象执行 | Spring 为EmpService创建的 JDK 动态代理 / CGLIB 动态代理对象 |
| 织入 | Weaving | 将切面逻辑嵌入到目标对象的业务逻辑中,形成代理对象的过程 | Spring 容器启动时,自动将LogAspect的逻辑织入到EmpService的目标方法中 |
关键补充:通知(Advice)的 5 种类型
通知是切面的核心逻辑,Spring AOP 支持 5 种通知类型,对应切入点的不同执行时机:
- 前置通知(@Before) :在目标方法执行前执行(如权限校验、方法入参日志);
- 后置通知(@After) :在目标方法执行后执行(无论方法成功还是异常,都会执行,如资源释放);
- 返回通知(@AfterReturning) :在目标方法正常执行完成后执行(如打印方法出参、执行结果日志);
- 异常通知(@AfterThrowing) :在目标方法抛出异常后执行(如打印异常堆栈、异常告警);
- 环绕通知(@Around) :包裹目标方法的执行,可在方法执行前后、异常时执行自定义逻辑(功能最强大,可控制目标方法是否执行)。
代码实现
java
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class MyAspect1 {
@Before("execution(* com.tgt.service.*.*(..))")
public void before(){
log.info("【前置通知】");
}
@AfterReturning("execution(* com.tgt.service.*.*(..))")
public void afterReturning(){
log.info("【运行后通知】");
}
@AfterThrowing("execution(* com.tgt.service.*.*(..))")
public void afterThrowing(){
log.info("【异常后通知】");
}
@After("execution(* com.tgt.service.*.*(..))")
public void after(){
log.info("【后置通知】");
}
@Around("execution(* com.tgt.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp)throws Throwable{
try {
log.info("【环绕通知,前置通知】");
//2.调用目标方法
Object result = pjp.proceed();
log.info("【环绕通知,运行后通知】");
return result;
} catch (Throwable t) {
log.info("【环绕通知,异常后通知】");
throw t;
} finally {
log.info("【环绕通知,后置通知】");
}
}
}
测试
java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class DeptServiceTest {
@Autowired
private DeptService deptService;
@Test
public void test(){
deptService.list();
//AOP底层实现原理:动态代理,springboot默认使用cglib
System.out.println(deptService.getClass().getName());//默认是cglib动态代理
}
}
结果


三、Spring AOP 核心原理:动态代理
Spring AOP 的底层实现依赖动态代理技术,它会为目标对象创建一个代理对象,所有对目标对象的调用都会先经过代理对象,切面逻辑正是在代理对象中嵌入的。Spring AOP 支持两种动态代理方式:
1. JDK 动态代理
- 适用场景:目标对象实现了至少一个接口;
- 实现原理 :基于 Java 反射的
java.lang.reflect.Proxy类和InvocationHandler接口,动态生成接口的实现类(代理类); - 特点:无需额外依赖,仅代理接口方法,不支持代理类的非接口方法。
2. CGLIB 动态代理
- 适用场景:目标对象未实现任何接口(默认选择);
- 实现原理:基于 ASM 字节码框架,动态生成目标对象的子类(代理类),通过重写目标方法实现代理;
- 特点:无需目标对象实现接口,支持代理所有非 final 方法(final 方法无法被重写,无法代理)。
3. Spring AOP 代理选择规则
- 如果目标对象实现了接口,默认使用JDK 动态代理;
- 如果目标对象未实现接口,使用CGLIB 动态代理;
- 可通过配置(
spring.aop.proxy-target-class=true)强制使用 CGLIB 动态代理。
四、AOP 切入点表达式详解
切入点表达式是 AOP 的核心配置,用于精准匹配目标方法,Spring AOP 支持两种表达式风格:AspectJ 表达式 (常用)和自定义注解表达式。
1. 常用 AspectJ 表达式(execution)
execution是最常用的切入点表达式,用于匹配方法执行,语法格式:
plaintext
execution(访问修饰符? 返回值类型 包名.类名?方法名(参数列表) 异常类型?)
-
?表示可选参数; -
支持通配符:
*:匹配任意单个元素(如任意返回值、任意类、任意方法);..:匹配任意多个元素(如任意包层级、任意参数列表);+:匹配类及其子类。
常用示例
| 表达式 | 含义 |
|---|---|
execution(* com.example.aop.service.*.*(..)) |
匹配com.example.aop.service包下所有类的所有方法 |
execution(* com.example.aop.service.EmpService.*(..)) |
匹配EmpService类的所有方法 |
execution(public * com.example.aop.service.*.add*(..)) |
匹配service包下所有类的以add开头的公有方法 |
execution(* com.example.aop.service..*.*(..)) |
匹配service包及其子包下所有类的所有方法 |
execution(* com.example.aop.service.EmpService.getEmpById(Long)) |
匹配EmpService的getEmpById方法(参数为 Long 类型) |
2. 自定义注解表达式(@annotation)
如果需要更灵活地指定目标方法(如仅对加了@Log注解的方法生效),可使用@annotation表达式:
(1)创建自定义注解
java
import java.lang.annotation.*;
@Target(ElementType.METHOD) // 仅作用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时可见
@Documented
public @interface Log {
// 注解属性:日志描述
String value() default "";
}
(2)在切面中使用 @annotation 表达式
java
@Aspect
@Component
public class CustomLogAspect {
// 切入点:匹配所有加了@Log注解的方法
@Pointcut("@annotation(com.example.aop.annotation.Log)")
public void customLogPointcut() {}
// 前置通知
@Before("customLogPointcut() && @annotation(log)")
public void beforeLog(JoinPoint joinPoint, Log log) {
String methodName = joinPoint.getSignature().getName();
log.info("【自定义日志注解】目标方法:{},日志描述:{}", methodName, log.value());
}
}
(3)在业务方法上添加注解
java
@Service
public class EmpService {
@Log("新增员工操作")
public void addEmp(Emp emp) { /* ... */ }
}
五、Spring Boot AOP 实战:完整示例
接下来我们通过 Spring Boot 搭建完整的 AOP 示例,实现 "日志打印" 和 "权限校验" 两个切面功能,让你直观感受 AOP 的使用方式。
1. 前置准备
(1)引入核心依赖
在pom.xml中引入 Spring Boot Web 和 AOP 的依赖:
xml
<!-- Spring Boot Web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot AOP 依赖(自动引入AspectJ相关依赖) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Lombok(简化日志编写,可选) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
(2)创建核心业务类
- 编写LogOperator注解
java
package com.tgt.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 LogOperator {
}
- 员工实体类
Emp.java
java
import lombok.Data;
@Data
public class Emp {
private Long id;
private String name;
private Integer age;
private String deptName;
}
- 操作日志实体类
OperateLog.java
java
package com.tgt.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
private Integer id; //ID
private Integer operateEmpId; //操作人ID
private LocalDateTime operateTime; //操作时间
private String className; //操作类名
private String methodName; //操作方法名
private String methodParams; //操作方法参数
private String returnValue; //操作方法返回值
private Long costTime; //操作耗时
//页面展示字段
private String operateEmpName; //操作人姓名
}
- 员工业务类
EmpService.java
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class EmpService {
// 新增员工
public void addEmp(Emp emp) {
if (emp == null) {
throw new RuntimeException("员工信息不能为空");
}
log.info("核心业务:新增员工成功,员工姓名:{}", emp.getName());
}
// 删除员工
public void deleteEmp(Long empId) {
if (empId == null || empId <= 0) {
throw new RuntimeException("员工ID不合法");
}
log.info("核心业务:删除员工成功,员工ID:{}", empId);
}
// 查询员工
public Emp getEmpById(Long empId) {
if (empId == null || empId <= 0) {
throw new RuntimeException("员工ID不合法");
}
Emp emp = new Emp();
emp.setId(empId);
emp.setName("张三");
emp.setAge(28);
emp.setDeptName("研发部");
log.info("核心业务:查询员工成功,员工ID:{}", empId);
return emp;
}
}
(3)创建操作日志表
sql
-- 操作日志表
create table operate_log(
id int unsigned primary key auto_increment comment 'ID',
operate_emp_id int unsigned comment '操作人ID',
operate_time datetime comment '操作时间',
class_name varchar(100) comment '操作的类名',
method_name varchar(100) comment '操作的方法名',
method_params varchar(1000) comment '方法参数',
return_value varchar(2000) comment '返回值',
cost_time int comment '方法执行耗时, 单位:ms'
) comment '操作日志表';
(4)编写OperateLogMapper类
java
import com.tgt.pojo.OperateLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OperateLogMapper {
//插入日志数据
@Insert("insert into operate_log (operate_emp_id, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
"values (#{operateEmpId}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")
public void insert(OperateLog log);
}
2. 实战 1:日志切面(LogAspect)
创建日志切面类,实现 "打印方法入参、出参、执行时间、异常信息" 的功能,使用 5 种通知类型演示:
java
@Aspect
@Slf4j
@Component
public class LogAspect {
@Autowired
private OperateLogMapper operateLogMapper;
@Around("execution(* com.tgt.controller.*.*(..)) && @annotation(com.tgt.anno.LogOperator)")
public Object around(ProceedingJoinPoint pjp)throws Throwable {
//1.定义并封装OperateLog对象
OperateLog operateLog = new OperateLog();
//operate_emp_id 登录人id
// 获取token
String token = httpServletRequest.getHeader("token");
// 解析令牌得到empId
Claims claims = JwtUtils.parseJWT(token);
Integer empId = Integer.valueOf(claims.get("empId").toString());
// 封装登录人id
operateLog.setOperateEmpId(empId);
//operate_time 操作时间,系统当前时间
operateLog.setOperateTime(LocalDateTime.now());
//class_name 类全名
operateLog.setClassName(pjp.getTarget().getClass().getName());
//method_name 方法名
operateLog.setMethodName(pjp.getSignature().getName());
//method_params 方法参数
Object[] args = pjp.getArgs();
operateLog.setMethodParams(Arrays.toString(args));
//定义封装开始毫秒数
Long start = System.currentTimeMillis();
//2.执行目标方法并获取返回值
Object result = pjp.proceed();
//定义封装结束毫秒数
Long end = System.currentTimeMillis();
//return_value 返回值
operateLog.setReturnValue(result.toString());
//cost_time 耗时统计
operateLog.setCostTime(end - start);
//调用operateLogMapper插入日志
operateLogMapper.insert(operateLog);
//2.返回数据
return result;
}
}
3. 实战 3:测试接口(EmpController)
创建 Controller 测试接口,调用业务方法,验证 AOP 切面是否生效:
java
@RestController
@RequestMapping("/emp")
public class EmpController {
@Autowired
private EmpService empService;
@LogOperator
@PostMapping("/add")
public String addEmp(@RequestBody Emp emp) {
empService.addEmp(emp);
return "新增员工成功";
}
@LogOperator
@DeleteMapping("/delete/{empId}")
public String deleteEmp(@PathVariable Long empId) {
empService.deleteEmp(empId);
return "删除员工成功";
}
@LogOperator
@GetMapping("/get/{empId}")
public Emp getEmpById(@PathVariable Long empId) {
return empService.getEmpById(empId);
}
}
4. 运行测试
启动 Spring Boot 项目,通过 Postman 或浏览器调用接口(如POST http://localhost:8080/emp/add),查看控制台日志:
- 日志切面会打印 "前置通知"(入参、请求信息)→ "环绕通知"(执行耗时)→ "返回通知"(出参)→ "后置通知";
- 权限切面会先校验当前用户权限,校验通过后才会执行业务方法;
- 若传入非法参数(如
empId=-1),异常通知会打印异常堆栈信息。
六、AOP 最佳实践与注意事项
1. 核心最佳实践
(1)合理设计切面粒度
- 一个切面对应一个单一职责(如日志切面只负责日志打印,权限切面只负责权限校验),避免 "大而全" 的切面;
- 通用横切逻辑(如日志、事务、权限)封装为公共切面,提高复用性。
(2)精准匹配切入点
- 尽量缩小切入点的匹配范围(如指定具体类 / 方法,而非所有包),减少不必要的性能损耗;
- 复杂场景优先使用自定义注解(
@annotation),更灵活、可读性更高。
(3)优先使用环绕通知实现复杂逻辑
- 若需要控制目标方法执行(如超时控制、重试机制),优先使用
@Around环绕通知,功能更强大; - 简单逻辑(如日志打印、权限校验)可使用前置 / 返回 / 异常通知,代码更简洁。
(4)避免切面嵌套过多
- 过多的切面嵌套会增加系统复杂度和性能损耗,建议控制切面数量,避免不必要的切面叠加。
2. 注意事项
(1)Spring AOP 仅支持方法级连接点
- Spring AOP 不支持字段级、构造方法级的拦截,若需要更细粒度的 AOP 功能,可直接使用 AspectJ 框架;
- 内部方法调用无法触发切面(如
EmpService的a方法调用b方法,b方法的切面不会生效,因为内部调用不经过代理对象)。
(2)异常处理
- 在切面中捕获异常时,需注意是否会吞掉业务异常(建议仅记录异常,不拦截异常,让业务层处理);
@AfterThrowing仅能捕获目标方法抛出的未被捕获的异常。
(3)性能考量
- 动态代理会带来轻微的性能损耗,在高并发场景下,需合理设计切入点,避免对高频方法进行不必要的切面拦截;
- 避免在切面中执行耗时操作(如远程调用、复杂数据库查询)。
七、总结与拓展
本文全面讲解了 AOP 的核心概念、工作原理,并通过 Spring Boot 实战演示了日志切面和权限切面的实现,核心要点总结:
- AOP 核心价值:分离横切逻辑与业务逻辑,解决代码冗余、耦合度高的问题,提升代码可维护性;
- 核心概念:切面(Aspect)、切入点(Pointcut)、通知(Advice)是 AOP 的三大核心,通知分为 5 种类型;
- 底层原理:Spring AOP 基于动态代理(JDK 动态代理 + CGLIB 动态代理)实现;
- 实战要点 :通过
@Aspect标记切面类,@Pointcut定义切入点,@Before/@After等注解定义通知类型; - 最佳实践:单一职责、精准匹配切入点、避免切面嵌套过多。
拓展方向:
- 事务管理 :Spring 的声明式事务(
@Transactional)正是基于 AOP 实现的,底层通过事务切面完成事务的开启、提交、回滚; - 缓存实现 :Spring Cache(
@Cacheable)也是基于 AOP 实现的,通过切面拦截方法,实现缓存的读取和写入; - 分布式追踪:如 SkyWalking、Zipkin 等分布式追踪工具,通过 AOP 切面收集方法调用链路信息;
- AspectJ 框架:若需要更强大的 AOP 功能(如字段拦截、构造方法拦截),可直接使用 AspectJ 框架(编译期 / 加载期织入)。
AOP 是 Spring 框架的核心特性之一,也是企业级开发中必备的技术之一。掌握 AOP 不仅能提升代码质量,还能让你更深入地理解 Spring 框架的底层设计思想,为后续的高级开发打下坚实基础。