开发者

Spring @Async 注解异步调用失效的五种解决方案

目录
  • Spring @Async 的正常工作原理
  • 内部调用问题
  • 为什么内部调用会失效?
  • 五种解决方案
    • 方案 1:自我注入(Self-Injection)
    • 方案 2:使用 ApplicationContext 获取代理对象
    • 方案 3:使用 AopContext 获取代理对象
    • 方案 4:拆分为单独的服务类
    • 方案 5:手动使用 TaskExecutor
  • 针对返回值的异步方法
    • 异常处理与实践建议
      • 五种方案对比
        • 总结

          给一个方法加上了@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 @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 @Async 注解异步调用失效的五种解决方案

          内部调用失效的核心原因是: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

          方案 http://www.devze.com4:拆分为单独的服务类

          将异步方法拆分到单独的服务类中:

          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 Strinphpg 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.http://www.devze.comspringframework.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<"通知发送失败: " + 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 jsorg.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 // 必须单独添加事务注解,会创建新的事务
          js    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-");
              }
          }

          五种方案对比

          Spring @Async 注解异步调用失效的五种解决方案

          总结

          解决方案

          实现复杂度

          代码侵入性

          额外依赖

          架构清晰度

          适用场景

          自我注入

          (仅添加一个自注入字段,无方法逻辑修改)

          简单项目,快速解决

          ApplicationContext

          ApplicationContext

          需要明确控制代理获取

          AopContext

          需开启 exposeProxy

          不想增加依赖字段

          拆分服务

          大型项目,关注点分离

          手动 TaskExecutor

          (需修改方法注解和调用逻辑)

          TaskExecutor

          需要精细控制异步执行

          需灵活处理返回值

          需要复杂异步编排

          0

          上一篇:

          下一篇:

          精彩评论

          暂无评论...
          验证码 换一张
          取 消

          最新开发

          开发排行榜