使用@Async注解的方法返回空列表,原因何在?
问题:@Async注解导致Spring Data JPA仓库返回空列表
问题背景
在Spring Data JPA仓库方法上使用@Async注解后,调用方法返回空列表;移除启动类上的@EnableAsync注解后,查询能正常返回预期的3条数据。以下是完整的最小复现示例:
异步仓库接口
public interface AsynchronousCardRepository extends Repository<Card, UUID> { @Async Future<List<Card>> findAll(); }
测试类
@DataJpaTest @ActiveProfiles("test") @Sql( executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = {"/schema-h2.sql", "/data-h2.sql"} ) public class AsynchronousCardRepositoryTest { @Autowired AsynchronousCardRepository cardRepository; @Test @SneakyThrows void findAll() { Future<List<Card>> futureCards = cardRepository.findAll(); List<Card> cards = futureCards.get(); assertThat(cards).asList().hasSize(3); } }
数据库表结构SQL
-- schema CREATE TABLE IF NOT EXISTS PUBLIC.account ( id UUID PRIMARY KEY ); CREATE TABLE IF NOT EXISTS PUBLIC.card ( id UUID PRIMARY KEY, account_id UUID NOT NULL REFERENCES account (id), status VARCHAR(30) NOT NULL );
测试数据SQL
-- data INSERT INTO PUBLIC.ACCOUNT VALUES ('00000000-0000-0000-0000-000000000ac1'); INSERT INTO PUBLIC.CARD VALUES ('00000000-0000-0000-0000-0000000000c1', '00000000-0000-0000-0000-000000000ac1', 'ACTIVE'); INSERT INTO PUBLIC.CARD VALUES ('00000000-0000-0000-0000-0000000000c2', '00000000-0000-0000-0000-000000000ac1', 'ACTIVE'); INSERT INTO PUBLIC.CARD VALUES ('00000000-0000-0000-0000-0000000000c3', '00000000-0000-0000-0000-000000000ac1', 'NON_ACTIVE');
Card实体类
@Entity @Table(name = "card") @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class Card { @Id @GeneratedValue(strategy = GenerationType.AUTO) private UUID id; @ManyToOne @JoinColumn(name = "account_id", referencedColumnName = "id") private Account account; @Enumerated(value = EnumType.STRING) private CardStatus status; }
CardStatus枚举
public enum CardStatus { ACTIVE, NON_ACTIVE }
Account实体类
@Entity @Table(name = "account") @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class Account { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") private UUID id; @OneToMany(mappedBy = "account") private List<Card> cards; }
Spring启动类
@SpringBootApplication @EnableAsync // 移除该注解后查询正常 public class CreditServiceApplication { public static void main(String[] args) { SpringApplication.run(CreditServiceApplication.class, args); } }
测试环境配置
spring: datasource: url: jdbc:h2:mem:db;DB_CLOSE_DELAY=-1;MODE=MySQL;DATABASE_TO_LOWER=TRUE driver-class-name: org.h2.Driver username: sa password: jpa: hibernate: ddl-auto: none show-sql: true database-platform: org.hibernate.dialect.H2Dialect properties: hibernate: format_sql: true h2: console: enabled: false settings: web-allow-others: true config: activate: on-profile: test logging: level: org: hibernate: type: TRACE springframework: jdbc: core: TRACE datasource: init: DEBUG test: context: jdbc: DEBUG
报错信息
java.lang.AssertionError: Expected size: 3 but was: 0 in: [] at by.afinny.credit.unit.repository.AsynchronousCardRepositoryTest.findAll(AsynchronousCardRepositoryTest.java:32) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
原因分析
核心问题是事务上下文的隔离:
@DataJpaTest会为每个测试方法自动创建事务,默认测试结束后回滚该事务。@Sql脚本的插入操作在测试线程的事务中执行,此时数据未提交到数据库(事务未结束)。@Async注解会让方法在全新线程中执行,该线程无法继承测试线程的事务上下文。异步线程执行查询时,测试线程的事务仍处于未提交状态,因此看不到@Sql插入的数据,返回空列表。- 移除
@EnableAsync后,方法同步执行,和@Sql脚本处于同一事务上下文,能读取到未提交的数据,因此返回正确结果。
解决方案
方案一:修改测试类的事务传播行为
给测试方法添加@Transactional(propagation = Propagation.NOT_SUPPORTED),让测试在非事务环境下执行,@Sql的插入操作会自动提交到数据库,异步线程即可查到数据:
@Test @SneakyThrows @Transactional(propagation = Propagation.NOT_SUPPORTED) void findAll() { Future<List<Card>> futureCards = cardRepository.findAll(); List<Card> cards = futureCards.get(); assertThat(cards).asList().hasSize(3); }
方案二:给异步仓库方法添加独立事务
在仓库方法上添加@Transactional(propagation = Propagation.REQUIRES_NEW),确保异步方法在独立事务中执行,同时给测试类添加@Rollback(false)让测试事务提交:
public interface AsynchronousCardRepository extends Repository<Card, UUID> { @Async @Transactional(propagation = Propagation.REQUIRES_NEW) Future<List<Card>> findAll(); }
测试类修改:
@DataJpaTest @ActiveProfiles("test") @Sql( executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = {"/schema-h2.sql", "/data-h2.sql"} ) @Rollback(false) public class AsynchronousCardRepositoryTest { // ... 测试代码 }
方案三:手动控制测试事务提交
在测试方法中注入TestTransaction,手动提交事务后再调用异步方法:
@Autowired private TestTransaction testTransaction; @Test @SneakyThrows void findAll() { // 提交测试事务,让@Sql的数据写入数据库 testTransaction.end(); testTransaction.start(); Future<List<Card>> futureCards = cardRepository.findAll(); List<Card> cards = futureCards.get(); assertThat(cards).asList().hasSize(3); }
内容的提问来源于stack exchange,提问作者Sergey Zolotarev
相关产品推荐
相关产品推荐

