问题

介绍一下Spring的AOP

答案

1. 核心概念

AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,通过预编译方式运行期动态代理实现程序功能的统一维护。AOP是Spring框架的核心特性之一,用于将横切关注点(Cross-cutting Concerns)与业务逻辑分离。

横切关注点:在多个模块中都需要的功能,如日志记录、事务管理、权限控制、性能监控等。

核心优势

  • 减少代码重复
  • 提高模块化程度
  • 业务逻辑与系统服务解耦
  • 易于维护和扩展

2. AOP核心术语

(1)切面(Aspect)

横切关注点的模块化,包含通知和切点的定义。

@Aspect
@Component
public class LoggingAspect {
    // 这是一个切面类
}

(2)连接点(Join Point)

程序执行过程中的某个特定点,如方法调用、异常抛出等。在Spring AOP中,连接点始终代表方法执行

public void transfer(String from, String to, double amount) {
    // 方法执行前 - 连接点
    // 方法执行中 - 连接点
    // 方法执行后 - 连接点
    // 方法异常时 - 连接点
}

(3)切点(Pointcut)

匹配连接点的表达式,定义在哪里应用通知。

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

(4)通知(Advice)

在切面的某个特定连接点上执行的动作。

@Before("serviceLayer()")
public void logBefore(JoinPoint joinPoint) {
    // 前置通知:方法执行前执行
    System.out.println("执行方法: " + joinPoint.getSignature().getName());
}

(5)目标对象(Target Object)

被一个或多个切面通知的对象,也称为被通知对象。

(6)AOP代理(AOP Proxy)

由AOP框架创建的对象,用于实现切面契约。Spring AOP使用JDK动态代理CGLIB代理

(7)织入(Weaving)

将切面应用到目标对象并创建代理对象的过程。

织入时机

  • 编译期织入:AspectJ支持
  • 类加载期织入:AspectJ支持
  • 运行期织入:Spring AOP采用,性能略低但灵活

3. Spring AOP实现原理

代理模式选择

// 有接口 - 使用JDK动态代理
public interface UserService {
    void addUser(String name);
}

@Service
public class UserServiceImpl implements UserService {
    public void addUser(String name) {
        System.out.println("添加用户: " + name);
    }
}

// 无接口或强制CGLIB - 使用CGLIB代理
@Service
public class OrderService {
    public void createOrder(Long orderId) {
        System.out.println("创建订单: " + orderId);
    }
}

代理选择规则

  • 目标对象实现了接口 → JDK动态代理
  • 目标对象未实现接口 → CGLIB代理
  • 强制使用CGLIB:@EnableAspectJAutoProxy(proxyTargetClass = true)

4. 切点表达式语法

execution表达式(最常用)

// 语法:execution(修饰符? 返回值 包.类.方法(参数) throws异常?)
// ?表示可选

// 示例1:匹配所有public方法
@Pointcut("execution(public * *(..))")

// 示例2:匹配service包及子包下所有方法
@Pointcut("execution(* com.example.service..*.*(..))")

// 示例3:匹配所有save开头的方法
@Pointcut("execution(* save*(..))")

// 示例4:匹配第一个参数为String的方法
@Pointcut("execution(* *(String, ..))")

// 示例5:匹配返回值为User的方法
@Pointcut("execution(com.example.User *(..))")

其他切点指示符

// 1. within:匹配指定类型内的方法
@Pointcut("within(com.example.service.UserService)")

// 2. this:匹配代理对象类型(限定代理对象)
@Pointcut("this(com.example.service.UserService)")

// 3. target:匹配目标对象类型
@Pointcut("target(com.example.service.UserService)")

// 4. args:匹配参数类型
@Pointcut("args(String, Long)")

// 5. @annotation:匹配有指定注解的方法
@Pointcut("@annotation(com.example.annotation.Log)")

// 6. @within:匹配标注了指定注解的类
@Pointcut("@within(org.springframework.stereotype.Service)")

// 7. @target:匹配目标对象标注了指定注解
@Pointcut("@target(org.springframework.stereotype.Repository)")

// 8. @args:匹配参数标注了指定注解
@Pointcut("@args(com.example.annotation.Validated)")

切点组合

// 与(&&)
@Pointcut("execution(* com.example.service.*.*(..)) && @annotation(com.example.Log)")

// 或(||)
@Pointcut("execution(* save*(..)) || execution(* update*(..))")

// 非(!)
@Pointcut("execution(* com.example.service.*.*(..)) && !execution(* com.example.service.Internal*.*(..))")

5. 完整使用示例

定义切面

@Aspect
@Component
@Slf4j
public class LoggingAspect {

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

    // 前置通知
    @Before("serviceLayer()")
    public void logBefore(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        log.info("方法开始执行: {}, 参数: {}", methodName, Arrays.toString(args));
    }

    // 后置通知(无论是否异常都执行)
    @After("serviceLayer()")
    public void logAfter(JoinPoint joinPoint) {
        log.info("方法执行结束: {}", joinPoint.getSignature().getName());
    }

    // 返回通知(方法正常返回后执行)
    @AfterReturning(pointcut = "serviceLayer()", returning = "result")
    public void logAfterReturning(JoinPoint joinPoint, Object result) {
        log.info("方法返回: {}, 结果: {}", joinPoint.getSignature().getName(), result);
    }

    // 异常通知(方法抛出异常后执行)
    @AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
    public void logAfterThrowing(JoinPoint joinPoint, Exception ex) {
        log.error("方法异常: {}, 异常信息: {}", joinPoint.getSignature().getName(), ex.getMessage());
    }

    // 环绕通知(最强大,可以控制方法执行)
    @Around("serviceLayer()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        String methodName = joinPoint.getSignature().getName();

        log.info("方法开始: {}", methodName);

        try {
            // 执行目标方法
            Object result = joinPoint.proceed();
            
            long endTime = System.currentTimeMillis();
            log.info("方法结束: {}, 耗时: {}ms", methodName, endTime - startTime);
            
            return result;
        } catch (Throwable throwable) {
            log.error("方法异常: {}, 异常: {}", methodName, throwable.getMessage());
            throw throwable;
        }
    }
}

启用AOP

@Configuration
@EnableAspectJAutoProxy  // 启用AspectJ自动代理
public class AppConfig {
    // 配置类
}

或在Spring Boot中(默认已启用):

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

6. 实际应用场景

场景1:日志记录

@Aspect
@Component
public class LogAspect {
    
    @Around("@annotation(com.example.annotation.Log)")
    public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = signature.getName();
        Object[] args = joinPoint.getArgs();
        
        // 记录请求日志
        log.info("请求: {}.{}, 参数: {}", className, methodName, args);
        
        long startTime = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long endTime = System.currentTimeMillis();
        
        // 记录响应日志
        log.info("响应: {}.{}, 结果: {}, 耗时: {}ms", 
                 className, methodName, result, endTime - startTime);
        
        return result;
    }
}

场景2:权限控制

@Aspect
@Component
public class PermissionAspect {
    
    @Autowired
    private UserContext userContext;
    
    @Before("@annotation(requirePermission)")
    public void checkPermission(JoinPoint joinPoint, RequirePermission requirePermission) {
        String[] permissions = requirePermission.value();
        User currentUser = userContext.getCurrentUser();
        
        if (currentUser == null) {
            throw new UnauthorizedException("未登录");
        }
        
        boolean hasPermission = Arrays.stream(permissions)
            .anyMatch(p -> currentUser.hasPermission(p));
        
        if (!hasPermission) {
            throw new ForbiddenException("无权限访问");
        }
    }
}

// 使用
@Service
public class UserService {
    
    @RequirePermission("USER:DELETE")
    public void deleteUser(Long userId) {
        // 删除用户逻辑
    }
}

场景3:事务管理(Spring内置)

// Spring的@Transactional就是通过AOP实现的
@Service
public class OrderService {
    
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Order order) {
        // 保存订单
        orderRepository.save(order);
        // 扣减库存
        inventoryService.deduct(order.getProductId(), order.getQuantity());
    }
}

场景4:缓存管理

@Aspect
@Component
public class CacheAspect {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Around("@annotation(cacheable)")
    public Object cache(ProceedingJoinPoint joinPoint, Cacheable cacheable) throws Throwable {
        // 生成缓存key
        String key = generateKey(joinPoint, cacheable.key());
        
        // 查询缓存
        Object cachedValue = redisTemplate.opsForValue().get(key);
        if (cachedValue != null) {
            return cachedValue;
        }
        
        // 执行方法
        Object result = joinPoint.proceed();
        
        // 写入缓存
        redisTemplate.opsForValue().set(key, result, cacheable.timeout(), TimeUnit.SECONDS);
        
        return result;
    }
}

场景5:性能监控

@Aspect
@Component
public class PerformanceAspect {
    
    @Around("execution(* com.example.service.*.*(..))")
    public Object monitor(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().toShortString();
        long startTime = System.currentTimeMillis();
        
        Object result = joinPoint.proceed();
        
        long endTime = System.currentTimeMillis();
        long duration = endTime - startTime;
        
        // 性能告警
        if (duration > 1000) {
            log.warn("方法执行慢: {}, 耗时: {}ms", methodName, duration);
            // 发送告警
            alertService.sendAlert(methodName, duration);
        }
        
        // 上报监控指标
        metricsService.recordMethodDuration(methodName, duration);
        
        return result;
    }
}

7. Spring AOP与AspectJ对比

维度 Spring AOP AspectJ
实现方式 运行时动态代理 编译期/加载期织入
织入时机 运行时 编译期、加载期、运行期
连接点 仅方法执行 方法、构造器、字段读写等
性能 略慢(代理开销) 更快(直接字节码)
依赖 Spring容器 独立使用
功能 满足大部分场景 功能更强大
学习成本

8. 注意事项

(1)代理限制

@Service
public class UserService {
    
    // AOP会生效(外部调用)
    public void methodA() {
        System.out.println("Method A");
        methodB();  // AOP不会生效(内部调用)
    }
    
    public void methodB() {
        System.out.println("Method B");
    }
}

原因:内部调用不经过代理对象,直接调用目标对象方法。

解决方案

  • 方案1:通过ApplicationContext获取代理对象
  • 方案2:使用AopContext.currentProxy()
  • 方案3:拆分到不同类

(2)final方法和类

// CGLIB无法代理final类
public final class FinalClass {
    public void method() {}
}

public class MyService {
    // CGLIB无法代理final方法
    public final void finalMethod() {}
}

(3)private方法

public class UserService {
    // Spring AOP无法拦截private方法
    private void privateMethod() {}
}

9. 面试答题要点

标准回答结构

  1. 定义:AOP是面向切面编程,用于将横切关注点与业务逻辑分离

  2. 核心概念:切面、切点、通知、连接点、织入

  3. 实现原理
    • JDK动态代理(有接口)
    • CGLIB代理(无接口)
    • 运行时织入
  4. 通知类型:Before、After、AfterReturning、AfterThrowing、Around

  5. 应用场景:日志、事务、权限、缓存、性能监控

  6. 与AspectJ对比:Spring AOP更轻量但功能有限

加分点

  • 了解代理模式选择原理
  • 知道内部调用失效的原因和解决方案
  • 能说明切点表达式语法
  • 了解CGLIB的限制(final类/方法)
  • 能举实际项目应用案例

10. 总结

核心要点

维度 说明
本质 通过代理模式实现横切关注点分离
优势 减少重复代码,提高模块化
实现 JDK动态代理 + CGLIB
连接点 仅支持方法执行
场景 日志、事务、权限、缓存、监控

记忆口诀

  • AOP面向切面,横切要分离
  • 代理有两种,JDK和CGLIB
  • 通知有五类,环绕最灵活
  • 内部调用坑,代理会失效
  • 日志事务权限,场景很常见

使用建议

  • 适合横切关注点:日志、事务、权限、缓存
  • 避免过度使用:保持代码可读性
  • 注意代理限制:内部调用、final、private
  • 性能敏感场景:考虑使用AspectJ

Spring AOP是Spring框架的核心特性,掌握其原理和应用是成为Spring高级开发者的必备技能。