springboot使用@Async注解時異步方法不生效原因分析及解決方案


 

一、前言

  很多小伙伴在初次使用springboot框架@Async注解時,可能會發現明明在方法上添加了@Async注解,並且也在啟動類上添加了@EnableAsync注解,但是方法依舊沒有異步的去執行。

二、思考

  很大可能性是因為是在同一個類里面,一個方法去調用另外一個有@Async注解的方法,這種情況下異步方法是不會有效果的(@Transational也是同理)。

  除此之外,在使用springboot框架執行異步方法時,有以下幾點需要注意

  1. 必須在啟動類中增加@EnableAsync注解;
  2. 異步類沒有被springboot管理,在有異步方法的類上添加@Component注解(或其他注解)且保證可以掃描到異步類;
  3. 測試異步方法不能與異步方法在同一個類中;
  4. 測試類中需要使用spring容器初始化的異步類,不能自己手動new對象;

三、原因分析

  上面的1、2、4點大家都會注意到,但是經常會忽視第3點,那么為什么在同一個類里面,用一個方法去調用另外一個有@Async注解的方法,異步方法不會生效呢?

  原來,spring 在掃描bean的時候會掃描該類的方法上是否包含  @Async 注解,如果包含,spring會為這個bean動態地生成一個子類(即代理類 proxy),代理類是繼承原來那個bean的,並且重寫了父類中被 @Async 注解的方法(如果該注解是加在了類上,則會重寫該類的所有方法),並利用AOP切面為這些方法加上異步邏輯。
  此時,當這個有注解的方法被調用的時候,實際上調用的是代理類中重寫過的方法。然而,如果這個有注解的方法是被同一個類中的其他方法調用的,那么就不會調用該代理類了,而是直接通過當前對象去調,所以也就不生效了,所以我們看到的現象就是該異步方法沒有生效。
  具體實現原理可以看這篇文章  深入理解Spring系列之十五:@Async實現原理 
  為什么一個方法a()調用同一個類中另外一個方法b()的時候,b()不是通過代理類來調用的呢?可以看下面的例子(為了簡化,用偽代碼表示):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
class  A{
     @Async
     method b(){...}
     
     method a(){     //標記1
         b();
     }
}
  
 
//Spring掃描注解后,會創建另外一個代理類,並對添加注解的方法根據切入點創建代理
//調用代理,執行切入點處理器invoke方法,實現異步執行
class  proxy$A{
     A objectA =  new  A();
     method b(){     //標記2
         //MethodInterceptor.invoke
         objectA.b();
     }
  
     method a(){     //標記3
         objectA.a();     //由於a()沒有注解,所以不會創建代理,而是直接調用A的實例的a()方法
     }
}

  當我們調用A的bean的a()方法的時候,也是被proxy$A攔截,執行proxy$A.a()(標記3),然而,由以上代碼可知,這時候它調用的是objectA.a(),也就是由原來的bean來調用a()方法了,所以代碼跑到了“標記1”。由此可見,“標記2”並沒有被執行到,所以startTransaction()方法也沒有運行。

四、代碼實踐

  根據上面的注意點,我們可以使用以下四種情況進行測試

  • 1st:調用當前類中帶有@Async注解的方法...
  • 2nd:調用其他類Test類中帶有@Async注解的方法...
  • 3rd:調用其他類Test1類不帶@Async注解的方法...
  • 4th:調用其他類Test2類(沒有被springboot初始化的類)中帶有Async注解的方法...

代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Slf4j
@Component
public  class  TestAsync {
     @Autowired
     private  Test test;
     @Autowired
     private  Test1 test1;
     public  void  testCommon(){
 
         log.info( "主線程名:{}" +Thread.currentThread().getName());
         log.info( "1st:調用當前類中帶有@Async注解的方法..." );
         this .testAsync();
         log.info( "當前類名:{}" , this .getClass().getName());
         log.info( "===========================================" );
         log.info( "2nd:調用其他類Test類中帶有@Async注解的方法..." );
         test.testAsync();
         Class c= test.getClass();
         log.info( "當前類名:{}" ,c.getName());
         log.info( "Test類的父類:{}" ,c.getSuperclass().getName());
         log.info( "===========================================" );
         log.info( "3rd:調用其他類Test1類不帶@Async注解的方法..." );
         test1.test();
         log.info( "當前類名:{}" ,test1.getClass().getName());
         log.info( "===========================================" );
         log.info( "4th:調用其他類Test2類(沒有被springboot初始化的類)中帶有Async注解的方法..." );
         Test2 test2 =  new  Test2();
         test2.testAsync();
         log.info( "當前類名:{}" ,test2.getClass().getName());
     }
 
     @Async
     public  void  testAsync(){
         log.info( "當前類調用線程名:" +Thread.currentThread().getName());
     }
}
1
2
3
4
5
6
7
8
9
10
11
12
@Slf4j
@Component
public  class  Test {
     @Async
     public  void  testAsync(){
         log.info( "Test類線程名:" +Thread.currentThread().getName());
     }
 
     public  void  method(){
         log.info( "this is Test.method" );
     }
}
1
2
3
4
5
6
7
@Slf4j
@Component
public  class  Test1 {
     public  void  test() {
         log.info( "Test1類線程名:" +Thread.currentThread().getName());
     }
}
1
2
3
4
5
6
@Slf4j
public  class  Test2 {
     public  void  testAsync() {
         log.info( "Test2類線程名:" +Thread.currentThread().getName());
     }
}

測試結果如下:

 

 通過類名可以看出當前只有情況2有效的,它是使用的代理類,並且該代理類是目標類的子類。

五、總結

  這篇文章主要分析了在spingboot中可能導致@Async注解失敗的幾種情況以及如何解決。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM