知识模块
☕ Java 知识模块
八、Spring 全家桶
Spring AOP

Spring AOP

什么是 AOP?

AOP(Aspect-Oriented Programming):面向切面编程,将横切关注点(日志、事务、权限等)从业务逻辑中分离出来。

核心概念

概念说明示例
切面(Aspect)横切关注点的模块化@Aspect 类
连接点(Join Point)程序执行的某个点方法调用、异常抛出
切点(Pointcut)匹配连接点的表达式execution(* com.example..(..))
通知(Advice)在切点执行的动作@Before、@After、@Around
目标对象(Target)被通知的对象业务 Bean
代理(Proxy)AOP 创建的对象JDK 动态代理、CGLIB
织入(Weaving)将切面应用到目标对象编译期、类加载期、运行期

通知类型

@Aspect
@Component
public class LoggingAspect {
    
    // 前置通知
    @Before("execution(* com.example.service.*.*(..))")
    public void before(JoinPoint joinPoint) {
        System.out.println("方法执行前: " + joinPoint.getSignature().getName());
    }
    
    // 后置通知(无论是否异常都执行)
    @After("execution(* com.example.service.*.*(..))")
    public void after(JoinPoint joinPoint) {
        System.out.println("方法执行后");
    }
    
    // 返回通知(方法成功返回)
    @AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
    public void afterReturning(JoinPoint joinPoint, Object result) {
        System.out.println("方法返回: " + result);
    }
    
    // 异常通知
    @AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint, Exception ex) {
        System.out.println("方法异常: " + ex.getMessage());
    }
    
    // 环绕通知(最强大)
    @Around("execution(* com.example.service.*.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕前");
        try {
            Object result = joinPoint.proceed(); // 执行目标方法
            System.out.println("环绕后");
            return result;
        } catch (Exception e) {
            System.out.println("环绕异常");
            throw e;
        } finally {
            System.out.println("环绕最终");
        }
    }
}

通知执行顺序

正常执行:
Around 前 → Before → 目标方法 → Around 后 → After → AfterReturning

异常执行:
Around 前 → Before → 目标方法抛异常 → Around 异常 → After → AfterThrowing

切点表达式

execution(最常用)

// 匹配所有 public 方法
execution(public * *(..))
 
// 匹配指定包下所有方法
execution(* com.example.service.*.*(..))
 
// 匹配指定类所有方法
execution(* com.example.service.UserService.*(..))
 
// 匹配指定方法名
execution(* com.example.service.UserService.getUser(..))
 
// 匹配特定参数
execution(* com.example.service.UserService.getUser(Long))
 
// 匹配任意参数
execution(* com.example.service.UserService.getUser(..))
 
// 匹配返回值
execution(String com.example.service.*.*(..))

其他切点指示符

// within:匹配类型
@Pointcut("within(com.example.service..*)")
public void serviceLayer() {}
 
// @annotation:匹配有特定注解的方法
@Pointcut("@annotation(com.example.annotation.Log)")
public void logAnnotation() {}
 
// @within:匹配有特定注解的类
@Pointcut("@within(org.springframework.stereotype.Service)")
public void serviceClass() {}
 
// args:匹配参数类型
@Pointcut("args(String, ..)")
public void firstArgString() {}
 
// bean:匹配 Bean 名称
@Pointcut("bean(userService)")
public void userServiceBean() {}
 
// 组合切点
@Pointcut("execution(* com.example.service.*.*(..)) && args(id, ..)")
public void serviceMethodWithFirstArg(Long id) {}

切点表达式组合

@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
 
@Pointcut("execution(* com.example.dao.*.*(..))")
public void daoLayer() {}
 
// && 与
@Before("serviceLayer() && args(id)")
public void beforeService(Long id) {}
 
// || 或
@Before("serviceLayer() || daoLayer()")
public void beforeServiceOrDao() {}
 
// ! 非
@Before("serviceLayer() && !execution(* com.example.service.UserService.*(..))")
public void beforeServiceExcludeUserService() {}

代理机制

JDK 动态代理 vs CGLIB

特性JDK 动态代理CGLIB
实现方式基于接口基于继承
要求目标类实现接口目标类不能是 final
性能调用稍慢(反射)创建慢,调用快
Spring 默认接口用 JDK无接口用 CGLIB

Spring 代理选择策略

1. 目标类实现接口?
   ├── 是 → 默认使用 JDK 动态代理
   └── 否 → 使用 CGLIB

2. 配置强制使用 CGLIB
   @EnableAspectJAutoProxy(proxyTargetClass = true)

   spring.aop.proxy-target-class=true

代理对象创建过程

1. Spring 创建目标 Bean
2. 初始化后(postProcessAfterInitialization)
3. 检查是否有切面匹配该 Bean
4. 有匹配 → 创建代理对象
   ├── 实现接口 → JDK 动态代理
   └── 无接口 → CGLIB 代理
5. 返回代理对象,替换原 Bean

代码示例

// 目标类
@Service
public class UserService {
    public User getUser(Long id) {
        return new User(id, "张三");
    }
}
 
// 切面
@Aspect
@Component
public class LoggingAspect {
    @Around("execution(* com.example.service.UserService.*(..))")
    public Object log(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("调用方法: " + pjp.getSignature());
        Object result = pjp.proceed();
        System.out.println("返回结果: " + result);
        return result;
    }
}
 
// 代理后的调用链
UserService proxy = context.getBean(UserService.class);
proxy.getUser(1L);
 
// 实际执行:
// proxy.getUser() → LoggingAspect.log() → target.getUser()

@EnableAspectJAutoProxy

@Configuration
@EnableAspectJAutoProxy(
    proxyTargetClass = true,  // 强制使用 CGLIB
    exposeProxy = true        // 暴露代理对象到 AopContext
)
public class AopConfig {
}

exposeProxy 用途

// 问题:同类方法调用不走代理
@Service
public class UserService {
    
    @Transactional
    public void methodA() {
        // 直接调用 methodB,不走代理,@Transactional 失效
        methodB();
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // ...
    }
}
 
// 解决:通过代理调用
@Service
public class UserService {
    
    @Transactional
    public void methodA() {
        // 通过代理调用
        ((UserService) AopContext.currentProxy()).methodB();
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // ...
    }
}

实际应用

日志切面

@Aspect
@Component
@Slf4j
public class LogAspect {
    
    @Around("@annotation(logAnnotation)")
    public Object log(ProceedingJoinPoint pjp, LogAnnotation logAnnotation) throws Throwable {
        String methodName = pjp.getSignature().getName();
        Object[] args = pjp.getArgs();
        
        log.info("方法 {} 开始执行,参数: {}", methodName, Arrays.toString(args));
        
        long start = System.currentTimeMillis();
        try {
            Object result = pjp.proceed();
            long cost = System.currentTimeMillis() - start;
            log.info("方法 {} 执行成功,耗时: {}ms,结果: {}", methodName, cost, result);
            return result;
        } catch (Exception e) {
            log.error("方法 {} 执行异常: {}", methodName, e.getMessage());
            throw e;
        }
    }
}
 
// 使用
@LogAnnotation
public User getUser(Long id) {
    return userDao.findById(id);
}

权限切面

@Aspect
@Component
public class PermissionAspect {
    
    @Before("@annotation(requirePermission)")
    public void checkPermission(JoinPoint jp, RequirePermission requirePermission) {
        String permission = requirePermission.value();
        User user = SecurityContext.getCurrentUser();
        
        if (!user.hasPermission(permission)) {
            throw new PermissionDeniedException("无权限: " + permission);
        }
    }
}
 
// 使用
@RequirePermission("user:delete")
public void deleteUser(Long id) {
    userDao.deleteById(id);
}

性能监控切面

@Aspect
@Component
public class PerformanceAspect {
    
    @Around("execution(* com.example.service.*.*(..))")
    public Object monitor(ProceedingJoinPoint pjp) throws Throwable {
        String methodName = pjp.getSignature().toShortString();
        
        long start = System.nanoTime();
        Object result = pjp.proceed();
        long cost = System.nanoTime() - start;
        
        if (cost > 1_000_000) { // 超过 1ms 记录
            MetricsCollector.recordSlowMethod(methodName, cost / 1_000_000.0);
        }
        
        return result;
    }
}

面试高频问题

Q1: AOP 是如何实现的?

回答要点:

  1. 编译期织入:AspectJ(需要特殊编译器)
  2. 类加载期织入:AspectJ LTW(Load-Time Weaving)
  3. 运行期织入:Spring AOP(动态代理)

Spring AOP 使用运行期织入:

  • 通过 BeanPostProcessor 在 Bean 初始化后创建代理
  • JDK 动态代理或 CGLIB

Q2: Spring AOP 和 AspectJ 的区别?

特性Spring AOPAspectJ
织入时机运行期编译期/类加载期
实现方式动态代理字节码增强
功能方法级别方法、字段、构造函数等
性能稍慢更快
依赖Spring 容器独立使用

Q3: 什么情况下 AOP 会失效?

回答要点:

  1. 同类方法调用:直接调用不走代理
// 解决方案:
// 1. 注入自己
@Autowired
private UserService self;
 
public void methodA() {
    self.methodB(); // 走代理
}
 
// 2. 使用 AopContext
((UserService) AopContext.currentProxy()).methodB();
  1. private/protected 方法:CGLIB 无法代理

  2. final 方法/类:CGLIB 无法继承

  3. 构造函数内调用:Bean 未完成初始化

Q4: JDK 动态代理和 CGLIB 的区别?

对比JDK 动态代理CGLIB
实现原理反射 + 接口继承 + 字节码生成
要求必须实现接口不能是 final 类
性能反射调用稍慢FastClass 优化
Spring 选择接口优先无接口时使用

Q5: @Around 和其他通知的执行顺序?

@Around 前 → @Before → 目标方法 → @Around 后 → @After → @AfterReturning/@AfterThrowing

多个切面的顺序:

@Order(1)  // 数字越小优先级越高
@Aspect
public class Aspect1 {}
 
@Order(2)
@Aspect
public class Aspect2 {}
 
// 执行顺序:
// Aspect1 Around 前 → Aspect2 Around 前 → Aspect1 Before → Aspect2 Before → 目标方法

总结

Spring AOP 核心要点:
1. 核心概念:切面、切点、通知、连接点
2. 通知类型:@Before、@After、@AfterReturning、@AfterThrowing、@Around
3. 切点表达式:execution 最常用
4. 代理机制:JDK 动态代理(接口)、CGLIB(类)
5. 失效场景:同类调用、private/final 方法