Stories

Detail Return Return

Spring @Async 內部調用失效問題:五種解決方案實戰分析

是不是遇到過這種情況:你給一個方法加上了@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 的工作原理如下:

graph TD
    A[調用方] --> B[代理對象]
    B --> C{"是否有@Async註解?"}
    C -->|是| D[提交到線程池]
    C -->|否| E[直接執行方法]
    D --> F[異步執行]
    E --> G[同步執行]

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方法中調用它時,它還是會同步執行,這不是我們預期的行為。

為什麼內部調用會失效?

graph TD
    A[內部方法調用] --> B["直接調用this.method()"]
    B --> |導致| C[this引用指向目標對象而非代理對象]
    C --> |無代理攔截| D[繞過Spring AOP代理]
    D --> E["無法觸發@Async處理"]
    E --> F[同步執行]

    G[外部方法調用] --> H[通過代理對象調用]
    H --> I[經過Spring AOP處理]
    I --> J["識別@Async註解"]
    J --> K[提交到線程池]
    K --> L[異步執行]

內部調用失效的核心原因是: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 方法有返回值,它應該返回FutureCompletableFuture。在處理內部調用時,上述解決方案同樣適用:

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);
    }
}

實踐建議

  1. 合理配置線程池:默認情況下,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();
    }
}
  1. 適當使用超時控制:對於需要獲取結果的異步方法,添加超時控制,但要注意阻塞問題:
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. 慎用方案選擇
  • 對於簡單場景,自我注入(方案 1)最簡單直接
  • 對於複雜業務邏輯,拆分服務(方案 4)是更好的架構選擇
  • 如果需要細粒度控制,直接使用 TaskExecutor(方案 5)是最靈活的選擇
  1. 注意事務傳播
    異步方法執行在單獨的線程中,會導致事務傳播行為失效。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) {
        // 此方法將有自己的事務,而非繼承調用方的事務
    }
}
  1. 驗證異步執行
// 在測試類中驗證異步執行
@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 需要精細控制異步執行
需靈活處理返回值
需要複雜異步編排
user avatar
0 users favorite the story!

Post Comments

Some HTML is okay.