问题
介绍一下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. 面试答题要点
标准回答结构:
-
定义:AOP是面向切面编程,用于将横切关注点与业务逻辑分离
-
核心概念:切面、切点、通知、连接点、织入
- 实现原理:
- JDK动态代理(有接口)
- CGLIB代理(无接口)
- 运行时织入
-
通知类型:Before、After、AfterReturning、AfterThrowing、Around
-
应用场景:日志、事务、权限、缓存、性能监控
- 与AspectJ对比:Spring AOP更轻量但功能有限
加分点:
- 了解代理模式选择原理
- 知道内部调用失效的原因和解决方案
- 能说明切点表达式语法
- 了解CGLIB的限制(final类/方法)
- 能举实际项目应用案例
10. 总结
核心要点:
| 维度 | 说明 |
|---|---|
| 本质 | 通过代理模式实现横切关注点分离 |
| 优势 | 减少重复代码,提高模块化 |
| 实现 | JDK动态代理 + CGLIB |
| 连接点 | 仅支持方法执行 |
| 场景 | 日志、事务、权限、缓存、监控 |
记忆口诀:
- AOP面向切面,横切要分离
- 代理有两种,JDK和CGLIB
- 通知有五类,环绕最灵活
- 内部调用坑,代理会失效
- 日志事务权限,场景很常见
使用建议:
- 适合横切关注点:日志、事务、权限、缓存
- 避免过度使用:保持代码可读性
- 注意代理限制:内部调用、final、private
- 性能敏感场景:考虑使用AspectJ
Spring AOP是Spring框架的核心特性,掌握其原理和应用是成为Spring高级开发者的必备技能。