Spring Framework 為常見緩存場景提供了全面的抽象,而無需耦合到任何受支持的緩存實現(xiàn)。但是,特定存儲的到期時間聲明不是此抽象的一部分。如果我們要設(shè)置緩存的生存時間,則必須調(diào)整所選緩存提供程序的配置。從這篇文章中,您將學(xué)習(xí)如何為具有不同 TTL 配置的多個 Caffeine 緩存準(zhǔn)備設(shè)置。
1. 研究案例
讓我們從問題的定義開始。我們想象中的應(yīng)用程序需要緩存兩個不同的 REST 端點,但其中一個應(yīng)該比另一個更頻繁地過期。考慮以下外觀實現(xiàn):
@Service
class ForeignEndpointGateway {
private RestTemplate restTemplate;
ForeignEndpointGateway(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Cacheable("messages")
public Message findMessage(long id) {
String url ="http://somedomain.com/messages/" + id;
return restTemplate.getForObject(url, Message.class);
}
@Cacheable("notifications")
public Notification findNotification(long id) {
String url ="http://somedomain.com/notifications/" + id;
return restTemplate.getForObject(url, Notification.class);
}
}
?@Cacheable
?注釋標(biāo)記方法Spring的緩存機制。值得一提的是,緩存的方法必須是公開的。每個注解都指定了應(yīng)該用于特定方法的相應(yīng)緩存的名稱。
緩存實例只不過是一個簡單的鍵值容器。在我們的例子中,鍵是基于輸入?yún)?shù)創(chuàng)建的,值是方法的結(jié)果,但它不必那么簡單。Spring 提供的緩存抽象允許更多,但這是另一篇文章的主題。如果你對細(xì)節(jié)感興趣,我推薦你參考文檔。讓我們堅持我們的主要目標(biāo),即為兩個聲明的緩存定義不同的 TTL 值。
2. 常用緩存設(shè)置
將?@Cacheable
?注釋放在方法上并不是在應(yīng)用程序中運行緩存機械化所需的唯一內(nèi)容。根據(jù)所選的提供商,可能會有幾個額外的步驟。
2.1. 開啟 Spring 緩存
無論您選擇哪個提供程序,設(shè)置的起點始終是將?@EnableCaching
?注釋添加到您的配置類之一,通常是主應(yīng)用程序類。這會在您的 Spring 上下文中注冊所有必需的組件。
@SpringBootApplication
@EnableCaching
public class TtlCacheApplication {
// content omitted for clarity
}
2.2. 必需的依賴項
在使用@EnableCaching注釋的常規(guī) Spring 應(yīng)用程序中,需要開發(fā)人員提供?CacheManager
?類型的 bean 。幸運的是,Spring Boot 緩存啟動器提供了默認(rèn)管理器,并根據(jù)類路徑上可用的依賴項創(chuàng)建了一個適當(dāng)?shù)木彺嫣峁┏绦?,在我們的例子中?Caffeine 庫。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
2.3. 基本配置
Spring Boot 支持的大多數(shù)緩存提供程序可以使用專用的應(yīng)用程序?qū)傩赃M(jìn)行調(diào)整。要為演示應(yīng)用程序所需的兩個緩存設(shè)置 TTL,我們可以使用以下值:
spring.cache.cache-names=messages,notifications
spring.cache.caffeine.spec=maximumSize=100,expireAfterAccess=1800s
以一種非常簡單的方式,我們將緩存的 TTL 設(shè)置為 30 分鐘,并將它們的容量設(shè)置為 100。但是,這種配置的主要問題是所有緩存都使用相同的設(shè)置。不可能為每個緩存設(shè)置不同的規(guī)范。他們都需要共享一個全局的。如果您不介意此類限制,則可以進(jìn)行基本設(shè)置。否則,您應(yīng)該繼續(xù)閱讀下一部分。
3.區(qū)分緩存
Spring Boot 有效地處理流行的配置,但我們的場景不屬于這個幸運組。為了根據(jù)我們的需要自定義緩存,我們需要超越預(yù)定義的 bean 并編寫一些自定義初始化代碼。
3.1. 自定義緩存管理器
無需禁用 Spring Boot 提供的默認(rèn)配置,因為我們只能覆蓋一個必要的對象。通過定義名為cacheManager的 bean,我們替換了 Spring Boot 提供的 bean 。下面我們創(chuàng)建兩個緩存。第一個稱為消息,其過期時間等于 30 分鐘。另一個名為通知的值存儲 60 分鐘。當(dāng)您創(chuàng)建自定義緩存管理器時,application.properties 中的設(shè)置(之前在基本示例中介紹過)不再使用,可以安全地刪除。
@Bean
public CacheManager cacheManager(Ticker ticker) {
CaffeineCache messageCache = buildCache("messages", ticker,30);
CaffeineCache notificationCache = buildCache("notifications", ticker,60);
SimpleCacheManager manager =new SimpleCacheManager();
manager.setCaches(Arrays.asList(messageCache, notificationCache));
return manager;
}
private CaffeineCache buildCache(String name, Ticker ticker,int minutesToExpire) {
return new CaffeineCache(name, Caffeine.newBuilder()
.expireAfterWrite(minutesToExpire, TimeUnit.MINUTES)
.maximumSize(100)
.ticker(ticker)
.build());
}
@Bean
public Ticker ticker() {
return Ticker.systemTicker();
}
Caffeine 庫帶有一個方便的緩存構(gòu)建器。在我們的演示中,我們只關(guān)注不同的 TTL 值,但也可以根據(jù)需要自定義其他選項(例如容量或訪問后非常有用的到期時間)。
在上面的例子中,我們還創(chuàng)建了ticker bean,我們的緩存共享它。自動收報機負(fù)責(zé)跟蹤時間的流逝。實際上,將Ticker類型的實例傳遞給緩存構(gòu)建器并不是強制性的,如果沒有提供,Caffeine 會創(chuàng)建一個。但是,如果我們想為我們的解決方案編寫測試,單獨的 bean 將更容易存根。
3.2. TTL緩存測試
我們在集成測試中需要的第一件事是一個帶有假代碼的配置類,它允許模擬時間流逝。Caffeine 庫本身不提供這樣的代碼,但文檔中提到了 guava-testlib,我們需要將其聲明為我們項目的依賴項。
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava-testlib</artifactId>
<version>20.0</version>
<scope>test</scope>
</dependency>
如果測試類中存在一個內(nèi)部靜態(tài)配置類,則 Spring Boot 1.4.0 中添加的?@SpringBootTest
?注釋會自動檢測并利用內(nèi)部靜態(tài)配置類。通過導(dǎo)入主配置類,我們保留了原始項目設(shè)置,并僅用假的替換了股票代碼實例。
@RunWith(SpringRunner.class)
@SpringBootTest
public class MessageRepositoryTest {
@Configuration
@Import(TtlCacheApplication.class)
public static class TestConfig {
static FakeTicker fakeTicker =new FakeTicker();
@Bean
public Ticker ticker() {
return fakeTicker::read;
}
}
}
我們將在緩存網(wǎng)關(guān)類使用的?RestTemplate
?實例上使用監(jiān)控,以觀察對真實 REST 端點的可能調(diào)用數(shù)量。監(jiān)控應(yīng)該返回一些存根值以防止實際調(diào)用發(fā)生。
private static final long MESSAGE_ID =1;
private static final long NOTIFICATION_ID =2;
@SpyBean
private RestTemplate restTemplate;
@Autowired
private ForeignEndpointGateway gateway;
@Before
public void setUp()throws Exception {
Message message = stubMessage(MESSAGE_ID);
Notification notification = stubNotification(NOTIFICATION_ID);
doReturn(message)
.when(restTemplate)
.getForObject(anyString(), eq(Message.class));
doReturn(notification)
.when(restTemplate)
.getForObject(anyString(), eq(Notification.class));
}
最后,我們可以用我們的快樂路徑場景編寫一個測試,以確認(rèn) TTL 配置是否符合我們的預(yù)期。
@Test
public void shouldUseCachesWithDifferentTTL()throws Exception {
// 0 minutes
foreignEndpointGateway.findMessage(MESSAGE_ID);
foreignEndpointGateway.findNotification(NOTIFICATION_ID);
verify(restTemplate, times(1)).getForObject(anyString(), eq(Message.class));
verify(restTemplate, times(1)).getForObject(anyString(), eq(Notification.class));
// after 5 minutes
TestConfig.fakeTicker.advance(5, TimeUnit.MINUTES);
foreignEndpointGateway.findMessage(MESSAGE_ID);
verify(restTemplate, times(1)).getForObject(anyString(), eq(Message.class));
// after 35 minutes
TestConfig.fakeTicker.advance(30, TimeUnit.MINUTES);
foreignEndpointGateway.findMessage(MESSAGE_ID);
foreignEndpointGateway.findNotification(NOTIFICATION_ID);
verify(restTemplate, times(2)).getForObject(anyString(), eq(Message.class));
verify(restTemplate, times(1)).getForObject(anyString(), eq(Notification.class));
// after 65 minutes
TestConfig.fakeTicker.advance(30, TimeUnit.MINUTES);
foreignEndpointGateway.findNotification(NOTIFICATION_ID);
verify(restTemplate, times(2)).getForObject(anyString(), eq(Notification.class));
}
一開始,?Message
?和?Notification
?對象都是從端點獲取并放置在緩存中。5 分鐘后,將再次調(diào)用?Message
?對象。由于消息緩存 TTL 配置為 30 分鐘,我們預(yù)計將從緩存中獲取該值,并且不會調(diào)用端點。再過 30 分鐘后,我們預(yù)計緩存的消息已過期,我們通過對端點的另一次調(diào)用來確認(rèn)這一點。但是,通知緩存已配置為將值保留 60 分鐘。通過再次嘗試獲取通知,我們確認(rèn)另一個緩存仍然有效。最后,自動收報機再前進(jìn) 30 分鐘,從測試開始算起總共 65 分鐘。我們驗證通知也已過期并從緩存中刪除。
3. 與其他緩存提供者的 TTL
如前所述,Caffeine 的主要缺點是無法區(qū)分所有緩存。?spring.cache.caffeine.spec
? 中的規(guī)范適用于全球。希望在未來的版本中可以簡化多個緩存的設(shè)置,但現(xiàn)在我們需要堅持手動配置。
對于其他緩存提供者,幸運的是情況要容易得多。?EhCache
?、?Hazelcast
?和 ?Infinitspan
? 使用專用的 XML 配置文件,其中每個緩存都可以單獨配置。
4. 總結(jié)
盡管 Spring Boot 在為我們解決了平凡的配置方面做得非常出色,但有時我們需要自己做出更好的決定。在簡單的情況下,Caffeine 緩存的默認(rèn)設(shè)置可能就足夠了,但與其他支持的緩存提供程序相比,它顯得相形見絀。閱讀這篇文章后,您應(yīng)該知道如何準(zhǔn)備 Caffeine 緩存庫的基本和更復(fù)雜的自定義配置。