@Async注解的使用 及@Async 注解失效问题的分析与解决方案

在开发过程中,我们会遇到很多使用线程池的业务场景,例如异步短信通知、异步记录操作日志。大多数使用线程池的场景,就是会将一些可以进行异步操作的业务放在线程池中去完成。

例如在生成订单的时候给用户发送短信,生成订单的结果不应该被发送短信的成功与否所左右,也就是说生成订单这个主操作是不依赖于发送短信这个操作,所以我们就可以把发送短信这个操作置为异步操作。

那么本文就是来看看Spring中提供的优雅的异步处理方案:在Spring3中,Spring中引入了一个新的注解@Async,这个注解让我们在使用Spring完成异步操作变得非常方便。作配置,使用@Async即可实现。

@Async的使用

但让我们来实现一个小的demo:

注意在启动类要加上 @EnableAsync 注解;

@RestController
@Slf4j
public class AsyncTestController {
    @GetMapping("test")
    public void testAsync() throws InterruptedException {
        log.info("=====主线程执行: " + Thread.currentThread().getName());
        this.doAsync01();
        this.doAsync02();
        log.info("=====主线程执行: " + Thread.currentThread().getName());
    }

    @Async
    public void doAsync01() throws InterruptedException {
        Thread.sleep(3000);
        log.info("=====子线程执行: " + Thread.currentThread().getName());
    }

    @Async
    public void doAsync02() {
        log.info("=====子线程执行: " + Thread.currentThread().getName());
    }
}

执行结果如下: 

从线程名和执行时间可以看出,@Async注解并没有起到预期的作用,只不过是串行的单行线程罢了。 

发现问题:@Async注解不起作用

但是,当我们把异步方法放到另一个类中:

@RestController
@Slf4j
public class AsyncTestController {
    @Autowired
    private AsyncService asyncService;

    @GetMapping("test")
    public void testAsync() throws InterruptedException {
        log.info("=====主线程执行: " + Thread.currentThread().getName());
        asyncService.doAsync01();
        asyncService.doAsync02();
        log.info("=====主线程执行: " + Thread.currentThread().getName());
    }
}
@Service
@Slf4j
public class AsyncService {
    @Async("defaultTaskExecutor")
    public void doAsync01() throws InterruptedException {
        Thread.sleep(3000);
        log.info("=====子线程1执行: " + Thread.currentThread().getName());
    }

    @Async
    public void doAsync02() {
        log.info("=====子线程2执行: " + Thread.currentThread().getName());
    }
}

执行结果如下:

有次可见 @Async注解是有用的,上面只是我们使用的方式不正确。但是这是为什么呢?

问题分析

这个问题与我们以前遇到的一个问题十分的相似,不知道大家以前可遇到过@Transactional 注解有时候也会失效的情况,@Transactional注解失效是因为 它是基于AOP的,而AOP又是基于动态代理实现的,@Async注解也是一样的,在我们后面的例子中,真正调用 doAsync01 和 doAsync02 方法的是 AsyncService 的代理对象。其实Spring容器在初始化的时候Spring容器会将含有AOP注解的类对象“替换”为代理对象(简单这么理解),那么注解失效的原因就很明显了,就是因为调用方法的是对象本身而不是代理对象,因为没有经过Spring容器,那么解决方法也会沿着这个思路来解决。

解决方案

我们上面第二个例子的解决方法就是将要异步执行的方法单独抽取成一个类,这样的确可以解决异步注解失效的问题,原理就是当把执行异步的方法单独抽取成一个类的时候,这个类肯定是被Spring管理的,其他Spring组件需要调用的时候肯定会注入进去,这时候实际上注入进去的就是代理类了,其实还有其他的解决方法,并不一定非要单独抽取成一个类。

其他的解决方案:

  • 在AsyncTestController 中通过上下文获取自己的代理对象调用异步方法
    其实我们的注入对象都是从Spring容器中给当前Spring组件进行成员变量的赋值,由于TestService使用了AOP注解,那么实际上TestService在Spring容器中实际存在的是它的代理对象。

代码实现如下:

@RestController
@Slf4j
public class AsyncTestController {

    @GetMapping("test")
    public void testAsync() throws InterruptedException {
        log.info("=====主线程执行: " + Thread.currentThread().getName());
        AsyncTestController asyncTestController = SpringUtils.getBean(AsyncTestController.class);
        asyncTestController.doAsync01();
        asyncTestController.doAsync02();
        log.info("=====主线程执行: " + Thread.currentThread().getName());
    }

    @Async("defaultTaskExecutor")
    public void doAsync01() throws InterruptedException {
        Thread.sleep(3000);
        log.info("=====子线程1执行: " + Thread.currentThread().getName());
    }

    @Async("defaultTaskExecutor")
    public void doAsync02() {
        log.info("=====子线程2执行: " + Thread.currentThread().getName());
    }
}

SpringUtils工具类: 

@Component("springContextUtil")
public class SpringUtils implements ApplicationContextAware {


    private static ApplicationContext applicationContext = null;

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    @SuppressWarnings("unchecked")
    public static  T getBean(String beanId) {
        return (T) applicationContext.getBean(beanId);
    }

    public static  T getBean(Class requiredType) {
        return (T) applicationContext.getBean(requiredType);
    }

    /**
     * Spring容器启动后,会把 applicationContext 给自动注入进来,然后我们把 applicationContext
     * 赋值到静态变量中,方便后续拿到容器对象
     *
     * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext)
     */
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringUtils.applicationContext = applicationContext;
    }
}

 

执行结果如下:成功

  • 开启cglib代理手动获取spring代理类:

在启动类加上@EnableAspectJAutoProxy(exposeProxy = true) 注解,

使用AopContext.currentProxy()获取当前代理类:

这里为了证明Spring容器中的对象就是当前代理类对象特地输出了一句话:

@RestController
@Slf4j
public class AsyncTestController {

    @GetMapping("test")
    public void testAsync() throws InterruptedException {
        log.info("=====主线程执行: " + Thread.currentThread().getName());
        
        AsyncTestController currentProxy = (AsyncTestController) AopContext.currentProxy();
        currentProxy.doAsync01();
        currentProxy.doAsync02();
        
        log.info("=====主线程执行: " + Thread.currentThread().getName());
    }

    @Async("defaultTaskExecutor")
    public void doAsync01() throws InterruptedException {
        Thread.sleep(3000);
        log.info("=====子线程1执行: " + Thread.currentThread().getName());
    }

    @Async("defaultTaskExecutor")
    public void doAsync02() {
        log.info("=====子线程2执行: " + Thread.currentThread().getName());
    }
}

执行结果如下:

并没有像上面的方法一样成功,反而报错了 ,但是@EnableAspectJAutoProxy(exposeProxy = true)注解命名就加了呀,为啥还会报 java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.呢? 详见:https://blog.csdn.net/weixin_40910372/article/details/103565970。

@Async注解失败的常见场景

好了,我们回到现在的问题上来,从网上找了很多关于会导致@Async注解失败的原因,这里总结一下,希望能帮到大家。

  • 没有在@SpringBootApplication启动类当中添加注解@EnableAsync注解。
  • 异步方法使用注解@Async的返回值只能为void或者Future。
  • 没有走Spring的代理类。

其中的后两种是我们比较常见的失误了。
解决方法如下:

1.注解的方法必须是public方法。
2.方法一定要从另一个类中调用,也就是从类的外部调用,类的内部调用是无效的。
3.如果需要从类的内部调用,需要先获取其代理类,通过代理类调用异步方法。

@Async注解使用细节

  • @Async注解一般用在方法上,如果用在类上,那么这个类所有的方法都是异步执行的;

  • @Async可以放在任何方法上,哪怕你是private的(若是同类调用,请务必注意注解失效的情况~~~)

  • 所使用的@Async注解方法的类对象应该是Spring容器管理的bean对象

  • @Async可以放在接口处(或者接口方法上)。但是只有使用的是JDK的动态代理时才有效,CGLIB会失效。因此建议:统一写在实现类的方法上

  • 需要注解@EnableAsync来开启异步注解的支持

  • 若你希望得到异步调用的返回值,请你的返回值用Futrue变量包装起来 

你可能感兴趣的:(线程,@Async失效)