Spring Boot 在测试时禁用 @Cacheable 缓存

1、简介

缓存是一种有效的策略,当执行结果在一段已知时间内没有变化时,可以避免重复执行逻辑,从而提高性能。

Spring Boot 提供了 @Cacheable 注解,可以在方法上定义该注解,它就会缓存方法的结果。在某些情况下,例如在测试环境中进行测试时,我们可能需要禁用缓存来观察某些修改后的行为。

本文将带你了解如何配置 Spring Boot 中的缓存,以及如何在需要时禁用缓存。

2、缓存配置

设置一个简单的用例,通过 ISBN(国际标准书号)查询图书评论,并在某个逻辑中使用 @Cacheable 对该方法返回的结果进行缓存。

实体类 BookReview 如下,它包含 bookRatingisbn 等信息:

@Entity
@Table(name="BOOK_REVIEWS")
public class BookReview {
    @Id
    @GeneratedValue(strategy= GenerationType.SEQUENCE, generator = "book_reviews_reviews_id_seq")
    @SequenceGenerator(name = "book_reviews_reviews_id_seq", sequenceName = "book_reviews_reviews_id_seq", allocationSize = 1)
    private Long reviewsId;
    private String userId;
    private String isbn;
    private String bookRating;
   
    // Get / Set 方法省略
}

BookRepository 中添加一个简单的 findByIsbn() 方法,用于按 isbn 查询书评:

public interface BookRepository extends JpaRepository<BookReview, Long> {
    List<BookReview> findByIsbn(String isbn);
}

BookReviewsLogic 类包含一个在 BookRepository 中调用 findByIsbn() 的方法。我们添加了 @Cacheable 注解,将指定 isbn 的结果缓存在 book_reviews 缓存中:

@Service
public class BookReviewsLogic {
    @Autowired
    private BookRepository bookRepository;

    @Cacheable(value = "book_reviews", key = "#isbn")
    public List<BookReview> getBooksByIsbn(String isbn){
        return bookRepository.findByIsbn(isbn);
    }
}

添加了 @Cacheable 注解后,还需要激活、配置缓存。可以在 @Configuration 配置类上添加 @EnableCaching 注解来自动配置缓存。

如下,使用 HashMap 存储缓存项目:

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager();
    }
}

缓存配置完成后。如果我们在 BookReviewsLogic 中执行 getBooksByIsbn(),该方法返回的结果就会在第一次执行时被缓存起来,然后立即返回,而无需重新计算(即查询数据库),从而提高了性能。

编写一个简单的测试来验证一下:

@Test
public void givenCacheEnabled_whenLogicExecuted2ndTime_thenItDoesntQueriesDB(CapturedOutput output){
    BookReview bookReview = insertBookReview();

    String target = "Hibernate: select bookreview0_.reviews_id as reviews_1_0_, "
      + "bookreview0_.book_rating as book_rat2_0_, "
      + "bookreview0_.isbn as isbn3_0_, "
      + "bookreview0_.user_id as user_id4_0_ "
      + "from book_reviews bookreview0_ "
      + "where bookreview0_.isbn=?";

    // 第 1 次执行
    bookReviewsLogic.getBooksByIsbn(bookReview.getIsbn());
    String[] logs = output.toString()
      .split("\\r?\\n");
    assertThat(logs).anyMatch(e -> e.contains(target));

    // 第 2 次执行
    bookReviewsLogic.getBooksByIsbn(bookReview.getIsbn());
    logs = output.toString()
      .split("\\r?\\n");

    long count = Arrays.stream(logs)
      .filter(e -> e.equals(target))
      .count();

    // count 1 表示第一次执行的 select 查询日志。
    assertEquals(1,count);
}

在上述测试中,我们执行了两次 getBooksByIsbn(),捕获了日志,并确认 select 查询只执行了一次,因为 getBooksByIsbn() 方法在第二次执行时会返回缓存结果。

要为针对数据库执行的查询生成 SQL 日志,可以在 application.properties 文件中设置以下属性:

# 在日志中输出 JPA 执行的 SQL 语句
spring.jpa.show-sql=true

3、禁用缓存

要禁用缓存,可以在 application.properties 文件中使用一个额外的自定义属性(即 appconfig.cache.enabled):

appconfig.cache.enabled=true

然后,可以在缓存配置类中注入该配置值,并根据配置值返回同的缓存实现:

@Bean
public CacheManager cacheManager(@Value("${appconfig.cache.enabled}") String isCacheEnabled) {
    if (isCacheEnabled.equalsIgnoreCase("false")) {
        return new NoOpCacheManager();
    }

    return new ConcurrentMapCacheManager();
}

如上,查属性是否设置为禁用缓存。如果是,可以返回 NoOpCacheManager 的实例,这是一个不执行缓存的 CacheManager。否则,可以返回基于 Hash 的 CacheManager器。

通过上述简单设置,就可以禁用 Spring Boot 应用中的缓存了。

通过一个简单的测试来验证上述设置。

首先,需要修改在 application.properties 中定义的缓存属性。对于测试设置,可以使用 @TestPropertySource 覆盖该属性:

@SpringBootTest(classes = BookReviewApplication.class)
@ExtendWith(OutputCaptureExtension.class)
@TestPropertySource(properties = {
    "appconfig.cache.enabled=false"
})
public class BookReviewsLogicCacheDisabledUnitTest {
    // ...
}

然后,测试将与之前类似,执行两次逻辑。检查 SQL 查询日志,看它是否在当前测试中记录了两次,因为缓存被禁用了,所以执行结果不会被缓存:

long count = Arrays.stream(logs)
   .filter(e -> e.contains(target))
   .count();

// count 2 表示第 1 次和第 2 次执行的 select 查询日志。
assertEquals(2, count);

4、总结

本文简要介绍了 Spring Boot 中的缓存,以及如何在需要测试代码的某些部分时禁用缓存。


Ref:https://www.baeldung.com/spring-boot-disable-cacheable-annotation