一、前言
很多小伙伴在初次使用springboot框架@Async注解時,可能會發現明明在方法上添加了@Async注解,並且也在啟動類上添加了@EnableAsync注解,但是方法依舊沒有異步的去執行。
二、思考
很大可能性是因為是在同一個類里面,一個方法去調用另外一個有@Async注解的方法,這種情況下異步方法是不會有效果的(@Transational也是同理)。
除此之外,在使用springboot框架執行異步方法時,有以下幾點需要注意
- 必須在啟動類中增加@EnableAsync注解;
- 異步類沒有被springboot管理,在有異步方法的類上添加@Component注解(或其他注解)且保證可以掃描到異步類;
- 測試異步方法不能與異步方法在同一個類中;
- 測試類中需要使用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注解失敗的幾種情況以及如何解決。
