@AfterReturning
定义与作用
@AfterReturning 声明返回后通知(After Returning Advice),在目标方法成功返回后触发。与 @After 不同,@AfterReturning 只在方法正常返回时执行,如果方法抛出异常则不会执行。它的核心优势是可以访问目标方法的返回值,适用于结果缓存、返回值日志记录、数据后处理等场景。
关键特性:
- 只在目标方法正常返回时执行,抛出异常时不执行
- 可以通过
returning属性将返回值绑定到 Advice 方法的参数 - 可以修改返回值(但修改后代理返回的是修改后的对象,不影响目标方法内部)
- 无法访问异常对象(获取异常需使用
@AfterThrowing)
适用位置与常用属性
适用位置
@AfterReturning 标注在切面类的方法上,表示该方法是一个返回后通知。该方法必须位于 @Aspect 切面类内部。
@Aspect
@Component
public class CacheAspect {
@AfterReturning(pointcut = "execution(* com.feixiang.service.*.*(..))", returning = "result")
public void cacheResult(Object result) {
// 返回后通知逻辑
}
}
常用属性
| 属性 | 类型 | 说明 |
|---|---|---|
value | String | 切入点表达式,或引用 @Pointcut 方法名 |
pointcut | String | 与 value 等价,用于替代 value 声明切点 |
returning | String | 将目标方法的返回值绑定到 Advice 方法的同名参数。若目标方法返回 void,此参数值为 null |
argNames | String | 参数名称,用于绑定目标方法参数(通常可省略) |
核心原理
@AfterReturning 在代理链中的执行位置
@AfterReturning 在目标方法正常返回后、结果返回给客户端前执行:
returning 属性绑定机制
绑定规则:
returning = "result"表示将目标方法的返回值绑定到 Advice 方法中名为result的参数- Advice 方法中
result参数的类型必须与目标方法返回值类型兼容(或其父类/接口) - 如果目标方法返回
void,result参数值为null - 如果
returning指定的名称在 Advice 方法中找不到同名参数,启动时不会报错,但无法注入返回值
完整示例:飞翔科技学生管理系统结果缓存
场景简述
广州飞翔科技公司的后端开发小崔发现:学生成绩查询接口 queryScore 被频繁调用,但成绩数据更新频率很低。架构师白歌建议引入结果缓存,将查询结果缓存 5 分钟,减少数据库压力。小崔决定使用 @AfterReturning 实现"查询成功后将结果放入缓存"的逻辑。
操作前代码/配置
在未使用 @AfterReturning 之前,小崔在每个 Service 方法中手动处理缓存:
@Service
public class StudentScoreService {
private final ScoreDao scoreDao;
private final Map<String, Score> cache = new ConcurrentHashMap<>();
public StudentScoreService(ScoreDao scoreDao) {
this.scoreDao = scoreDao;
}
public Score queryScore(Long studentId) {
// 先查缓存
String key = "score:" + studentId;
Score cached = cache.get(key);
if (cached != null) {
System.out.println("[缓存] 命中缓存,学号:" + studentId);
return cached;
}
// 查数据库
Score score = scoreDao.findByStudentId(studentId);
// 放入缓存(重复!缓存逻辑侵入业务代码)
if (score != null) {
cache.put(key, score);
System.out.println("[缓存] 放入缓存,学号:" + studentId);
}
return score;
}
public void updateScore(Long studentId, Double newScore) {
// 业务逻辑
scoreDao.updateScore(studentId, newScore);
// 重复!更新后需要清缓存
String key = "score:" + studentId;
cache.remove(key);
System.out.println("[缓存] 清除缓存,学号:" + studentId);
}
}
问题:缓存逻辑散落在业务代码中,且每个查询方法都要重复编写缓存代码。如果白歌要求将缓存从本地 Map 改为 Redis,所有 Service 方法都要修改。
使用该注解的完整代码
第一步:定义缓存切面
@Aspect
@Component
public class CacheAspect {
// 模拟缓存(实际项目中使用 Redis 或 Caffeine)
private final Map<String, Object> cache = new ConcurrentHashMap<>();
@Pointcut("execution(* com.feixiang.student.service.*.query*(..))")
public void queryMethod() {}
@Pointcut("execution(* com.feixiang.student.service.*.get*(..))")
public void getMethod() {}
// 查询前先看缓存(使用 @Around 更合适,但此处演示 @AfterReturning 的缓存写入)
// 实际项目中,缓存读取通常用 @Around,缓存写入用 @AfterReturning
// @AfterReturning:查询成功后,将结果放入缓存
@AfterReturning(pointcut = "queryMethod() || getMethod()", returning = "result")
public void putToCache(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
// 生成缓存 key:方法名 + 参数
String key = generateCacheKey(methodName, args);
if (result != null) {
cache.put(key, result);
System.out.println("[飞翔科技-缓存] 放入缓存,key=" + key + ",value=" + result);
}
}
// @AfterReturning:记录查询结果日志
@AfterReturning(pointcut = "queryMethod()", returning = "result")
public void logQueryResult(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
if (result instanceof Score) {
Score score = (Score) result;
System.out.println("[飞翔科技-日志] 查询结果:学号=" + score.getStudentId() +
",成绩=" + score.getValue());
}
}
// 提供缓存读取方法(供其他切面或 Service 使用)
public Object getFromCache(String key) {
return cache.get(key);
}
public void evictCache(String key) {
cache.remove(key);
}
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);
}
}
第三步:配置类
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.feixiang.student")
public class AppConfig {
}
操作后运行结果及分析
测试代码:
public class AfterReturningDemo {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
StudentScoreService service = ctx.getBean(StudentScoreService.class);
CacheAspect cacheAspect = ctx.getBean(CacheAspect.class);
// 第一次查询:数据库查询,结果放入缓存
System.out.println("=== 第一次查询 ===");
Score score1 = service.queryScore(2024001L);
System.out.println("返回结果:" + score1);
// 验证缓存
System.out.println("\n=== 验证缓存 ===");
Object cached = cacheAspect.getFromCache("queryScore:[2024001]");
System.out.println("缓存中的值:" + cached);
// 第二次查询:模拟从缓存读取(实际项目中用 @Around 实现)
System.out.println("\n=== 第二次查询 ===");
Score score2 = service.queryScore(2024001L);
System.out.println("返回结果:" + score2);
// 测试查询所有
System.out.println("\n=== 查询所有 ===");
service.queryAllScores();
ctx.close();
}
}
控制台输出:
=== 第一次查询 ===
[业务] 查询学生 2024001 的成绩
[飞翔科技-缓存] 放入缓存,key=queryScore:[2024001],value=Score{studentId=2024001, value=88.5}
[飞翔科技-日志] 查询结果:学号=2024001,成绩=88.5
返回结果:Score{studentId=2024001, value=88.5}
=== 验证缓存 ===
缓存中的值:Score{studentId=2024001, value=88.5}
=== 第二次查询 ===
[业务] 查询学生 2024001 的成绩
[飞翔科技-缓存] 放入缓存,key=queryScore:[2024001],value=Score{studentId=2024001, value=88.5}
[飞翔科技-日志] 查询结果:学号=2024001,成绩=88.5
返回结果:Score{studentId=2024001, value=88.5}
=== 查询所有 ===
[业务] 查询所有学生成绩
[飞翔科技-缓存] 放入缓存,key=queryAllScores:[],value=[Score{...}, Score{...}]
分析:
@AfterReturning在queryScore成功返回后,自动将结果放入缓存returning = "result"将返回值绑定到 Advice 方法的result参数- 业务代码完全纯净,缓存逻辑统一由切面维护
- 若需将缓存改为 Redis,只需修改
CacheAspect,无需触碰StudentScoreService
易错场景与面试考点
易错场景一:returning 属性名称与 Advice 方法参数名不一致
反例:
@AfterReturning(pointcut = "queryMethod()", returning = "result")
public void logResult(Object score) { // 错误!参数名是 score,不是 result
System.out.println("返回值:" + score); // score 始终为 null
}
现象:score 参数始终为 null,无法获取返回值。
原因:returning = "result" 要求 Advice 方法中必须有名为 result 的参数,Spring 通过参数名匹配进行绑定。如果名称不一致,绑定失败,参数值为 null。
正确做法:
@AfterReturning(pointcut = "queryMethod()", returning = "result")
public void logResult(Object result) { // 正确:参数名与 returning 一致
System.out.println("返回值:" + result);
}
注意:需要编译时开启
-parameters选项保留参数名,否则 Spring 无法通过反射获取参数名。或者显式使用argNames = "joinPoint,result"指定。
易错场景二:@AfterReturning 中修改返回值但类型不匹配
反例:
@AfterReturning(pointcut = "queryMethod()", returning = "result")
public void modifyResult(Score result) {
// 错误!这不会修改代理返回的值
result = new Score(9999L, 100.0); // 修改局部引用,不影响返回值
}
正确做法:若需修改返回值,应修改原对象属性或返回新对象(但 @AfterReturning 是 void 方法,无法返回新值)。真正修改返回值需使用 @Around:
@Around("queryMethod()")
public Object modifyResult(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = joinPoint.proceed();
if (result instanceof Score) {
Score score = (Score) result;
score.setValue(score.getValue() + 5); // 修改对象属性
}
return result;
}
面试高频考点
考点一:@AfterReturning 和 @After 的区别?
@AfterReturning只在目标方法正常返回时执行,可以访问返回值;@After无论正常返回还是抛出异常都会执行,但无法访问返回值。如果方法抛出异常,@AfterReturning不执行而@After执行。两者都用于后置处理,但@AfterReturning专注于成功场景的数据处理,@After专注于必须执行的清理工作。
考点二:@AfterReturning 的 returning 属性有什么作用?
returning属性将目标方法的返回值绑定到 Advice 方法的同名参数。例如returning = "result"要求 Advice 方法声明Object result参数,Spring 在运行时将目标方法的返回值注入该参数。如果目标方法返回void,该参数值为null。绑定失败时参数值也为null,不会抛异常。
考点三:@AfterReturning 能否阻止方法返回?能否修改返回值?
@AfterReturning无法阻止方法返回,因为它在目标方法已经执行完毕后触发。它可以修改返回值对象的内部状态(如修改对象属性),但由于 Advice 方法是void返回类型,无法替换整个返回值对象。若要完全控制返回值或阻止返回,必须使用@Around。