是不是遇到過這種情況:你給一個方法加上了@Async 註解,期待它能異步執行,結果發現它還是同步執行的?更困惑的是,同樣的註解在其他地方卻能正常工作。這個問題困擾了很多 Java 開發者,尤其是當你在同一個類中調用帶有@Async 註解的方法時。今天,我們就來深入解析這個問題的原因,並提供多種實用的解決方案。
Spring @Async 的正常工作原理
在討論內部調用問題前,我們先了解一下@Async 註解的基本工作原理。
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
// 簡單的用户類
class User {
private String email;
private String name;
// 默認構造器(Spring Bean實例化需要)
public User() {}
public User(String email, String name) {
this.email = email;
this.name = name;
}
public String getEmail() { return email; }
public String getName() { return name; }
public void setEmail(String email) { this.email = email; }
public void setName(String name) { this.name = name; }
}
@Service
public class EmailService {
@Async
public void sendEmail(String to, String content) {
// 耗時的郵件發送邏輯
System.out.println("發送郵件中... 當前線程: " + Thread.currentThread().getName());
}
}
@Service
public class UserService {
@Autowired
private EmailService emailService;
public void registerUser(User user) {
// 用户註冊邏輯
System.out.println("註冊用户中... 當前線程: " + Thread.currentThread().getName());
// 異步發送歡迎郵件
emailService.sendEmail(user.getEmail(), "歡迎註冊!");
// 註冊完成,立即返回
System.out.println("註冊完成!");
}
}
Spring @Async 的工作原理如下:
Spring 通過 AOP 代理實現@Async 功能。當一個方法被@Async 註解標記時,Spring 會創建一個代理對象。當外部代碼調用該方法時,調用實際上首先被代理對象攔截,然後代理將任務提交到線程池異步執行。
Spring 默認對實現接口的類使用 JDK 動態代理,對非接口類使用 CGLIB 代理。但無論哪種代理,重要的是調用必須經過代理對象,才能觸發@Async 的處理邏輯。
內部調用問題
問題出現在同一個類中調用自己的@Async 方法時:
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class NotificationService {
public void notifyAll(List<User> users, String message) {
System.out.println("開始通知所有用户... 當前線程: " + Thread.currentThread().getName());
for (User user : users) {
// 調用同一個類中的@Async方法
sendNotification(user, message); // 問題:這裏變成了同步調用!
}
System.out.println("通知流程初始化完成!"); // 實際要等所有通知發送完才會執行到這裏
}
@Async
public void sendNotification(User user, String message) {
// 模擬耗時操作
try {
System.out.println("正在發送通知給" + user.getName() +
"... 當前線程: " + Thread.currentThread().getName());
Thread.sleep(1000); // 模擬耗時操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
上面的代碼中,雖然sendNotification方法標記了@Async,但當在notifyAll方法中調用它時,它還是會同步執行,這不是我們預期的行為。
為什麼內部調用會失效?
內部調用失效的核心原因是:Spring 的 AOP 是基於代理實現的,而內部方法調用會繞過代理機制。
當你在一個類中直接調用同一個類的方法時(即使用this.method()或簡單的method()),這種調用是通過 Java 的常規方法調用機制直接執行的,完全繞過了 Spring 創建的代理對象。沒有經過代理,@Async 註解就無法被識別和處理,因此方法會按普通方法同步執行。
從源碼角度看,Spring 通過AsyncAnnotationBeanPostProcessor處理帶有@Async 註解的方法,創建代理對象。當方法調用經過代理時,代理會檢測註解並將任務提交給配置的TaskExecutor(Spring 用於執行異步任務的核心接口,提供線程池管理等功能)。內部調用直接執行原始方法,根本不經過這個處理流程。
五種解決方案
方案 1:自我注入(Self-Injection)
最簡單的方法是在類中注入自己:
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
@Service
public class NotificationService {
@Autowired
private NotificationService self; // 注入自己的代理對象
public void notifyAll(List<User> users, String message) {
System.out.println("開始通知所有用户... 當前線程: " + Thread.currentThread().getName());
for (User user : users) {
// 通過自注入的引用調用@Async方法
self.sendNotification(user, message); // 現在是異步調用!
}
System.out.println("通知流程初始化完成!"); // 立即執行,不等待通知完成
}
@Async
public void sendNotification(User user, String message) {
// 實現同前...
}
}
工作原理:當 Spring 注入self字段時,它實際上注入的是一個代理對象,而不是原始對象。通過代理調用方法,確保@Async 註解能被正確處理。
優點:
- 實現簡單,僅需添加一個自引用字段,無需修改方法邏輯
- 不改變原有的類結構
缺點:
- 可能導致循環依賴問題(不過 Spring 通常能處理這類循環依賴)
- 代碼看起來可能有點奇怪,自注入不是一種常見模式
- 如果服務類需要序列化,代理對象可能導致序列化問題
方案 2:使用 ApplicationContext 獲取代理對象
通過 Spring 的 ApplicationContext 手動獲取代理對象:
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import java.util.List;
@Service
public class NotificationService {
@Autowired
private ApplicationContext applicationContext;
public void notifyAll(List<User> users, String message) {
System.out.println("開始通知所有用户... 當前線程: " + Thread.currentThread().getName());
// 獲取代理對象
NotificationService proxy = applicationContext.getBean(NotificationService.class);
for (User user : users) {
// 通過代理對象調用@Async方法
proxy.sendNotification(user, message); // 異步調用成功
}
System.out.println("通知流程初始化完成!");
}
@Async
public void sendNotification(User user, String message) {
// 實現同前...
}
}
工作原理:從 ApplicationContext 獲取的 bean 總是代理對象(如果應該被代理的話)。通過這個代理調用方法會觸發所有 AOP 切面,包括@Async。
優點:
- 清晰明瞭,顯式獲取代理對象
- 不需要添加額外的字段
缺點:
- 增加了對 ApplicationContext 的依賴
- 每次調用前都需要獲取 bean,略顯冗餘
方案 3:使用 AopContext 獲取代理對象
利用 Spring AOP 提供的工具類獲取當前代理:
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.aop.framework.AopContext;
import java.util.List;
@Configuration
@EnableAsync
@EnableAspectJAutoProxy(exposeProxy = true) // 重要:暴露代理對象
public class AsyncConfig {
// 異步配置...
}
@Service
public class NotificationService {
public void notifyAll(List<User> users, String message) {
System.out.println("開始通知所有用户... 當前線程: " + Thread.currentThread().getName());
// 獲取當前代理對象
NotificationService proxy = (NotificationService) AopContext.currentProxy();
for (User user : users) {
// 通過代理對象調用@Async方法
proxy.sendNotification(user, message); // 異步調用成功
}
System.out.println("通知流程初始化完成!");
}
@Async
public void sendNotification(User user, String message) {
// 實現同前...
}
}
工作原理:Spring AOP 提供了AopContext.currentProxy()方法來獲取當前的代理對象。調用方法時,使用這個代理對象而不是this。
注意事項:必須在配置中設置@EnableAspectJAutoProxy(exposeProxy = true)來暴露代理對象,否則會拋出異常。
優點:
- 無需注入其他對象
- 代碼清晰,直接使用 AOP 上下文
缺點:
- 需要顯式配置
exposeProxy = true - 依賴 Spring AOP 的特定 API
方案 4:拆分為單獨的服務類
將異步方法拆分到單獨的服務類中:
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
@Service
public class AsyncNotificationService {
@Async
public void sendNotification(User user, String message) {
// 模擬耗時操作
try {
System.out.println("正在發送通知給" + user.getName() +
"... 當前線程: " + Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
@Service
public class NotificationService {
@Autowired
private AsyncNotificationService asyncService;
public void notifyAll(List<User> users, String message) {
System.out.println("開始通知所有用户... 當前線程: " + Thread.currentThread().getName());
for (User user : users) {
// 調用專門的異步服務
asyncService.sendNotification(user, message); // 正常異步調用
}
System.out.println("通知流程初始化完成!");
}
}
工作原理:將需要異步執行的方法移動到專門的服務類中,然後通過依賴注入使用這個服務。這樣,調用總是通過 Spring 代理對象進行的。
優點:
- 符合單一職責原則,代碼組織更清晰
- 避免了所有與代理相關的問題
- 可以更好地對異步操作進行組織和管理
- 更符合依賴倒置原則,便於單元測試和模擬測試
缺點:
- 需要創建額外的類
- 可能導致類的數量增加
方案 5:手動使用 TaskExecutor
完全放棄@Async 註解,手動使用 Spring 的 TaskExecutor:
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.TaskExecutor;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Service
public class NotificationService {
@Autowired
private TaskExecutor taskExecutor; // Spring提供的任務執行器接口
public void notifyAll(List<User> users, String message) {
System.out.println("開始通知所有用户... 當前線程: " + Thread.currentThread().getName());
for (User user : users) {
// 手動提交任務到執行器
taskExecutor.execute(() -> {
sendNotification(user, message); // 異步執行
});
// 如需獲取返回值,可以使用CompletableFuture
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return sendNotificationWithResult(user, message);
}, taskExecutor);
// 非阻塞處理結果
future.thenAccept(result -> {
System.out.println("通知結果: " + result);
});
// 鏈式操作示例:轉換結果並組合多個異步操作
CompletableFuture<Integer> processedFuture = future
.thenApply(result -> result.length()) // 轉換結果
.thenCombine( // 組合另一個異步操作
CompletableFuture.supplyAsync(() -> user.getName().length()),
(len1, len2) -> len1 + len2
);
// 非阻塞異常處理
processedFuture.exceptionally(ex -> {
System.err.println("處理失敗: " + ex.getMessage());
return -1;
});
}
System.out.println("通知流程初始化完成!");
}
// 注意:不再需要@Async註解
public void sendNotification(User user, String message) {
// 實現同前...
}
public String sendNotificationWithResult(User user, String message) {
// 返回通知結果
return "已通知" + user.getName();
}
}
工作原理:直接使用 Spring 的 TaskExecutor 提交任務,完全繞過 AOP 代理機制。
優點:
- 完全控制異步執行的方式和時機
- 不依賴 AOP 代理,更直接和透明
- 可以更細粒度地控制任務執行(如添加超時、錯誤處理等)
- 支持靈活的返回值處理,結合 CompletableFuture 實現非阻塞編程
- 支持複雜的異步編排(如鏈式操作、組合多個異步任務)
缺點:
- 失去了@Async 的聲明式便利性
- 需要更多的手動編碼
- 需要移除@Async 註解,修改方法簽名和調用邏輯,代碼侵入性高
針對返回值的異步方法
如果你的@Async 方法有返回值,它應該返回Future或CompletableFuture。在處理內部調用時,上述解決方案同樣適用:
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.CompletableFuture;
// 示例業務類
class ReportRequest {
private String id;
// 默認構造器
public ReportRequest() {}
public ReportRequest(String id) { this.id = id; }
public String getId() { return id; }
public void setId(String id) { this.id = id; }
}
class Report {
private String id;
private String content;
// 默認構造器
public Report() {}
public Report(String id, String content) {
this.id = id;
this.content = content;
}
}
@Service
public class ReportService {
@Autowired
private ReportService self; // 使用方案1:自我注入
public void generateReports(List<ReportRequest> requests) {
List<CompletableFuture<Report>> futures = new ArrayList<>();
for (ReportRequest request : requests) {
// 通過代理調用返回CompletableFuture的異步方法
CompletableFuture<Report> future = self.generateReport(request);
futures.add(future);
}
// 等待所有報告生成完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
// 處理結果
for (CompletableFuture<Report> future : futures) {
Report report = future.join();
// 處理報告...
}
}
@Async
public CompletableFuture<Report> generateReport(ReportRequest request) {
// 模擬耗時的報告生成
try {
System.out.println("生成報告中... 當前線程: " + Thread.currentThread().getName());
Thread.sleep(2000);
Report report = new Report(request.getId(), "報告內容...");
return CompletableFuture.completedFuture(report);
} catch (Exception e) {
CompletableFuture<Report> future = new CompletableFuture<>();
future.completeExceptionally(e);
return future;
}
}
}
異常處理與實踐建議
異步方法的異常處理需要特別注意:異步執行的方法拋出的異常不會傳播到調用方,因為異常發生在不同的線程中。
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import org.springframework.scheduling.annotation.AsyncResult;
@Service
public class RobustNotificationService {
@Autowired
private RobustNotificationService self;
private static final Logger logger = LoggerFactory.getLogger(RobustNotificationService.class);
public void notifyAll(List<User> users, String message) {
for (User user : users) {
// 錯誤:無法捕獲異步方法的異常,因為異常發生在另一個線程
// try {
// self.sendNotification(user, message);
// } catch (Exception e) {
// logger.error("Failed to send notification to user: " + user.getId(), e);
// }
// 正確方式1:使用全局異常處理器(在AsyncConfigurer中配置)
self.sendNotification(user, message);
// 正確方式2:如果方法返回Future,可以通過future捕獲異常
Future<?> future = self.sendNotificationWithFuture(user, message);
try {
future.get(); // 阻塞並捕獲異常
} catch (Exception e) {
logger.error("通知發送失敗: " + user.getName(), e);
// 處理失敗情況
}
// 正確方式3:使用CompletableFuture的異常處理
CompletableFuture<Void> cf = self.sendNotificationWithCompletableFuture(user, message);
cf.exceptionally(ex -> {
logger.error("通知發送失敗: " + user.getName(), ex);
return null;
});
}
}
@Async
public void sendNotification(User user, String message) {
try {
// 通知邏輯...
if (user.getName() == null) {
throw new RuntimeException("用户名不能為空");
}
} catch (Exception e) {
// 記錄詳細的異常信息,但異常不會傳播到調用方
logger.error("通知失敗: " + user.getName(), e);
// 異常會被AsyncUncaughtExceptionHandler處理(如果配置了)
throw e;
}
}
@Async
public Future<Void> sendNotificationWithFuture(User user, String message) {
// 實現邏輯...
return new AsyncResult<>(null);
}
@Async
public CompletableFuture<Void> sendNotificationWithCompletableFuture(User user, String message) {
// 實現邏輯...
return CompletableFuture.completedFuture(null);
}
}
實踐建議:
- 合理配置線程池:默認情況下,Spring 使用
SimpleAsyncTaskExecutor,每次調用都會創建新線程,這在生產環境中是不可接受的。應配置適當的線程池:
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 核心線程數
executor.setMaxPoolSize(10); // 最大線程數
executor.setQueueCapacity(25); // 隊列容量
executor.setThreadNamePrefix("MyAsync-");
// 拒絕策略:當隊列滿且線程數達到最大時的處理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 允許核心線程超時,適用於負載波動的場景
executor.setAllowCoreThreadTimeOut(true);
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
}
- 適當使用超時控制:對於需要獲取結果的異步方法,添加超時控制,但要注意阻塞問題:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
// 阻塞式超時控制(慎用,會阻塞當前線程)
CompletableFuture<Report> future = reportService.generateReport(request);
try {
Report report = future.get(30, TimeUnit.SECONDS); // 設置30秒超時
} catch (TimeoutException e) {
logger.error("報告生成超時", e);
// 處理超時情況
}
// 更好的非阻塞方式:
future.orTimeout(30, TimeUnit.SECONDS)
.thenAccept(report -> processReport(report))
.exceptionally(ex -> {
if (ex instanceof TimeoutException) {
logger.error("報告生成超時");
} else {
logger.error("報告生成失敗", ex);
}
return null;
});
- 慎用方案選擇:
- 對於簡單場景,自我注入(方案 1)最簡單直接
- 對於複雜業務邏輯,拆分服務(方案 4)是更好的架構選擇
- 如果需要細粒度控制,直接使用 TaskExecutor(方案 5)是最靈活的選擇
- 注意事務傳播:
異步方法執行在單獨的線程中,會導致事務傳播行為失效。Spring 的事務上下文通過ThreadLocal與當前線程綁定,異步方法在新線程中執行時,無法訪問調用方的ThreadLocal數據,因此必須在異步方法上單獨聲明@Transactional以創建新事務。
@Service
public class TransactionService {
@Autowired
private TransactionService self;
@Transactional
public void saveWithTransaction(Entity entity) {
// 事務操作...
// 錯誤:異步方法在新線程中執行,當前事務不會傳播
self.asyncOperation(entity); // 不會共享當前事務
}
@Async
@Transactional // 必須單獨添加事務註解,會創建新的事務
public void asyncOperation(Entity entity) {
// 此方法將有自己的事務,而非繼承調用方的事務
}
}
- 驗證異步執行:
// 在測試類中驗證異步執行
@SpringBootTest
public class AsyncServiceTest {
@Autowired
private NotificationService service;
@Test
public void testAsyncExecution() throws Exception {
// 記錄主線程名稱
String mainThread = Thread.currentThread().getName();
// 保存異步線程名稱
final String[] asyncThread = new String[1];
CountDownLatch latch = new CountDownLatch(1);
User user = new User();
user.setName("TestUser");
// 重寫異步方法以捕獲線程名稱
service.sendNotificationWithCompletableFuture(user, "test")
.thenAccept(v -> {
asyncThread[0] = Thread.currentThread().getName();
latch.countDown();
});
// 等待異步操作完成
latch.await(5, TimeUnit.SECONDS);
// 驗證線程不同
assertThat(mainThread).isNotEqualTo(asyncThread[0]);
assertThat(asyncThread[0]).startsWith("MyAsync-");
}
}
五種方案對比
總結
| 解決方案 | 實現複雜度 | 代碼侵入性 | 額外依賴 | 架構清晰度 | 適用場景 |
|---|---|---|---|---|---|
| 自我注入 | 低 | 低
(僅添加一個自注入字段,無方法邏輯修改) |
無 | 中 | 簡單項目,快速解決 |
| ApplicationContext | 中 | 中 | ApplicationContext | 中 | 需要明確控制代理獲取 |
| AopContext | 中 | 中 | 需開啓 exposeProxy | 中 | 不想增加依賴字段 |
| 拆分服務 | 高 | 低 | 無 | 高 | 大型項目,關注點分離 |
| 手動 TaskExecutor | 高 | 高
(需修改方法註解和調用邏輯) |
TaskExecutor | 高 | 需要精細控制異步執行
需靈活處理返回值 需要複雜異步編排 |