Stories

Detail Return Return

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

是不是遇到過這種情況:你給一個方法加上了@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 san-mu Avatar ecomools Avatar lyhabc Avatar hankin_liu Avatar devlive Avatar mengxiang_592395ab95632 Avatar guangmingleiluodetouyingyi_bccdlf Avatar code4world Avatar
Favorites 8 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.