SpringBoot :同一个类中调用另一个方法没有触发 Spring AOP

本文详细解析了Spring AOP的工作原理,特别是在同一类内部方法调用时AOP失效的问题及解决方案,同时探讨了如何正确应用@Transactional注解确保事务管理。

转载自:https://segmentfault.com/a/1190000008379179

最近做项目,没有时间写博客,后悔了,这博客都好几个月没更新,立个flag,最低一周一更,搬砖不能停止思考

一直没有触及AOP切面技术,偶然机会发现该博客,感觉写的很好,先记录,慢慢学习。

起因

考虑如下一个例子:

@Target(value = {ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyMonitor {
}
@Component
@Aspect
public class MyAopAdviseDefine {
    private Logger logger = LoggerFactory.getLogger(getClass());
 
    @Pointcut("@annotation(com.xys.demo4.MyMonitor)")
    public void pointcut() {
    }
 
    // 定义 advise
    @Before("pointcut()")
    public void logMethodInvokeParam(JoinPoint joinPoint) {
        logger.info("---Before method {} invoke, param: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs());
    }
}
@Service
public class SomeService {
    private Logger logger = LoggerFactory.getLogger(getClass());
 
    public void hello(String someParam) {
        logger.info("---SomeService: hello invoked, param: {}---", someParam);
        test();
    }
 
    @MyMonitor
    public void test() {
        logger.info("---SomeService: test invoked---");
    }
}
@EnableAspectJAutoProxy(proxyTargetClass = true)
@SpringBootAppliMyion
public class MyAopDemo {
    @Autowired
    SomeService someService;
 
    public static void main(String[] args) {
        SpringAppliMyion.run(MyAopDemo.class, args);
    }
 
    @PostConstruct
    public void aopTest() {
        someService.hello("abc");
    }
}

       在这个例子中, 我们定义了一个注解 MyMonitor, 这个是一个方法注解, 我们的期望是当有此注解的方法被调用时, 需要执行指定的切面逻辑, 即执行 MyAopAdviseDefine.logMethodInvokeParam 方法.

        在 SomeService 类中, 方法 test() 被 MyMonitor 所注解, 因此调用 test() 方法时, 应该会触发 logMethodInvokeParam 方法的调用.不过有一点我们需要注意到, 我们在 MyAopDemo 测试例子中, 并没有直接调用 SomeService.test() 方法, 而是调用了 SomeService.hello() 方法, 在 hello 方法中, 调用了同一个类内部的 SomeService.test() 方法. 按理说, test() 方法被调用时, 会触发 AOP 逻辑, 但是在这个例子中, 我们并没有如愿地看到 MyAopAdviseDefine.logMethodInvokeParam 方法的调用, 这是为什么呢?

       这是由于 Spring AOP (包括动态代理和 CGLIB 的 AOP) 的限制导致的. Spring AOP 并不是扩展了一个类(目标对象), 而是使用了一个代理对象来包装目标对象, 并拦截目标对象的方法调用. 这样的实现带来的影响是: 在目标对象中调用自己类内部实现的方法时, 这些调用并不会转发到代理对象中, 甚至代理对象都不知道有此调用的存在.

       即考虑到上面的代码中, 我们在 MyAopDemo.aopTest() 中, 调用了 someService.hello("abc"), 这里的 someService bean 其实是 Spring AOP 所自动实例化的一个代理对象, 当调用 hello() 方法时, 先进入到此代理对象的同名方法中, 然后在代理对象中执行 AOP 逻辑(因为 hello 方法并没有注入 AOP 横切逻辑, 因此调用它不会有额外的事情发生), 当代理对象中执行完毕横切逻辑后, 才将调用请求转发到目标对象的 hello() 方法上. 因此当代码执行到 hello() 方法内部时, 此时的 this 其实就不是代理对象了, 而是目标对象, 因此再调用 SomeService.test() 自然就没有 AOP 效果了.

简单来说, 在 MyAopDemo 中所看到的 someService 这个 bean 和在 SomeService.hello() 方法内部上下文中的 this 其实代表的不是同一个对象(可以通过分别打印两者的 hashCode 以验证), 前者是 Spring AOP 所生成的代理对象, 而后者才是真正的目标对象(SomeService 实例).

解决

弄懂了上面的分析, 那么解决这个问题就十分简单了. 既然 test() 方法调用没有触发 AOP 逻辑的原因是因为我们以目标对象的身份(target object) 来调用的, 那么解决的关键自然就是以代理对象(proxied object)的身份来调用 test() 方法.
因此针对于上面的例子, 我们进行如下修改即可:

@Service
public class SomeService {
    private Logger logger = LoggerFactory.getLogger(getClass());
 
    @Autowired
    private SomeService self;
 
    public void hello(String someParam) {
        logger.info("---SomeService: hello invoked, param: {}---", someParam);
        self.test();
    }
 
    @CatMonitor
    public void test() {
        logger.info("---SomeService: test invoked---");
    }
}

上面展示的代码中, 我们使用了一种很 subtle 的方式, 即将 SomeService bean 注入到 self 字段中(这里再次强调的是, SomeService bean 实际上是一个代理对象, 它和 this 引用所指向的对象并不是同一个对象), 因此我们在 hello 方法调用中, 使用 self.test() 的方式来调用 test() 方法, 这样就会触发 AOP 逻辑了.

Spring AOP 导致的 @Transactional 不生效的问题

这个问题同样地会影响到 @Transactional 注解的使用, 因为 @Transactional 注解本质上也是由 AOP 所实现的.

例如我在 stackoverflow 上看到的一个类似的问题: Spring @Transaction method call by the method within the same class, does not work?
这里也记录下来以作参考.

那个哥们遇到的问题如下:

public class UserService {
 
    @Transactional
    public boolean addUser(String userName, String password) {
        try {
            // call DAO layer and adds to database.
        } catch (Throwable e) {
            TransactionAspectSupport.currentTransactionStatus()
                    .setRollbackOnly();
 
        }
    }
 
    public boolean addUsers(List<User> users) {
        for (User user : users) {
            addUser(user.getUserName, user.getPassword);
        }
    } 
}

他在 addUser 方法上使用 @Transactional 来使用事务功能, 然后他在外部服务中, 通过调用 addUsers 方法批量添加用户. 经过了上面的分析后, 现在我们就可知道其实这里添加注解是不会启动事务功能的, 因为 AOP 逻辑整个都没生效嘛.

解决这个问题的方法有两个, 一个是使用 AspectJ 模式的事务实现:

<tx:annotation-driven mode="aspectj"/>

另一个就是和我们刚才在上面的例子中的解决方式一样:

 
public class UserService {
    private UserService self;
 
    public void setSelf(UserService self) {
        this.self = self;
    }
 
    @Transactional
    public boolean addUser(String userName, String password) {
        try {
        // call DAO layer and adds to database.
        } catch (Throwable e) {
            TransactionAspectSupport.currentTransactionStatus()
                .setRollbackOnly();
 
        }
    }
 
    public boolean addUsers(List<User> users) {
        for (User user : users) {
            self.addUser(user.getUserName, user.getPassword);
        }
    } 
}
<think>好的,用户问的是在Spring Cloud的控制层中如何调用同一个的异步方法。首先,我需要回忆一下Spring框架中异步方法调用的机制。通常,我们会使用@Async注解来标记一个方法,使其异步执行。但这里有个问题,如果直接在一个内部调用一个异步方法,由于SpringAOP代理机制,直接调用中的方法可能会导致@Async失效,因为代理对象没有被使用。 接下来,我需要确认用户是否了解@Async的基本用法以及可能的陷阱。用户可能已经知道如何在不同的之间调用异步方法,但问题是在同一个调用。这时候,可能需要通过自注入的方式获取代理对象,或者调整方法的结构,使得异步方法被正确代理。 然后,我需要考虑可能的解决方案。常见的做法包括:1. 将异步方法移到一个Bean中,然后通过依赖注入调用。2. 使用AopContext.currentProxy()来获取当前的代理对象,再调用异步方法。不过,这种方法需要启用exposeProxy配置。3. 自注入,即在自己的中注入自己的实例,通过这个实例调用方法外,需要提醒用户配置@EnableAsync,并确保异步执行器已经正确配置,避免使用默认的简单执行器,可能导致线程资源不足。同时,可能需要处理事务管理的问题,如果异步方法涉及事务的话。 可能用户在使用过程中遇到了异步方法没有生效的情况,所以需要详细说明步骤和可能出错的点。例如,确保启动上有@EnableAsync,异步方法所在的必须是一个Spring管理的Bean,方法必须是public的,且不能在同一个中直接调用。 总结下来,回答应该分步骤说明如何正确实现同一中的异步方法调用,包括自注入的方法和可能的配置调整,同时指出常见的问题和解决方案。</think>在Spring Cloud控制层中调用异步方法需解决自调用失效问题,以下是具体步骤和注意事项: 1. **基础配置** ```java @SpringBootApplication @EnableAsync // 必须启用异步支持 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` 2. **典型错误示例(异步失效)** ```java @RestController public class MyController { public void syncMethod() { this.asyncMethod(); // 直接调用方法会导致@Async失效 } @Async public void asyncMethod() { // 异步逻辑 } } ``` 3. **正确解决方案(三种方式)** **方案一:自注入调用(推荐)** ```java @RestController public class MyController { @Autowired // 注入自身代理对象 private MyController selfProxy; public ResponseEntity<String> syncMethod() { selfProxy.asyncMethod(); // 通过代理对象调用 return ResponseEntity.ok("已触发异步方法"); } @Async public void asyncMethod() { // 执行耗时操作,例如: IntStream.range(1, 5).forEach(i -> { try { Thread.sleep(1000); System.out.println("异步执行进度:" + i + "/4"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } } ``` **方案二:通过ApplicationContext获取代理** ```java @Autowired private ApplicationContext context; public void syncMethod() { context.getBean(MyController.class).asyncMethod(); } ``` **方案三:启用AOP代理暴露(需配置)** ```java @EnableAspectJAutoProxy(exposeProxy = true) // 添加到启动 // 调用方式 public void syncMethod() { ((MyController)AopContext.currentProxy()).asyncMethod(); } ``` 4. **关键配置项** ```properties # 配置自定义线程池(避免默认SimpleAsyncTaskExecutor问题) spring.task.execution.pool.core-size=5 spring.task.execution.pool.max-size=10 spring.task.execution.pool.queue-capacity=100 spring.task.execution.thread-name-prefix=Async-Executor- ``` 5. **注意事项** - 异步方法必须为public方法 - 避免在@PostConstruct方法中直接调用异步方法 - 异步方法返回值为void或Future型 - 不同调用时可直接@Autowired注入调用 - 需要处理异步方法中的异常(建议实现AsyncUncaughtExceptionHandler) 6. **执行效果验证** ```java @Async public void asyncMethod() { System.out.println("当前线程: " + Thread.currentThread().getName()); // 输出示例:Async-Executor-1 (而非默认的简单线程) } ``` 建议优先采用自注入方案,这是Spring团队推荐的标准解决方案。如遇到循环依赖问题,可配合@Lazy注解解决: ```java @Autowired @Lazy // 延迟注入解决循环依赖 private MyController selfProxy; ```
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值