乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 Spring概述与IoC容器

    • Spring概述与IoC容器
    • Spring Framework 概述
    • IoC 与 DI 核心概念
    • @Configuration 详解
    • @Component 详解
    • @ComponentScan 详解
    • @Import 详解
    • @Profile 详解
    • @PropertySource 详解
    • @Service 详解
    • @Repository 详解
  • 第2章 Bean的定义与依赖注入

    • Bean的定义与依赖注入
    • @Bean 详解
    • @Autowired 详解
    • @Qualifier 详解
    • @Primary 详解
    • @Resource 详解
    • @Inject 详解
    • @Named 详解
    • @Value 详解
    • @Scope 详解
    • @Lazy 详解
  • 第3章 Bean生命周期与作用域

    • Bean生命周期与作用域
    • Bean生命周期概述
    • @PostConstruct
    • @PreDestroy
    • InitializingBean
    • DisposableBean
    • BeanPostProcessor
    • BeanFactoryPostProcessor
  • 第4章 AOP面向切面编程

    • AOP面向切面编程
    • AOP核心概念
    • @EnableAspectJAutoProxy
    • @Aspect
    • @Pointcut
    • @Before
    • @After
    • @AfterReturning
    • @AfterThrowing
    • @Around
  • 第5章 数据访问与事务管理

    • 数据访问与事务管理
    • 数据访问概述
    • @EnableTransactionManagement
    • @Transactional
    • @Transactional 的传播行为
    • @Transactional 的隔离级别
    • @Transactional 的回滚规则
    • @Transactional 的超时与只读属性
    • @TransactionalEventListener
  • 第6章 Spring Boot自动配置基础

    • Spring Boot自动配置基础
    • @SpringBootApplication 注解
    • @EnableAutoConfiguration 注解
    • @ConfigurationProperties 注解
    • @ConditionalOnClass 注解
    • @ConditionalOnMissingBean 注解
    • @ConditionalOnProperty 注解
  • 第7章 从容器到Web: Spring MVC导引

    • Spring MVC 导引
  • 第8章 扩展阅读

    • 扩展阅读
    • Spring 事件机制 — ApplicationEvent / ApplicationListener
    • @EventListener
    • SpEL — Spring 表达式语言
    • 校验 Validation — JSR-303 / JSR-380 Bean Validation
    • 类型转换与数据绑定 — Converter / DataBinder
  • 附录

    • Spring Framework 专业术语
    • Spring 核心知识点
    • Spring 面试高频考点
    • Spring 核心注解速查表

@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) {
        // 返回后通知逻辑
    }
}

常用属性

属性类型说明
valueString切入点表达式,或引用 @Pointcut 方法名
pointcutString与 value 等价,用于替代 value 声明切点
returningString将目标方法的返回值绑定到 Advice 方法的同名参数。若目标方法返回 void,此参数值为 null
argNamesString参数名称,用于绑定目标方法参数(通常可省略)

核心原理

@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。

上一页
@After
下一页
@AfterThrowing