AOP(Aspect-Oriented Programming,面向切面编程)是 Spring 核心特性之一,其核心思想是分离 “核心业务逻辑” 与 “横切逻辑” (如日志、事务、权限、监控等),通过 “织入” 机制将横切逻辑动态融入核心业务,实现代码复用、降低耦合度。
一、为什么需要 AOP?
在传统编程中,横切逻辑(如每个方法的日志记录)会嵌入到核心业务代码中,导致以下问题:
- 代码冗余:相同的横切逻辑重复出现在多个业务方法中;
- 耦合度高:横切逻辑与核心业务强绑定,修改横切逻辑需改动所有相关业务代码;
- 维护困难:核心业务代码被横切逻辑污染,可读性差。
AOP 通过 “切面” 封装横切逻辑,让核心业务代码只关注自身逻辑,横切逻辑集中管理,解决上述问题。
二、AOP 核心术语(必须掌握)
用 “日志切面” 举例,通俗解释关键概念:
| 术语 | 定义(通俗理解) |
|---|---|
| 核心业务逻辑(Target) | 被增强的目标对象 / 方法(如用户服务的 addUser()、deleteUser() 方法) |
| 横切逻辑(Advice) | 切面的具体逻辑(如日志记录的 “打印请求参数”“打印执行耗时”) |
| 切面(Aspect) | 横切逻辑的封装类(如 LogAspect 类,包含日志逻辑 + 切入点规则) |
| 连接点(JoinPoint) | 程序中 “可能被拦截” 的所有点(Spring AOP 仅支持方法级连接点,如所有方法的执行、异常抛出) |
| 切入点(Pointcut) | 从所有连接点中 “筛选出需要增强的点”(如 “所有 com.example.service 包下的方法”) |
| 织入(Weaving) | 将切面的横切逻辑动态融入目标对象的过程(Spring AOP 是运行时织入) |
| 代理对象(Proxy) | 织入后生成的对象(Spring 会为目标对象创建代理,开发者实际调用的是代理对象方法) |
| 通知类型(Advice Type) | 横切逻辑的执行时机(如方法执行前、执行后、异常时) |
关键区别:
- 连接点 = 所有可能被增强的 “候选点”(如 100 个方法);
- 切入点 = 被选中的连接点(如 10 个符合规则的方法)。
三、Spring AOP 核心原理:动态代理
Spring AOP 的底层实现依赖动态代理,即运行时为目标对象创建代理对象,横切逻辑通过代理对象执行。Spring 支持两种动态代理方式,自动切换:
1. JDK 动态代理(默认,优先使用)
-
适用场景:目标对象实现了接口(如
UserService实现IUserService); -
实现原理:基于 JDK 自带的
java.lang.reflect.Proxy类和InvocationHandler接口; -
特点:
- 无需额外依赖,JDK 原生支持;
- 代理类是目标接口的 “实现类”(运行时动态生成);
- 只能代理接口中定义的方法(目标类中未实现接口的方法无法代理)。
简单示例(JDK 动态代理底层逻辑) :
// 1. 目标接口
public interface IUserService {
void addUser();
}
// 2. 目标实现类(核心业务)
public class UserService implements IUserService {
@Override
public void addUser() {
System.out.println("核心业务:添加用户");
}
}
// 3. 横切逻辑(日志)+ 代理处理器
public class LogInvocationHandler implements InvocationHandler {
private Object target; // 目标对象(被代理的对象)
public LogInvocationHandler(Object target) {
this.target = target;
}
// 代理对象的所有方法调用都会触发此方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 横切逻辑:方法执行前
System.out.println("日志:方法 " + method.getName() + " 开始执行");
// 执行核心业务(目标对象的方法)
Object result = method.invoke(target, args);
// 横切逻辑:方法执行后
System.out.println("日志:方法 " + method.getName() + " 执行结束");
return result;
}
}
// 4. 生成代理对象并调用
public class Test {
public static void main(String[] args) {
// 目标对象
IUserService target = new UserService();
// 代理处理器(绑定目标对象+横切逻辑)
InvocationHandler handler = new LogInvocationHandler(target);
// 生成代理对象(JDK 动态代理)
IUserService proxy = (IUserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
handler
);
// 调用代理对象方法(自动触发横切逻辑+核心业务)
proxy.addUser();
}
}
输出结果:
日志:方法 addUser 开始执行
核心业务:添加用户
日志:方法 addUser 执行结束
2. CGLIB 动态代理( fallback 方案)
-
适用场景:目标对象未实现接口(如
UserService无接口); -
实现原理:基于第三方库
cglib(Spring 已内置),通过继承目标类生成子类作为代理对象; -
特点:
- 无需目标类实现接口,适用范围更广;
- 代理类是目标类的 “子类”,需重写目标方法(因此目标方法不能是
final,否则无法重写); - 性能略优于 JDK 动态代理(因为是直接操作字节码)。
Spring 代理选择规则:
- 若目标对象实现接口 → 优先使用 JDK 动态代理;
- 若目标对象无接口 → 自动切换为 CGLIB 动态代理;
- 可通过配置强制使用 CGLIB(如
@EnableAspectJAutoProxy(proxyTargetClass = true))。
四、Spring AOP 核心组件及关系
Spring AOP 遵循 AOP 联盟规范,核心组件通过 “组合” 实现切面功能:
| 组件 | 作用 |
|---|---|
Aspect(切面) | 封装横切逻辑(Advice)和切入点(Pointcut)的类(如 @Aspect 注解类) |
Pointcut(切入点) | 定义 “哪些方法需要被增强”(通过表达式匹配,如 execution(* com.example.service.*.*(..))) |
Advice(通知) | 定义 “横切逻辑的执行时机”(如前置、后置、环绕通知) |
Advisor(通知器) | Spring AOP 内部核心组件,是 Pointcut + Advice 的组合(@Aspect 最终会被解析为 Advisor) |
Weaver(织入器) | 执行织入的组件(Spring 动态代理的底层实现) |
核心关系:Aspect = Pointcut + Advice → 解析为 Advisor → 由 Weaver 织入目标对象 → 生成 Proxy 代理对象。
五、Spring AOP 通知类型(执行时机)
Spring 支持 5 种通知类型,覆盖方法执行的全生命周期:
| 通知类型 | 注解 | 执行时机 | 特点 |
|---|---|---|---|
| 前置通知 | @Before | 目标方法执行之前 | 无返回值,不能阻止目标方法执行(除非抛异常) |
| 后置返回通知 | @AfterReturning | 目标方法正常执行完成后(无异常) | 可获取目标方法的返回值 |
| 后置最终通知 | @After | 目标方法执行之后(无论是否异常) | 类似 finally,必定执行 |
| 异常通知 | @AfterThrowing | 目标方法抛出异常后 | 可获取异常信息 |
| 环绕通知 | @Around | 目标方法执行前后(全程包裹) | 功能最强,可控制目标方法是否执行、修改参数 / 返回值 |
环绕通知示例(最常用) :
@Aspect
@Component
public class LogAspect {
// 切入点:匹配 com.example.service 包下所有方法
@Pointcut("execution(* com.example.service.*.*(..))")
public void servicePointcut() {}
// 环绕通知
@Around("servicePointcut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 前置逻辑:记录请求参数
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("环绕通知:方法 " + methodName + " 入参:" + Arrays.toString(args));
long startTime = System.currentTimeMillis();
Object result = null;
try {
// 2. 执行目标方法(必须调用,否则目标方法不执行)
result = joinPoint.proceed(args);
// 3. 后置返回逻辑(无异常时执行)
System.out.println("环绕通知:方法 " + methodName + " 返回值:" + result);
} catch (Exception e) {
// 4. 异常处理逻辑
System.out.println("环绕通知:方法 " + methodName + " 抛出异常:" + e.getMessage());
throw e; // 抛出异常,让后续异常通知处理
} finally {
// 5. 最终逻辑:记录执行耗时
long cost = System.currentTimeMillis() - startTime;
System.out.println("环绕通知:方法 " + methodName + " 执行耗时:" + cost + "ms");
}
return result;
}
}
六、Spring AOP 切入点表达式(Pointcut Expression)
切入点表达式用于 “筛选需要增强的方法”,Spring 支持多种表达式,最常用的是 execution:
1. 核心语法(execution)
execution(修饰符? 返回值 包名.类名.方法名(参数) 异常? )
?表示可选(修饰符、异常可省略);*表示任意(任意返回值、任意包、任意类、任意方法);..表示任意子包 / 任意参数(如com.example..*表示com.example下所有子包)。
2. 常用示例
| 表达式 | 含义 |
|---|---|
execution(* com.example.service.*.*(..)) | com.example.service 包下所有类的所有方法 |
execution(public * com.example.service.UserService.*(..)) | UserService 类的所有公共方法 |
execution(* com.example.service..*.*(String, ..)) | 子包下所有方法,第一个参数为 String 类型 |
execution(* com.example.service.IUserService.add*(..)) | 接口中所有以 add 开头的方法 |
3. 其他表达式(辅助匹配)
@annotation(注解全类名):匹配标注了指定注解的方法(如@Transactional);within(包名.类名):匹配指定类 / 包下的所有方法;this(接口类型):匹配实现了指定接口的目标对象的所有方法。
七、Spring AOP 执行流程(完整链路)
以 @Aspect 注解方式为例,整体执行流程如下:
-
容器启动阶段:
- Spring 扫描带有
@Component和@Aspect的切面类(如LogAspect); - 解析切面类中的
@Pointcut切入点表达式,确定需要增强的目标方法; - 解析
@Before/@Around等通知,将 “切入点 + 通知” 封装为Advisor对象; - 扫描业务 Bean(如
UserService),判断其方法是否匹配Advisor的切入点。
- Spring 扫描带有
-
代理对象创建阶段:
- 若业务 Bean 的方法匹配切入点,Spring 为该 Bean 创建动态代理对象(JDK/CGLIB);
- 代理对象持有目标对象(
target)和Advisor中的通知逻辑。
-
运行时调用阶段:
- 开发者调用代理对象的方法(而非目标对象);
- 代理对象触发通知链执行(按 “环绕前置 → 前置 → 目标方法 → 环绕后置 → 后置返回 / 异常 → 后置最终” 顺序);
- 执行完成后,代理对象返回结果给调用者。
八、Spring AOP 注意事项与常见问题
1. 与 AspectJ 的区别
- Spring AOP:轻量级,基于动态代理(运行时织入),仅支持方法级连接点,依赖 Spring 容器;
- AspectJ:完整的 AOP 框架,支持编译期 / 类加载期织入,可拦截字段访问、构造方法、静态方法等,功能更强(Spring 可整合 AspectJ 实现更复杂的 AOP 需求)。
2. 自调用问题(代理失效)
若目标对象的 A 方法调用自身的 B 方法,且 B 方法被切面增强,则通知不会执行(因为是 “目标对象内部调用”,未经过代理对象)。解决方案:
- 注入自身代理对象(
@Autowired private UserService proxy;,需开启exposeProxy = true); - 重构代码,避免自调用。
3. 代理选择与配置
- 强制使用 CGLIB:在 Spring Boot 中添加
@EnableAspectJAutoProxy(proxyTargetClass = true); - JDK 动态代理只能代理接口方法,若目标类有非接口方法,需使用 CGLIB。
4. 通知执行顺序
- 同一切面中:
@Around(前置)→@Before→ 目标方法 →@Around(后置)→@AfterReturning/@AfterThrowing→@After; - 多个切面:通过
@Order(数字)注解指定切面优先级(数字越小,优先级越高)。
九、spring的Aop几种实现方式
Spring AOP(面向切面编程)的核心是 通过 “切面” 封装横切逻辑(如日志、事务、权限) ,并动态织入目标方法,不侵入业务代码。其实现方式主要分为 4 种核心类型,本质差异在于 织入时机(编译期 / 类加载期 / 运行期) 和 技术底层(字节码操作 / 动态代理)
一、核心实现方式(按织入时机从早到晚排序)
1. 基于编译期织入:AspectJ 编译期织入(原生 AspectJ)
这是 最早期、织入时机最早 的 AOP 实现,完全依赖 AspectJ 框架(非 Spring 原生,需额外依赖),织入发生在 Java 代码编译为字节码的阶段。
实现原理
- 编写 AspectJ 语法的切面类(.aj 文件),通过 AspectJ 提供的编译器(
ajc)替代 javac 编译源码; - 编译时,
ajc会直接修改目标类的字节码,将切面逻辑(如前置 / 后置通知)直接嵌入目标方法中; - 运行时无需任何代理,目标类本身已包含切面逻辑,性能最优(无运行时开销)。
关键特点
- 依赖:需引入 AspectJ 核心依赖(
aspectjrt.jar、aspectjtools.jar); - 语法:需使用 AspectJ 专属注解(如
@AspectJ注解但需配合 ajc 编译,或.aj原生语法); - 优点:性能最好(编译期织入,无运行时代理开销);
- 缺点:侵入性强(需用 AspectJ 编译器,脱离 IDE 时配置复杂),Spring 项目中极少单独使用。
简单示例(.aj 文件切面)
// 切面类(.aj 文件)
public aspect LogAspect {
// 切入点:匹配 com.example.service 包下所有类的所有方法
pointcut serviceMethod() : execution(* com.example.service.*.*(..));
// 前置通知
before() : serviceMethod() {
System.out.println("【编译期织入】方法执行前日志");
}
}
2. 基于类加载期织入:AspectJ 加载时织入(LTW)
同样依赖 AspectJ 框架,但织入时机推迟到 目标类被类加载器加载到 JVM 的阶段,无需修改编译流程(用 javac 编译即可)。
实现原理
- 编写普通的 AspectJ 注解切面(如
@Aspect标注); - 启动 JVM 时,通过
-javaagent参数指定 AspectJ 的织入代理(aspectjweaver.jar); - 类加载器加载目标类时,织入代理会拦截类加载过程,动态修改目标类的字节码,植入切面逻辑。
关键特点
- 依赖:需引入
aspectjweaver.jar,并配置 JVM 代理参数; - 优点:无编译期侵入(用 javac 编译),性能接近编译期织入;
- 缺点:需配置 JVM 启动参数(如
-javaagent:/path/to/aspectjweaver.jar),部署复杂度高,Spring 项目中较少使用(除非追求极致性能)。
启动参数示例
java -javaagent:lib/aspectjweaver-1.9.20.jar -jar your-spring-app.jar
3. 基于运行期动态代理:JDK 动态代理(Spring 原生默认)
这是 Spring AOP 最核心、最常用 的实现方式,织入发生在 应用运行时,基于 JDK 原生的 java.lang.reflect.Proxy 接口实现,无需额外依赖。
实现原理
- 核心前提:目标类 必须实现接口(JDK 动态代理仅能代理接口方法);
- 代理生成:Spring 容器启动时,对实现接口的目标类,通过
Proxy.newProxyInstance()动态生成 代理类(实现目标类的所有接口); - 切面织入:代理类中嵌入切面逻辑,调用目标方法时,先执行切面的通知(如前置、后置),再反射调用目标类的真实方法。
关键特点
- 依赖:仅依赖 JDK 原生 API,无额外 jar 包;
- 优点:无侵入(目标类无需修改)、配置简单(Spring 自动生成代理);
- 缺点:仅支持接口代理(目标类必须实现接口,否则无法代理);运行时通过反射调用,有轻微性能开销(可忽略,除非高频调用)。
Spring 示例(注解式 AOP)
// 1. 目标接口
public interface UserService {
void addUser();
}
// 2. 目标实现类(必须实现接口)
@Service
public class UserServiceImpl implements UserService {
@Override
public void addUser() {
System.out.println("执行添加用户业务");
}
}
// 3. 切面类(Spring AOP 注解)
@Aspect
@Component
public class LogAspect {
// 切入点:匹配 UserService 接口的所有方法
@Pointcut("execution(* com.example.service.UserService.*(..))")
public void servicePointcut() {}
// 前置通知
@Before("servicePointcut()")
public void beforeAdvice() {
System.out.println("【JDK动态代理】方法执行前日志");
}
}
// 4. 启动类(开启 AOP 自动代理)
@SpringBootApplication
@EnableAspectJAutoProxy // 开启 AOP 支持(Spring Boot 可省略,默认自动开启)
public class AopDemoApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(AopDemoApplication.class, args);
UserService userService = context.getBean(UserService.class);
userService.addUser(); // 执行时会触发切面通知
}
}
4. 基于运行期动态代理:CGLIB 动态代理(Spring 补充实现)
当目标类 没有实现接口 时,Spring AOP 会自动切换为 CGLIB 代理(基于字节码操作框架 cglib-nodep.jar),织入时机同样是 应用运行时。
实现原理
- 核心前提:目标类无需实现接口(但不能是
final类,方法不能是final方法 ——CGLIB 无法代理 final 元素); - 代理生成:通过 CGLIB 的
Enhancer类动态生成 目标类的子类(继承目标类); - 切面织入:子类中重写目标类的方法,嵌入切面逻辑,调用时先执行切面通知,再通过
super调用父类(目标类)的真实方法。
关键特点
- 依赖:Spring 内置 CGLIB 依赖(Spring 5+ 已集成
cglib-nodep.jar,无需手动引入); - 优点:支持类代理(无需接口),性能比 JDK 动态代理略好(直接调用子类方法,无反射开销);
- 缺点:目标类不能是 final 类,方法不能是 final 方法(否则无法继承重写);生成子类字节码,有轻微类加载开销。
Spring 配置(切换为 CGLIB 代理)
Spring Boot 中默认策略:目标类有接口则用 JDK 代理,无接口则用 CGLIB 代理。若需强制使用 CGLIB 代理(即使有接口),可配置:
// 启动类中开启 AOP 并强制使用 CGLIB
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true) // proxyTargetClass=true 强制使用 CGLIB
public class AopDemoApplication { ... }
示例(目标类无接口)
// 目标类(无接口)
@Service
public class OrderService { // 无接口,Spring 自动用 CGLIB 代理
public void createOrder() {
System.out.println("执行创建订单业务");
}
}
// 切面类(与 JDK 代理共用,无需修改)
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.example.service.OrderService.*(..))")
public void orderPointcut() {}
@Before("orderPointcut()")
public void beforeAdvice() {
System.out.println("【CGLIB代理】方法执行前日志");
}
}
二、4 种实现方式核心对比
| 实现方式 | 织入时机 | 底层技术 | 核心依赖 | 支持目标类要求 | 性能 | 侵入性 | Spring 中常用度 |
|---|---|---|---|---|---|---|---|
| AspectJ 编译期织入 | 编译期 | AspectJ 编译器(ajc) | aspectjrt、aspectjtools | 无(任意类) | 最优 | 高 | 极低 |
| AspectJ 加载时织入(LTW) | 类加载期 | AspectJ 织入代理 | aspectjweaver | 无(任意类) | 接近最优 | 中 | 低 |
| JDK 动态代理 | 运行期 | JDK Proxy + 反射 | JDK 原生 API | 必须实现接口 | 中等(反射) | 无 | 极高 |
| CGLIB 动态代理 | 运行期 | CGLIB(字节码子类生成) | Spring 内置 cglib | 不能是 final 类 / 方法 | 中等偏优 | 无 | 极高 |
三、Spring AOP 实现方式的选择逻辑
Spring 容器启动时,会自动选择代理方式,核心逻辑:
-
若开启
@EnableAspectJAutoProxy(proxyTargetClass = true):强制使用 CGLIB 代理; -
若未开启(默认
proxyTargetClass = false):- 目标类实现接口 → 优先用 JDK 动态代理;
- 目标类未实现接口 → 自动用 CGLIB 代理;
-
若引入 AspectJ 依赖并配置 LTW / 编译期织入:优先使用 AspectJ 织入(需手动配置)。
四、关键总结
- 日常开发首选:Spring 原生的 JDK 动态代理 + CGLIB 动态代理(无需手动干预,Spring 自动适配),配合
@Aspect注解式切面,开发效率最高、侵入性最低; - 极致性能需求:可考虑 AspectJ 加载时织入(LTW),但需接受部署复杂度;
- 历史场景 / 特殊需求:AspectJ 编译期织入(仅适合老项目或对性能要求极致的场景);
- 核心区别:AspectJ 是 “静态织入”(修改字节码),Spring 原生代理是 “动态织入”(运行时生成代理);静态织入性能好但侵入性强,动态织入灵活但有轻微运行时开销。
十、总结
Spring AOP 的核心是动态代理和切面织入,核心价值是 “分离关注点”:
- 核心业务代码专注于业务逻辑,横切逻辑集中在切面类中,实现复用;
- 底层通过 JDK/CGLIB 动态代理生成代理对象,运行时织入通知逻辑;
- 基于注解的
@AspectJ方式简化了 AOP 配置,是实际开发中的首选。