1. 引言

缓存是一种有效的性能提升策略,通过避免在结果(对于已知一段时间内)没有变化的情况下重复执行逻辑。

Spring Boot 提供了@Cacheable注解,我们将其定义在方法上,以缓存该方法的结果。在某些场景中,比如在较低环境中的测试时,我们可能需要禁用缓存以观察某些修改后的行为。

本文将介绍如何在 Spring Boot 中配置缓存,并学习如何在需要时禁用缓存。

2. 缓存设置

让我们以一个简单的查询书籍评论示例来设置缓存,使用@Cacheable对方法进行缓存。

我们的实体类将是BookReview类,包含ratingisbn等属性:

@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;
   
    // getters & setters
}

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

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

BookReviewsLogic类包含一个方法,调用BookRepository中的findByIsbn()。我们在方法上添加@Cacheable注解,针对给定的isbnbook_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=?";

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

    // 2nd execution
    bookReviewsLogic.getBooksByIsbn(bookReview.getIsbn());
    logs = output.toString()
      .split("\\r?\\n");

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

    // count 1 means the select query log from 1st execution.
    assertEquals(1,count);
}

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

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

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实例,它是一个不执行缓存的缓存管理器。否则,我们可以返回基于哈希的缓存管理器。

有了上述简单的设置,我们可以在 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 means the select query log from 1st and 2nd execution.
assertEquals(2, count);

4. 总结

在这篇教程中,我们简要介绍了 Spring Boot 中的缓存,并设置了应用中的缓存。我们也学习了如何在需要时禁用缓存,以便测试代码的特定部分。此外,我们还编写了必要的测试以验证启用和禁用缓存的功能。

如往常一样,示例代码可在GitHub上找到。