欢迎光临
我们一直在努力

【Day41】Spring AOP:面向切面编程(原理、注解与日志实战)

本文收录于「Java 学习日记」专栏,聚焦 Spring 核心特性 ——AOP(面向切面编程),从底层原理、核心概念拆解到注解式开发 + 日志实战,帮你彻底掌握 “无侵入式增强代码” 的精髓~

一、为什么要学 Spring AOP?

在传统开发中,我们会遇到一个核心问题:通用功能与业务逻辑耦合。

  • 日志记录、性能监控、事务管理、权限校验等通用功能,需要在每个业务方法中重复编写;
  • 修改通用功能时(如调整日志格式),需要修改所有相关业务代码,维护成本极高;
  • 业务代码中混杂大量非核心逻辑,可读性差,不符合 “单一职责原则”。

而Spring AOP(Aspect-Oriented Programming) 就是为解决这些问题而生:

  • 核心目标:将日志、事务、权限等 “横切逻辑” 从业务逻辑中抽离出来,以 “切面” 的形式统一管理,实现 “无侵入式增强”;
  • 核心优势:不修改原有业务代码,仅通过配置 / 注解就能为目标方法添加增强逻辑;
  • 实际应用:Spring 事务管理(@Transactional)、全局日志、接口权限校验、性能监控等,都是 AOP 的典型场景。

今天这篇日记,我们从 AOP 核心概念→底层原理→注解开发→日志实战,逐步拆解 Spring AOP 的使用方式。

二、AOP 核心概念(必须先理解)

AOP 的核心是 “切面” 和 “连接点”,先通过通俗的例子理解核心术语:

假设你要给公司所有员工的 “上班打卡” 方法添加两个通用逻辑:① 打卡前验证身份(前置增强);② 打卡后记录打卡日志(后置增强)。

  • 员工的 “上班打卡” 方法 = 连接点;
  • 所有需要打卡的员工 = 切入点;
  • 身份验证 + 日志记录 = 通知(增强逻辑);
  • 把通知绑定到切入点的规则 = 切面;
  • 整个添加增强的过程 = 织入。

AOP 核心术语对照表

术语中文名称核心含义
Joinpoint(连接点) 连接点 可以被增强的方法(如 Service 的业务方法、Controller 的请求方法)
Pointcut(切入点) 切入点 实际被增强的连接点(如指定包下的所有 Service 方法)
Advice(通知 / 增强) 通知 / 增强 切入到连接点的通用逻辑(如日志、事务),分前置、后置、环绕等类型
Aspect(切面) 切面 切入点 + 通知的组合(定义 “对哪些方法,在什么时候,做什么增强”)
Weaving(织入) 织入 将切面逻辑嵌入到目标方法的过程(Spring 在运行时织入)
Target(目标对象) 目标对象 被增强的原始对象(如 UserService)
Proxy(代理对象) 代理对象 织入切面后生成的对象(Spring AOP 默认用 JDK 动态代理 / CGLIB 代理)

通知(Advice)的 5 种类型

通知类型触发时机核心特点
@Before 目标方法执行前 无法阻止目标方法执行(除非抛异常)
@AfterReturning 目标方法正常执行后 可获取方法返回值
@AfterThrowing 目标方法抛出异常后 可获取异常信息
@After 目标方法执行后(无论是否异常) 类似 finally,总会执行
@Around 目标方法执行前后(环绕) 可控制目标方法的执行时机、是否执行,功能最强

三、Spring AOP 底层原理(动态代理)

Spring AOP 的核心是动态代理,通过代理对象包裹目标对象,在不修改目标对象代码的前提下,为方法添加增强逻辑。Spring 支持两种动态代理方式:

1. JDK 动态代理(默认)

  • 适用场景:目标对象实现了接口;
  • 核心原理:通过java.lang.reflect.Proxy类动态生成代理类,代理类实现与目标对象相同的接口;
  • 特点:基于接口代理,无侵入,JDK 原生支持。

2. CGLIB 代理

  • 适用场景:目标对象没有实现接口;
  • 核心原理:通过 ASM 字节码框架生成目标对象的子类,重写目标方法;
  • 特点:基于子类代理,需保证目标方法非 final(final 方法无法重写)。

动态代理核心流程(伪代码)

java

运行

// 1. 定义增强逻辑(通知)
public class LogAdvice {
public void before() { System.out.println("方法执行前记录日志"); }
public void after() { System.out.println("方法执行后记录日志"); }
}

// 2. 生成代理对象
UserService target = new UserServiceImpl(); // 目标对象
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
(proxyObj, method, args) -> {
// 前置增强
new LogAdvice().before();
// 执行目标方法
Object result = method.invoke(target, args);
// 后置增强
new LogAdvice().after();
return result;
}
);

// 3. 调用代理对象方法(触发增强)
proxy.getUserById("1001");

四、Spring AOP 实战:注解式开发(日志案例)

1. 环境准备

(1)Maven 依赖(核心)

xml

<dependencies>
<!– Spring Context(包含AOP基础) –>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.29</version>
</dependency>
<!– Spring AOP核心依赖 –>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.29</version>
</dependency>
<!– AspectJ(AOP注解解析) –>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.9.1</version>
</dependency>
</dependencies>

(2)配置类(开启 AOP 注解)

java

运行

package com.example.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

// 开启AOP注解支持
@EnableAspectJAutoProxy
@Configuration
@ComponentScan("com.example") // 扫描注解
public class SpringConfig {
}

2. 编写业务目标对象

java

运行

// 业务接口
package com.example.service;

public interface UserService {
String getUserById(String userId);
void deleteUser(String userId) throws Exception;
}

// 业务实现类(目标对象)
package com.example.service.impl;

import com.example.service.UserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
@Override
public String getUserById(String userId) {
System.out.println("【业务逻辑】查询用户ID:" + userId);
return "用户-" + userId;
}

@Override
public void deleteUser(String userId) throws Exception {
System.out.println("【业务逻辑】删除用户ID:" + userId);
// 模拟异常
if ("1002".equals(userId)) {
throw new Exception("删除用户失败:用户不存在");
}
}
}

3. 编写切面类(核心)

java

运行

package com.example.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

// 1. 标记为组件(让Spring管理)
@Component
// 2. 标记为切面
@Aspect
public class LogAspect {

// 3. 定义切入点(通用表达式,可被多个通知复用)
// 切入点表达式:匹配com.example.service包下所有类的所有方法
@Pointcut("execution(* com.example.service.*.*(..))")
public void servicePointcut() {}

// 4. 前置通知:目标方法执行前执行
@Before("servicePointcut()")
public void beforeAdvice(JoinPoint joinPoint) {
// 获取方法名、参数
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("[前置通知] 方法:" + methodName + ",参数:" + Arrays.toString(args));
}

// 5. 后置返回通知:目标方法正常执行后执行(可获取返回值)
@AfterReturning(value = "servicePointcut()", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[后置返回通知] 方法:" + methodName + ",返回值:" + result);
}

// 6. 后置异常通知:目标方法抛出异常后执行(可获取异常)
@AfterThrowing(value = "servicePointcut()", throwing = "e")
public void afterThrowingAdvice(JoinPoint joinPoint, Exception e) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[后置异常通知] 方法:" + methodName + ",异常:" + e.getMessage());
}

// 7. 最终通知:目标方法执行后执行(无论是否异常)
@After("servicePointcut()")
public void afterAdvice(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[最终通知] 方法:" + methodName + " 执行完成");
}

// 8. 环绕通知:功能最强,可控制目标方法执行
@Around("servicePointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
long startTime = System.currentTimeMillis();

Object result = null;
try {
// 执行前置逻辑
System.out.println("[环绕通知-前置] 方法:" + methodName + " 开始执行");
// 执行目标方法(必须调用,否则目标方法不执行)
result = joinPoint.proceed();
// 执行后置返回逻辑
System.out.println("[环绕通知-后置返回] 方法:" + methodName + " 执行成功");
} catch (Throwable e) {
// 执行后置异常逻辑
System.out.println("[环绕通知-后置异常] 方法:" + methodName + " 执行异常:" + e.getMessage());
throw e; // 抛出异常,不影响原有业务逻辑
} finally {
// 执行最终逻辑
long costTime = System.currentTimeMillis() – startTime;
System.out.println("[环绕通知-最终] 方法:" + methodName + " 耗时:" + costTime + "ms");
}
return result;
}
}

4. 切入点表达式(核心语法)

上面的@Pointcut("execution(* com.example.service.*.*(..))")是切入点表达式,核心语法:

plaintext

execution(修饰符 返回值 包名.类名.方法名(参数))

  • *:通配符,匹配任意(如返回值任意、方法名任意);
  • ..:匹配任意参数个数、任意类型;
  • 示例:
    • execution(public * com.example.service.UserService.*(..)):匹配 UserService 的所有公有方法;
    • execution(* com.example.service..*.*(String)):匹配 service 包及子包下所有类的单个 String 参数方法;
    • @annotation(com.example.annotation.Log):匹配标注了 @Log 注解的方法(自定义注解切入点)。

5. 测试 AOP 增强效果

java

运行

package com.example.test;

import com.example.config.SpringConfig;
import com.example.service.UserService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AopTest {
public static void main(String[] args) {
// 1. 创建Spring容器
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);

// 2. 获取代理对象(注意:这里获取的是代理对象,而非原始UserServiceImpl)
UserService userService = context.getBean(UserService.class);

// 3. 测试正常方法
System.out.println("===== 测试正常方法 =====");
userService.getUserById("1001");

// 4. 测试异常方法
System.out.println("\\n===== 测试异常方法 =====");
try {
userService.deleteUser("1002");
} catch (Exception e) {
// 捕获异常,不影响程序运行
}

// 5. 关闭容器
context.close();
}
}

6. 运行结果(核心流程验证)

plaintext

===== 测试正常方法 =====
[环绕通知-前置] 方法:getUserById 开始执行
[前置通知] 方法:getUserById,参数:[1001]
【业务逻辑】查询用户ID:1001
[后置返回通知] 方法:getUserById,返回值:用户-1001
[最终通知] 方法:getUserById 执行完成
[环绕通知-后置返回] 方法:getUserById 执行成功
[环绕通知-最终] 方法:getUserById 耗时:1ms

===== 测试异常方法 =====
[环绕通知-前置] 方法:deleteUser 开始执行
[前置通知] 方法:deleteUser,参数:[1002]
【业务逻辑】删除用户ID:1002
[后置异常通知] 方法:deleteUser,异常:删除用户失败:用户不存在
[最终通知] 方法:deleteUser 执行完成
[环绕通知-后置异常] 方法:deleteUser 执行异常:删除用户失败:用户不存在
[环绕通知-最终] 方法:deleteUser 耗时:0ms

五、AOP 进阶:自定义注解实现精准切入

1. 自定义注解

java

运行

package com.example.annotation;

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 Log {
// 注解属性:日志描述
String value() default "";
}

2. 修改切入点表达式(匹配自定义注解)

java

运行

@Aspect
@Component
public class LogAspect {
// 匹配标注了@Log注解的方法
@Pointcut("@annotation(com.example.annotation.Log)")
public void logAnnotationPointcut() {}

// 通知绑定自定义切入点
@Around("logAnnotationPointcut() && @annotation(log)")
public Object aroundLogAnnotation(ProceedingJoinPoint joinPoint, Log log) throws Throwable {
String desc = log.value();
System.out.println("[自定义注解通知] 日志描述:" + desc);
return joinPoint.proceed();
}
}

3. 业务方法添加自定义注解

java

运行

@Service
public class UserServiceImpl implements UserService {
@Override
@Log("查询用户信息") // 添加自定义注解
public String getUserById(String userId) {
System.out.println("【业务逻辑】查询用户ID:" + userId);
return "用户-" + userId;
}
}

4. 测试结果

plaintext

[自定义注解通知] 日志描述:查询用户信息
[前置通知] 方法:getUserById,参数:[1001]
【业务逻辑】查询用户ID:1001

六、AOP 避坑指南与最佳实践

1. 常见问题与解决方案

问题现象根因解决方案
AOP 增强不生效 1. 未加@EnableAspectJAutoProxy;2. 目标类 / 切面类未被 Spring 管理;3. 切入点表达式错误 1. 检查注解开启;2. 确保添加@Component/@Service;3. 验证切入点表达式
环绕通知中未调用proceed() 目标方法不会执行 必须调用joinPoint.proceed()
CGLIB 代理报错(final 方法) 目标方法是 final,无法被子类重写 移除 final 修饰符,或改用 JDK 代理
内部方法调用 AOP 不生效 目标对象内部方法调用,未经过代理对象 1. 注入自身代理对象;2. 通过 ApplicationContext 获取代理对象

2. 最佳实践

  • 切入点表达式:尽量缩小匹配范围(如指定包 / 类),避免全局匹配,提升性能;
  • 通知选择:
    • 简单增强(如日志)用@Before/@AfterReturning;
    • 需控制目标方法执行(如性能监控、限流)用@Around;
  • 自定义注解:精准切入需要增强的方法,避免无差别增强;
  • 异常处理:环绕通知中捕获异常后,需重新抛出,避免掩盖业务异常;
  • 代理方式:默认使用 JDK 动态代理,如需 CGLIB 代理,配置@EnableAspectJAutoProxy(proxyTargetClass = true)。
  • 七、今日实战小任务

  • 基于日志案例,新增 “权限校验” 切面:在目标方法执行前,校验是否包含指定权限参数,无权限则抛出异常;
  • 实现 “接口耗时统计” 功能:通过环绕通知记录每个方法的执行耗时,超过 100ms 则打印警告日志;
  • 自定义@Transactional注解(简化版),通过 AOP 实现 “方法执行前开启事务,执行后提交事务,异常回滚事务”。

  • 总结

  • Spring AOP 核心是将日志、事务等横切逻辑抽离为 “切面”,通过动态代理织入到目标方法中,实现无侵入式增强;
  • AOP 核心术语:连接点(可增强的方法)、切入点(实际增强的方法)、通知(增强逻辑)、切面(切入点 + 通知);
  • 通知分 5 种类型:@Before(前置)、@AfterReturning(后置返回)、@AfterThrowing(后置异常)、@After(最终)、@Around(环绕,功能最强);
  • 实战中,通过@Aspect定义切面、@Pointcut定义切入点、@EnableAspectJAutoProxy开启注解支持,结合自定义注解可实现精准切入;
  • 核心避坑点:AOP 增强不生效、环绕通知未调用proceed()、内部方法调用代理不生效。
  • 下一篇【Day42】预告:Spring 声明式事务:@Transactional 注解使用与事务传播机制,关注专栏继续解锁 Spring 核心特性~若本文对你有帮助,欢迎点赞 + 收藏 + 关注,你的支持是我更新的最大动力💖!

    赞(0)
    未经允许不得转载:171主机测评 » 【Day41】Spring AOP:面向切面编程(原理、注解与日志实战)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址