JavaEE AOP详解(从原理到实践 基于Spring AOP + AspectJ,附完整代码实例)

一、AOP核心概念

1. 为什么需要AOP?

传统OOP编程中,重复的逻辑(如日志、事务、权限)会散落在各业务方法中,造成代码冗余维护困难。AOP通过横向切割将这些公共功能抽取成独立模块(切面),实现解耦。

2. AOP核心术语
术语 描述 生活类比
Aspect(切面) 封装横切逻辑的类(如日志、事务) 安保系统:处理监控、门禁等统一功能
JoinPoint(连接点) 程序执行期间的某个点(如方法执行、异常抛出) 大楼出入口:可能被安保系统监控的位置
Pointcut(切入点) 表达式,定义哪些连接点会应用切面逻辑 安保规则:规定哪些出入口需要检查身份证
Advice(通知) 切面在特定连接点执行的动作(如方法执行前、后) 安保动作:在出入口检查身份证、记录日志
Weaving(织入) 将切面代码与目标对象结合的过程(编译期/运行时) 在大楼建造时预埋监控设备

二、Spring AOP 实现详解

1. 依赖配置(Maven)
 
  



    org.springframework.boot
    spring-boot-starter-aop



    org.aspectj
    aspectjweaver
    1.9.7

2. 切面定义示例:日志记录
 
  

@Aspect
@Component
public class LoggingAspect {

    // 定义切入点:service包下的所有方法
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() {}

    // 前置通知:在方法执行前打印日志
    @Before("serviceLayer()")
    public void logMethodStart(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("【前置日志】方法 " + methodName + " 开始执行, 参数: " + Arrays.toString(joinPoint.getArgs()));
    }

    // 后置通知:在方法执行后打印日志(无论是否抛异常)
    @After("serviceLayer()")
    public void logMethodEnd(JoinPoint joinPoint) {
        System.out.println("【后置日志】方法 " + joinPoint.getSignature().getName() + " 执行结束");
    }

    // 返回通知:获取方法返回值
    @AfterReturning(pointcut = "serviceLayer()", returning = "result")
    public void logReturnValue(Object result) {
        System.out.println("【返回日志】方法返回值: " + result);
    }

    // 异常通知:捕获异常信息
    @AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
    public void logException(JoinPoint joinPoint, Exception ex) {
        System.out.println("【异常日志】方法 " + joinPoint.getSignature().getName() + " 抛出异常: " + ex.getMessage());
    }

    // 环绕通知:控制整个方法执行流程
    @Around("serviceLayer()")
    public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("【环绕前】准备执行方法");
        Object result = pjp.proceed(); // 执行目标方法
        System.out.println("【环绕后】方法执行完毕");
        return result;
    }
}
3. 目标业务类
 
  

@Service
public class UserService {
    public String getUserById(Long id) {
        System.out.println("执行业务方法:根据ID查询用户");
        if (id == 1) {
            return "用户: Alice";
        } else {
            throw new RuntimeException("用户不存在");
        }
    }
}
4. 测试代码
 
  

@SpringBootTest
public class UserServiceTest {
    @Autowired
    private UserService userService;

    @Test
    public void testLoggingAspect() {
        userService.getUserById(1L);
        System.out.println("------");
        try {
            userService.getUserById(2L);
        } catch (Exception ignored) {}
    }
}
5. 输出结果
【环绕前】准备执行方法
【前置日志】方法 getUserById 开始执行, 参数: [1]
执行业务方法:根据ID查询用户
【返回日志】方法返回值: 用户: Alice
【后置日志】方法 getUserById 执行结束
【环绕后】方法执行完毕
------
【环绕前】准备执行方法
【前置日志】方法 getUserById 开始执行, 参数: [2]
执行业务方法:根据ID查询用户
【异常日志】方法 getUserById 抛出异常: 用户不存在
【后置日志】方法 getUserById 执行结束

三、AOP高阶应用

1. 注解驱动自定义切面

定义自定义注解

 
  

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
}

切面实现方法耗时统计

 
  

@Aspect
@Component
public class ExecutionTimeAspect {

    @Around("@annotation(LogExecutionTime)")
    public Object logExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = pjp.proceed();
        long duration = System.currentTimeMillis() - startTime;
        System.out.println("方法 " + pjp.getSignature() + " 执行时间: " + duration + "ms");
        return result;
    }
}

在业务方法上使用注解

 
  

@Service
public class OrderService {
    @LogExecutionTime
    public void processOrder() {
        // 模拟耗时操作
        try { Thread.sleep(1500); } catch (InterruptedException ignored) {}
    }
}

2. 切入点表达式详解
表达式类型 示例 说明
execution execution(* com.service.*.*(..)) 匹配service包下所有类的所有方法
@annotation @annotation(com.example.LogExecutionTime) 匹配被指定注解标记的方法
within within(com.controller.*) 匹配controller包中所有方法
args args(java.lang.String, ..) 匹配第一个参数为String类型的方法
bean bean(userService) 匹配Spring容器中名为userService的Bean的方法

四、AOP原理与常见问题

1. 动态代理实现方式
  • JDK动态代理:基于接口代理,使用Proxy类和InvocationHandler接口。
  • CGLIB代理:通过继承目标类生成子类,覆盖方法实现代理(无需接口)。
2. 常见问题

Q1:为什么切入点表达式不生效?

  • 检查包路径是否正确
  • 确保目标方法是否为public
  • 确认是否在同一个Spring容器中

Q2:AOP适用的场景有哪些?

  • 日志记录、性能统计
  • 事务管理(@Transactional底层基于AOP)
  • 权限校验、接口限流
  • 数据脱敏、缓存控制

Q3:如何选择Spring AOP和AspectJ?

对比项 Spring AOP AspectJ
织入方式 运行时通过动态代理实现 编译期/类加载期织入
功能范围 仅支持方法级别的切入点 支持字段、构造方法等更细致的控制
性能 有轻微运行时开销 编译期织入无运行时开销
易用性 简单,与Spring整合无缝 需要额外编译器/工具支持

你可能感兴趣的:(JAVAEE,java-ee,spring,java)