@Around
定义与作用
@Around 声明环绕通知(Around Advice),是 Spring AOP 中最强大、最灵活的 Advice 类型。它完全包围目标方法的执行,可以在方法调用前后自定义行为,并决定是否执行原方法、修改传入参数、修改返回值,甚至替换整个方法逻辑。
关键特性:
@Around是唯一能控制目标方法是否执行的 Advice 类型@Around可以修改传入参数(通过ProceedingJoinPoint.proceed(Object[] args))@Around可以修改或替换返回值@Around可以捕获并处理异常,阻止异常传播或转换为其他异常@Around必须显式调用ProceedingJoinPoint.proceed(),否则目标方法不会执行@Around的返回值会作为代理方法的返回值返回给客户端
适用位置与常用属性
适用位置
@Around 标注在切面类的方法上,表示该方法是一个环绕通知。该方法必须位于 @Aspect 切面类内部。
@Aspect
@Component
public class PerformanceAspect {
@Around("execution(* com.feixiang.service.*.*(..))")
public Object measurePerformance(ProceedingJoinPoint joinPoint) throws Throwable {
// 方法执行前逻辑
Object result = joinPoint.proceed(); // 执行目标方法
// 方法执行后逻辑
return result;
}
}
常用属性
| 属性 | 类型 | 说明 |
|---|---|---|
value | String | 切入点表达式,或引用 @Pointcut 方法名。唯一必填属性 |
argNames | String | 参数名称,用于绑定目标方法参数(通常可省略) |
核心原理
@Around 在代理链中的执行位置
@Around 完全包围目标方法的执行,是代理链中最外层的 Advice:
ProceedingJoinPoint 的核心机制
ProceedingJoinPoint 是 JoinPoint 的子接口,是 @Around 的专属参数类型,提供了控制目标方法执行的关键方法:
ProceedingJoinPoint 核心方法:
| 方法 | 作用 |
|---|---|
proceed() | 使用原始参数执行目标方法 |
proceed(Object[] args) | 使用修改后的参数执行目标方法 |
getArgs() | 获取目标方法的参数数组(可修改) |
getSignature() | 获取方法签名 |
getTarget() | 获取目标对象 |
getThis() | 获取代理对象 |
完整示例:飞翔科技学生管理系统环绕切面
场景简述
广州飞翔科技公司的后端开发小崔需要为学生成绩管理系统实现一套完整的横切关注点:
- 性能监控:记录每个 Service 方法的执行耗时,超过 100ms 打印警告
- 参数校验:在方法执行前校验参数,失败时直接返回错误信息
- 结果缓存:查询方法先查缓存,命中则直接返回,未命中则执行查询并写入缓存
- 异常转换:将底层数据库异常转换为业务异常,统一返回格式
- 日志记录:记录方法入参和出参
架构师白歌指出:这些需求中,性能监控可以用 @Before + @After,但参数修改、返回值修改、异常转换、缓存控制都必须使用 @Around。小崔决定用 @Around 实现一个综合性的系统监控切面。
操作前代码/配置
在未使用 @Around 之前,小崔在每个 Service 方法中手动实现上述功能:
@Service
public class StudentScoreService {
private final ScoreDao scoreDao;
private final Map<String, Object> cache = new ConcurrentHashMap<>();
public StudentScoreService(ScoreDao scoreDao) {
this.scoreDao = scoreDao;
}
public Score queryScore(Long studentId) {
long start = System.currentTimeMillis();
// 1. 参数校验
if (studentId == null || studentId <= 0) {
throw new IllegalArgumentException("学号必须为正整数");
}
// 2. 缓存查询
String key = "score:" + studentId;
Score cached = (Score) cache.get(key);
if (cached != null) {
System.out.println("[缓存] 命中");
return cached;
}
// 3. 日志记录
System.out.println("[日志] queryScore 入参:studentId=" + studentId);
try {
// 4. 业务逻辑
Score score = scoreDao.findByStudentId(studentId);
// 5. 放入缓存
if (score != null) {
cache.put(key, score);
}
// 6. 日志记录
System.out.println("[日志] queryScore 出参:" + score);
// 7. 性能监控
long duration = System.currentTimeMillis() - start;
if (duration > 100) {
System.out.println("[警告] queryScore 耗时 " + duration + "ms,超过阈值");
}
return score;
} catch (DataAccessException e) {
// 8. 异常转换
throw new BusinessException("查询成绩失败,请稍后重试", e);
}
}
public void updateScore(Long studentId, Double newScore) {
long start = System.currentTimeMillis();
// 重复!同样的横切逻辑再次出现在另一个方法中
if (studentId == null || studentId <= 0) {
throw new IllegalArgumentException("学号必须为正整数");
}
if (newScore == null || newScore < 0 || newScore > 100) {
throw new IllegalArgumentException("成绩必须在 0-100 之间");
}
System.out.println("[日志] updateScore 入参:studentId=" + studentId + ", newScore=" + newScore);
try {
scoreDao.updateScore(studentId, newScore);
// 清除缓存
cache.remove("score:" + studentId);
long duration = System.currentTimeMillis() - start;
if (duration > 100) {
System.out.println("[警告] updateScore 耗时 " + duration + "ms");
}
} catch (DataAccessException e) {
throw new BusinessException("更新成绩失败", e);
}
}
}
问题:横切关注点代码严重重复,业务逻辑被淹没。如果大翔要求将缓存从本地 Map 改为 Redis,或调整性能阈值,所有方法都要修改。
使用该注解的完整代码
第一步:定义综合监控切面
@Aspect
@Component
public class SystemMonitorAspect {
// 模拟缓存(实际使用 Redis)
private final Map<String, Object> cache = new ConcurrentHashMap<>();
// 性能阈值(ms)
private static final long PERFORMANCE_THRESHOLD = 100;
@Pointcut("execution(* com.feixiang.student.service.*.*(..))")
public void serviceLayer() {}
// ========== 核心 @Around 切面:综合监控 ==========
@Around("serviceLayer()")
public Object monitor(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
Object[] args = joinPoint.getArgs();
// 1. 记录方法入参日志
System.out.printf("[飞翔科技-监控] %s.%s 开始执行,参数:%s%n",
className, methodName, Arrays.toString(args));
// 2. 性能计时开始
long start = System.currentTimeMillis();
// 3. 尝试从缓存获取(仅查询方法)
if (isQueryMethod(methodName)) {
String cacheKey = generateCacheKey(methodName, args);
Object cached = cache.get(cacheKey);
if (cached != null) {
System.out.println("[飞翔科技-监控] 缓存命中,直接返回");
return cached;
}
}
Object result;
try {
// 4. 执行目标方法(可传入修改后的参数)
result = joinPoint.proceed(args);
} catch (DataAccessException e) {
// 5. 异常转换:将数据库异常转为业务异常
System.err.println("[飞翔科技-监控] 数据库异常:" + e.getMessage());
throw new BusinessException(className + "." + methodName + " 执行失败,请稍后重试", e);
} catch (Exception e) {
// 6. 其他异常记录后重新抛出
System.err.println("[飞翔科技-监控] 业务异常:" + e.getMessage());
throw e;
}
// 7. 性能计时结束
long duration = System.currentTimeMillis() - start;
if (duration > PERFORMANCE_THRESHOLD) {
System.err.printf("[飞翔科技-监控] 性能警告:%s.%s 耗时 %dms,超过阈值 %dms%n",
className, methodName, duration, PERFORMANCE_THRESHOLD);
} else {
System.out.printf("[飞翔科技-监控] %s.%s 执行成功,耗时 %dms%n",
className, methodName, duration);
}
// 8. 查询结果放入缓存
if (isQueryMethod(methodName) && result != null) {
String cacheKey = generateCacheKey(methodName, args);
cache.put(cacheKey, result);
System.out.println("[飞翔科技-监控] 结果已缓存,key=" + cacheKey);
}
// 9. 记录方法出参日志
System.out.printf("[飞翔科技-监控] %s.%s 执行完成,返回值:%s%n",
className, methodName, result);
// 10. 可修改返回值(示例:给成绩加 5 分鼓励分)
if (result instanceof Score) {
Score score = (Score) result;
double adjusted = Math.min(score.getValue() + 5, 100);
score.setValue(adjusted);
System.out.println("[飞翔科技-监控] 成绩已调整:原=" + (adjusted - 5) + ", 现=" + adjusted);
return score;
}
return result;
}
// ========== @Around 修改参数示例 ==========
@Around("execution(* com.feixiang.student.service.*.queryScore(..))")
public Object normalizeQueryParam(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
// 修改参数:将负数学号转为正数
if (args[0] instanceof Long) {
Long studentId = (Long) args[0];
if (studentId != null && studentId < 0) {
args[0] = Math.abs(studentId);
System.out.println("[飞翔科技-监控] 参数修正:学号从 " + studentId + " 改为 " + args[0]);
}
}
// 使用修改后的参数执行目标方法
return joinPoint.proceed(args);
}
// ========== @Around 阻止方法执行示例 ==========
@Around("execution(* com.feixiang.student.service.*.deleteScore(..))")
public Object preventDelete(ProceedingJoinPoint joinPoint) throws Throwable {
// 模拟系统维护期间禁止删除操作
boolean isMaintenanceMode = true;
if (isMaintenanceMode) {
System.err.println("[飞翔科技-监控] 系统维护中,禁止删除成绩");
throw new BusinessException("系统维护中,暂不支持删除操作");
}
return joinPoint.proceed();
}
private boolean isQueryMethod(String methodName) {
return methodName.startsWith("query") || methodName.startsWith("get") || methodName.startsWith("find");
}
private String generateCacheKey(String methodName, Object[] args) {
return methodName + ":" + Arrays.toString(args);
}
}
第二步:纯净的业务 Service
@Service
public class StudentScoreService {
private final ScoreDao scoreDao;
public StudentScoreService(ScoreDao scoreDao) {
this.scoreDao = scoreDao;
}
public Score queryScore(Long studentId) {
// 纯粹的业务逻辑
System.out.println("[业务] 查询学生 " + studentId + " 的成绩");
return scoreDao.findByStudentId(studentId);
}
public List<Score> queryAllScores() {
System.out.println("[业务] 查询所有学生成绩");
return scoreDao.findAll();
}
public void updateScore(Long studentId, Double newScore) {
System.out.println("[业务] 更新学生 " + studentId + " 的成绩为 " + newScore);
scoreDao.updateScore(studentId, newScore);
}
public void deleteScore(Long studentId) {
System.out.println("[业务] 删除学生 " + studentId + " 的成绩");
scoreDao.deleteByStudentId(studentId);
}
}
第三步:配置类
@Configuration
@EnableAspectJAutoProxy(exposeProxy = true)
@ComponentScan("com.feixiang.student")
public class AppConfig {
}
操作后运行结果及分析
测试代码:
public class AroundDemo {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
StudentScoreService service = ctx.getBean(StudentScoreService.class);
// 测试 1:正常查询
System.out.println("=== 测试正常查询 ===");
Score score1 = service.queryScore(2024001L);
System.out.println("客户端收到结果:" + score1);
// 测试 2:缓存命中
System.out.println("\n=== 测试缓存命中 ===");
Score score2 = service.queryScore(2024001L);
System.out.println("客户端收到结果:" + score2);
// 测试 3:参数修正(负数转正)
System.out.println("\n=== 测试参数修正 ===");
Score score3 = service.queryScore(-2024002L);
System.out.println("客户端收到结果:" + score3);
// 测试 4:删除被阻止
System.out.println("\n=== 测试删除被阻止 ===");
try {
service.deleteScore(2024001L);
} catch (Exception e) {
System.err.println("客户端捕获异常:" + e.getMessage());
}
ctx.close();
}
}
控制台输出:
=== 测试正常查询 ===
[飞翔科技-监控] StudentScoreService.queryScore 开始执行,参数:[2024001]
[业务] 查询学生 2024001 的成绩
[飞翔科技-监控] StudentScoreService.queryScore 执行成功,耗时 15ms
[飞翔科技-监控] 结果已缓存,key=queryScore:[2024001]
[飞翔科技-监控] StudentScoreService.queryScore 执行完成,返回值:Score{studentId=2024001, value=88.5}
[飞翔科技-监控] 成绩已调整:原=88.5, 现=93.5
客户端收到结果:Score{studentId=2024001, value=93.5}
=== 测试缓存命中 ===
[飞翔科技-监控] StudentScoreService.queryScore 开始执行,参数:[2024001]
[飞翔科技-监控] 缓存命中,直接返回
客户端收到结果:Score{studentId=2024001, value=93.5}
=== 测试参数修正 ===
[飞翔科技-监控] StudentScoreService.queryScore 开始执行,参数:[-2024002]
[飞翔科技-监控] 参数修正:学号从 -2024002 改为 2024002
[业务] 查询学生 2024002 的成绩
[飞翔科技-监控] StudentScoreService.queryScore 执行成功,耗时 12ms
[飞翔科技-监控] 结果已缓存,key=queryScore:[2024002]
[飞翔科技-监控] StudentScoreService.queryScore 执行完成,返回值:Score{studentId=2024002, value=76.0}
[飞翔科技-监控] 成绩已调整:原=76.0, 现=81.0
客户端收到结果:Score{studentId=2024002, value=81.0}
=== 测试删除被阻止 ===
[飞翔科技-监控] StudentScoreService.deleteScore 开始执行,参数:[2024001]
[飞翔科技-监控] 系统维护中,禁止删除成绩
客户端捕获异常:系统维护中,暂不支持删除操作
分析:
@Around完全控制了目标方法的执行流程:日志、缓存、性能监控、异常转换、返回值修改全部在一个切面中完成joinPoint.proceed(args)使用修改后的参数执行目标方法,实现了参数修正- 缓存命中时直接返回,不调用
proceed(),目标方法不会执行 - 删除操作被阻止,通过抛出异常替代
proceed()调用 - 成绩被自动加了 5 分鼓励分,展示了返回值的修改能力
易错场景与面试考点
易错场景一:忘记调用 proceed() 导致目标方法不执行
反例:
@Around("serviceLayer()")
public Object forgetProceed(ProceedingJoinPoint joinPoint) {
System.out.println("方法开始");
// 错误!忘记调用 joinPoint.proceed()
System.out.println("方法结束");
return null; // 始终返回 null,目标方法从未执行
}
现象:调用任何 Service 方法都返回 null,业务逻辑完全不执行。
正确做法:
@Around("serviceLayer()")
public Object correctProceed(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("方法开始");
Object result = joinPoint.proceed(); // 必须调用!
System.out.println("方法结束");
return result;
}
易错场景二:proceed() 调用次数超过一次
反例:
@Around("serviceLayer()")
public Object multipleProceed(ProceedingJoinPoint joinPoint) throws Throwable {
// 错误!多次调用 proceed() 会导致目标方法执行多次
Object result1 = joinPoint.proceed();
Object result2 = joinPoint.proceed(); // 目标方法又执行了一次!
return result1;
}
现象:目标方法被执行两次,可能导致重复插入、重复扣款等严重问题。
正确做法:proceed() 通常只调用一次。若确实需要重试机制,应明确控制重试次数和条件:
@Around("serviceLayer()")
public Object retryProceed(ProceedingJoinPoint joinPoint) throws Throwable {
int maxRetries = 3;
for (int i = 0; i < maxRetries; i++) {
try {
return joinPoint.proceed();
} catch (TransientDataAccessException e) {
if (i == maxRetries - 1) throw e;
System.out.println("重试第 " + (i + 1) + " 次...");
}
}
return null;
}
易错场景三:@Around 方法签名错误
反例:
@Around("serviceLayer()")
public void wrongReturnType(ProceedingJoinPoint joinPoint) throws Throwable {
// 错误!返回类型为 void,但目标方法可能有返回值
joinPoint.proceed();
}
现象:如果目标方法返回非 void,代理返回 null,导致空指针异常。
正确做法:@Around 方法返回类型应为 Object,以兼容各种返回值类型:
@Around("serviceLayer()")
public Object correctReturnType(ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed();
}
经典面试考点:同类内部 this.method() 调用不触发 AOP 的问题及解决方案
这是 Spring AOP 面试中最高频、最核心的考点。
问题复现:
@Service
public class StudentScoreService {
public void batchUpdate(List<StudentScore> scores) {
for (StudentScore score : scores) {
// 危险!this.updateScore() 不会触发 AOP
this.updateScore(score.getStudentId(), score.getNewScore());
}
}
public void updateScore(Long studentId, Double newScore) {
// 此方法上有 @Around 权限校验和日志切面
System.out.println("[业务] 更新学生 " + studentId + " 的成绩");
}
}
原因分析:
Spring AOP 基于代理实现。当客户端通过容器获取 StudentScoreService 时,得到的是代理对象(CGLIB 子类或 JDK 代理)。代理对象在调用 updateScore() 时,会先执行 Advice 链,再调用目标对象的方法。
但在 batchUpdate() 内部,this 指向的是目标对象本身(StudentScoreService 原始实例),而不是代理对象。因此 this.updateScore() 直接调用了目标对象的方法,完全绕过了代理对象,Advice 链不会执行。
解决方案:
方案一:注入自身代理(推荐,简单直观)
@Service
public class StudentScoreService {
@Autowired
private StudentScoreService self;
public void batchUpdate(List<StudentScore> scores) {
for (StudentScore score : scores) {
// 通过代理对象调用,AOP 生效
self.updateScore(score.getStudentId(), score.getNewScore());
}
}
public void updateScore(Long studentId, Double newScore) {
System.out.println("[业务] 更新学生 " + studentId + " 的成绩");
}
}
注意:此方案要求 Spring 容器支持循环依赖(Spring Boot 2.x 默认支持)。如果项目关闭了循环依赖,此方案不可用。
方案二:使用 AopContext.currentProxy()(官方推荐,无循环依赖问题)
@Service
public class StudentScoreService {
public void batchUpdate(List<StudentScore> scores) {
for (StudentScore score : scores) {
// 获取当前代理对象,AOP 生效
((StudentScoreService) AopContext.currentProxy())
.updateScore(score.getStudentId(), score.getNewScore());
}
}
public void updateScore(Long studentId, Double newScore) {
System.out.println("[业务] 更新学生 " + studentId + " 的成绩");
}
}
配置类必须设置 exposeProxy = true:
@Configuration
@EnableAspectJAutoProxy(exposeProxy = true) // 必须开启!
@ComponentScan("com.feixiang.student")
public class AppConfig {
}
AopContext.currentProxy()从 ThreadLocal 中获取当前线程的代理对象,因此是线程安全的。但必须在代理方法内部调用,在@Async等异步方法中可能失效。
方案三:重构避免内部调用(最佳实践,从根本上解决问题)
将 updateScore 抽取到另一个 Service 中,通过依赖注入调用:
@Service
public class StudentScoreService {
private final ScoreUpdateHelper helper;
public StudentScoreService(ScoreUpdateHelper helper) {
this.helper = helper;
}
public void batchUpdate(List<StudentScore> scores) {
for (StudentScore score : scores) {
helper.updateScore(score.getStudentId(), score.getNewScore());
}
}
}
@Service
public class ScoreUpdateHelper {
public void updateScore(Long studentId, Double newScore) {
System.out.println("[业务] 更新学生 " + studentId + " 的成绩");
}
}
此方案从根本上消除了同类内部调用,AOP 自然生效,且代码职责更清晰。
面试高频考点
考点一:@Around 与 @Before + @After 的区别?什么时候必须用 @Around?
@Around完全包围目标方法,可以控制是否执行、修改参数、修改返回值、捕获异常。@Before+@After只能分别在方法前后执行逻辑,无法修改参数和返回值,也无法阻止方法执行。必须使用@Around的场景:① 需要修改传入参数;② 需要修改或替换返回值;③ 需要决定是否执行目标方法(如缓存命中直接返回);④ 需要捕获并处理异常(如重试、降级)。
考点二:ProceedingJoinPoint.proceed() 的作用?不调用会怎样?
proceed()是@Around中执行目标方法的关键调用。不调用proceed(),目标方法不会执行,代理直接返回@Around方法的返回值(通常为null)。proceed(Object[] args)可以传入修改后的参数数组,替换原始参数。多次调用proceed()会导致目标方法执行多次,可能引发重复操作。
考点三:同类内部 this.method() 为什么不触发 AOP?有哪些解决方案?
因为
this指向目标对象本身,而非 Spring 生成的代理对象。代理对象负责在方法调用前后插入 Advice,直接调用目标对象绕过了代理。解决方案:① 注入自身代理(@Autowired自己),通过代理对象调用;② 使用AopContext.currentProxy()获取当前代理(需exposeProxy = true);③ 重构代码,将方法抽取到另一个 Bean 中,通过依赖注入调用。方案三是最佳实践,从根本上避免同类内部调用。
考点四:@Around 方法的返回值有什么要求?
@Around方法的返回值会作为代理方法的返回值返回给客户端。因此返回类型应为Object,以兼容各种返回值类型。如果目标方法返回void,@Around可以返回null。如果@Around返回类型为void,但目标方法有返回值,客户端将收到null,可能导致空指针异常。