SpringのCache Abstractionで、Redisを試してみたくなったので、気になるところの確認を含めて
ちょっと遊んでみます。
Spring BootのAutoConfigure対象で、裏ではSpring Data Redisを使っているみたいです。
ここでは、簡単なJavaBeansをRedisに突っ込むことと、@CacheableアノテーションでCacheを使うくらいの確認を
してみます。
準備
まずは、Maven依存関係から。
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <spring.boot.version>1.5.1.RELEASE</spring.boot.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring.boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring.boot.version}</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
とりあえずSpringのCache AbstractionとRedis、それからテストが動けばいいことにするので、
依存関係はこのくらいで。
対象のRedisは、3.2.6でprotection modeはoffとします。
アプリケーションの準備
簡単な確認用のアプリケーションを準備します。
まずは、キャッシュに格納する値とするクラスを。
src/main/java/org/littlewings/spring/cache/Message.java
package org.littlewings.spring.cache; import java.io.Serializable; import java.time.LocalDateTime; public class Message implements Serializable { private static final long serialVersionUID = 1L; private String value; private LocalDateTime now; public Message() { } public Message(String value) { this.value = value; now = LocalDateTime.now(); } public String getValue() { return value; } public LocalDateTime getNow() { return now; } }
キャッシュしていることがわかりやすいように、インスタンス作成時の現在時刻を持つように
しておきましょう。あと、Serializableにしています。
キャッシュを適用するクラス。
src/main/java/org/littlewings/spring/cache/MessageService.java
package org.littlewings.spring.cache; import java.util.concurrent.TimeUnit; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @Service public class MessageService { @Cacheable(cacheNames = "myCache") public Message build(String message) { try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { // ignore } return new Message(message); } }
ちょっとスリープして、引数を先ほど作成したクラスに包んで返すだけです。
SpringのCache Abstractionの有効化。
src/main/java/org/littlewings/spring/cache/CacheConfig.java
package org.littlewings.spring.cache; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Configuration; @Configuration @EnableCaching public class CacheConfig { }
@EnableCachingがあるだけですね。
直接は使いませんが、Spring Bootのエントリポイントも作成しておきます。
src/main/java/org/littlewings/spring/cache/App.java
package org.littlewings.spring.cache; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { public static void main(String... args) { SpringApplication.run(App.class, args); } }
一応、テストコードの起動では間接的に使うので。
確認
それでは、使ってみましょう。
先ほど用意したクラスを使う、こんな感じのテストコードを用意。
src/test/java/org/littlewings/spring/cache/SimpleRedisCacheTest.java
package org.littlewings.spring.cache; import java.time.LocalDateTime; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.util.StopWatch; import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) @SpringBootTest public class SimpleRedisCacheTest { @Autowired MessageService messageService; @Test public void test() { StopWatch stopWatch = new StopWatch(); // slow stopWatch.start(); Message m1 = messageService.build("Hello World"); stopWatch.stop(); LocalDateTime now1 = m1.getNow(); long elapsed1 = (long) stopWatch.getLastTaskInfo().getTimeSeconds(); assertThat(m1.getValue()) .isEqualTo("Hello World"); assertThat(elapsed1) .isGreaterThanOrEqualTo(3L); // quick stopWatch.start(); Message m2 = messageService.build("Hello World"); stopWatch.stop(); LocalDateTime now2 = m2.getNow(); long elapsed2 = (long) stopWatch.getLastTaskInfo().getTimeSeconds(); assertThat(m2.getValue()) .isEqualTo("Hello World"); assertThat(elapsed2) .isEqualTo(0L); assertThat(now2) .isEqualTo(now1); // cached // slow stopWatch.start(); Message m3 = messageService.build("Hello Redis!!"); stopWatch.stop(); LocalDateTime now3 = m3.getNow(); long elapsed3 = (long) stopWatch.getLastTaskInfo().getTimeSeconds(); assertThat(m3.getValue()) .isEqualTo("Hello Redis!!"); assertThat(elapsed3) .isEqualTo(3L); assertThat(now3) .isNotEqualTo(now1); } }
キャッシュが効いているか測りつつ実行しますが、そこにはSpringのStopWatchを使いました。
StopWatch stopWatch = new StopWatch();
1回目は時間がかかっていることを確認し、
// slow stopWatch.start(); Message m1 = messageService.build("Hello World"); stopWatch.stop(); LocalDateTime now1 = m1.getNow(); long elapsed1 = (long) stopWatch.getLastTaskInfo().getTimeSeconds(); assertThat(m1.getValue()) .isEqualTo("Hello World"); assertThat(elapsed1) .isGreaterThanOrEqualTo(3L);
2回目はキャッシュされていて高速になり、現在時刻に変化もないことが確認できます。
// quick stopWatch.start(); Message m2 = messageService.build("Hello World"); stopWatch.stop(); LocalDateTime now2 = m2.getNow(); long elapsed2 = (long) stopWatch.getLastTaskInfo().getTimeSeconds(); assertThat(m2.getValue()) .isEqualTo("Hello World"); assertThat(elapsed2) .isEqualTo(0L); assertThat(now2) .isEqualTo(now1); // cached
今回用意したコードの場合、メソッドの引数がキーになるので、引数を変えるとまた時間がかかることが
確認できます。
// slow stopWatch.start(); Message m3 = messageService.build("Hello Redis!!"); stopWatch.stop(); LocalDateTime now3 = m3.getNow(); long elapsed3 = (long) stopWatch.getLastTaskInfo().getTimeSeconds(); assertThat(m3.getValue()) .isEqualTo("Hello Redis!!"); assertThat(elapsed3) .isEqualTo(3L); assertThat(now3) .isNotEqualTo(now1);
OKそうですね。
有効期限を設定する
先ほどの例では、特にキャッシュの有効期限は設定していませんでした。
設定できないとちょっと困りそうですが、Spring Data Redisだと@TimeToLiveというアノテーションを使いそうな
雰囲気です。
Redis Repositories / Time To Live
では、Spring CacheでのRedisではどうするかですが、RedisCacheManagerで設定すればよさそうです。
先ほど作成した、@EnableCachingを付与したJavaConfigクラスを次のように変更。
src/main/java/org/littlewings/spring/cache/CacheConfig.java
package org.littlewings.spring.cache; import java.util.HashMap; import java.util.Map; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.core.RedisTemplate; @Configuration @EnableCaching public class CacheConfig { @Bean public CacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) { RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate); cacheManager.setUsePrefix(true); // true => key prefix cache-name Map<String, Long> expires = new HashMap<>(); expires.put("myCache", 5L); // key is cache-name cacheManager.setExpires(expires); // expire per cache cacheManager.setDefaultExpiration(10L); // default expire cacheManager.afterPropertiesSet(); return cacheManager; } }
RedisCacheManager#setExpiresにMapを与えることで、キャッシュごとに有効期限を設定できます。
Map<String, Long> expires = new HashMap<>(); expires.put("myCache", 5L); // key is cache-name cacheManager.setExpires(expires); // expire per cache
今回は、@Cacheableに設定していた「myCache」というキャッシュに対して有効期限を設定しています。
また、デフォルトの有効期限も設定できて、RedisCacheManager#setDefaultExpirationで指定すれば
OKです。今回は10秒にしてみました。
cacheManager.setDefaultExpiration(10L); // default expire
なにも指定しないと、有効期限が0になっていてexpireしません。
なお、RedisCacheManager#setUsePrefixというのはキーの接頭辞としてキャッシュの名前を入れるかどうかで、
デフォルト「true」でキャッシュの名前が入ります。
cacheManager.setUsePrefix(true); // true => key prefix cache-name
では、確認してみます。
デフォルトの有効期限が効いているかどうかも確認するために、Serviceにメソッドを追加します。
src/main/java/org/littlewings/spring/cache/MessageService.java
package org.littlewings.spring.cache; import java.util.concurrent.TimeUnit; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @Service public class MessageService { @Cacheable(cacheNames = "myCache") public Message build(String message) { try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { // ignore } return new Message(message); } @Cacheable(cacheNames = "anotherCache") public Message buildAnother(String message) { try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { // ignore } return new Message(message); } }
超適当…。
テストコード。
src/test/java/org/littlewings/spring/cache/RedisCacheExpireTest.java
package org.littlewings.spring.cache; import java.time.LocalDateTime; import java.util.concurrent.TimeUnit; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.util.StopWatch; import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) @SpringBootTest public class RedisCacheExpireTest { @Autowired MessageService messageService; // ここに、テストを書く!! }
「myCache」を使う方は、こんな感じに。5秒で有効期限が切れることが、確認できます。
@Test public void expire() throws InterruptedException { StopWatch stopWatch = new StopWatch(); // slow stopWatch.start(); Message m1 = messageService.build("Hello World"); stopWatch.stop(); LocalDateTime now1 = m1.getNow(); long elapsed1 = (long) stopWatch.getLastTaskInfo().getTimeSeconds(); assertThat(elapsed1) .isGreaterThanOrEqualTo(3L); // quick stopWatch.start(); Message m2 = messageService.build("Hello World"); stopWatch.stop(); LocalDateTime now2 = m2.getNow(); long elapsed2 = (long) stopWatch.getLastTaskInfo().getTimeSeconds(); assertThat(elapsed2) .isEqualTo(0L); assertThat(now2) .isEqualTo(now1); // cached TimeUnit.SECONDS.sleep(5L); // sleep // slow stopWatch.start(); Message m3 = messageService.build("Hello World"); stopWatch.stop(); LocalDateTime now3 = m3.getNow(); long elapsed3 = (long) stopWatch.getLastTaskInfo().getTimeSeconds(); assertThat(elapsed3) .isGreaterThanOrEqualTo(3L); assertThat(now3) .isNotEqualTo(now1); }
もうひとつ用意した方では、5秒ではキャッシュが有効期限切れせず、10秒経過後に有効期限切れすることが
確認できます。
@Test public void defaultExpire() throws InterruptedException { StopWatch stopWatch = new StopWatch(); // slow stopWatch.start(); Message m1 = messageService.buildAnother("Hello World Another"); stopWatch.stop(); LocalDateTime now1 = m1.getNow(); long elapsed1 = (long) stopWatch.getLastTaskInfo().getTimeSeconds(); assertThat(elapsed1) .isGreaterThanOrEqualTo(3L); // quick stopWatch.start(); Message m2 = messageService.buildAnother("Hello World Another"); stopWatch.stop(); LocalDateTime now2 = m2.getNow(); long elapsed2 = (long) stopWatch.getLastTaskInfo().getTimeSeconds(); assertThat(elapsed2) .isEqualTo(0L); assertThat(now2) .isEqualTo(now1); // cached TimeUnit.SECONDS.sleep(5L); // sleep // quick stopWatch.start(); Message m3 = messageService.buildAnother("Hello World Another"); stopWatch.stop(); LocalDateTime now3 = m3.getNow(); long elapsed3 = (long) stopWatch.getLastTaskInfo().getTimeSeconds(); assertThat(elapsed3) .isGreaterThanOrEqualTo(0L); assertThat(now3) .isEqualTo(now1); // cached TimeUnit.SECONDS.sleep(5L); // sleep // slow stopWatch.start(); Message m4 = messageService.build("Hello World Another"); stopWatch.stop(); LocalDateTime now4 = m4.getNow(); long elapsed4 = (long) stopWatch.getLastTaskInfo().getTimeSeconds(); assertThat(elapsed4) .isEqualTo(3L); assertThat(now4) .isNotEqualTo(now1); }
Redisに保存する時のシリアライズ形式を変更する
ここまでのコードでRedisにデータを保存した場合ですが、デフォルトはJDKのシリアライズの仕組みを使うため、
CLIなどで見るとこんな感じになります。
127.0.0.1:6379> keys * 1) "myCache:\xac\xed\x00\x05t\x00\x0bHello World" 2) "anotherCache:\xac\xed\x00\x05t\x00\x13Hello World Another" 127.0.0.1:6379> get "myCache:\xac\xed\x00\x05t\x00\x0bHello World" "\xac\xed\x00\x05sr\x00$org.littlewings.spring.cache.Message\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x02L\x00\x03nowt\x00\x19Ljava/time/LocalDateTime;L\x00\x05valuet\x00\x12Ljava/lang/String;xpsr\x00\rjava.time.Ser\x95]\x84\xba\x1b\"H\xb2\x0c\x00\x00xpw\x0e\x05\x00\x00\a\xe1\x02\x01\x00\x117-\xb7}\xc0xt\x00\x0bHello World" 127.0.0.1:6379> get "anotherCache:\xac\xed\x00\x05t\x00\x13Hello World Another" "\xac\xed\x00\x05sr\x00$org.littlewings.spring.cache.Message\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x02L\x00\x03nowt\x00\x19Ljava/time/LocalDateTime;L\x00\x05valuet\x00\x12Ljava/lang/String;xpsr\x00\rjava.time.Ser\x95]\x84\xba\x1b\"H\xb2\x0c\x00\x00xpw\x0e\x05\x00\x00\a\xe1\x02\x01\x00\x12(\nO\xc5@xt\x00\x13Hello World Another"
ちょっとこれだと読みづらかったりするので、保存形式を変えてみましょう。
今回は、キーをString、値をJSON(Jackson2使用)に変更してみます。
@EnableCachingを付与したJavaConfigクラスを、次のように変更。
src/main/java/org/littlewings/spring/cache/CacheConfig.java
package org.littlewings.spring.cache; import java.util.HashMap; import java.util.Map; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration @EnableCaching public class CacheConfig { @Bean public RedisTemplate<Object, Object> redisTemplate(JedisConnectionFactory jedisConnectionFactory) { RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(jedisConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Message.class)); redisTemplate.setHashKeySerializer(redisTemplate.getKeySerializer()); redisTemplate.setHashValueSerializer(redisTemplate.getValueSerializer()); redisTemplate.afterPropertiesSet(); return redisTemplate; } @Bean public CacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) { RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate); cacheManager.setUsePrefix(true); // true => key prefix cache-name Map<String, Long> expires = new HashMap<>(); expires.put("myCache", 5L); // key is cache-name cacheManager.setExpires(expires); // expire per cache cacheManager.setDefaultExpiration(10L); // default expire cacheManager.afterPropertiesSet(); return cacheManager; } }
RedisTemplateについての設定を追加します。
@Bean public RedisTemplate<Object, Object> redisTemplate(JedisConnectionFactory jedisConnectionFactory) { RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(jedisConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Message.class)); redisTemplate.setHashKeySerializer(redisTemplate.getKeySerializer()); redisTemplate.setHashValueSerializer(redisTemplate.getValueSerializer()); redisTemplate.afterPropertiesSet(); return redisTemplate; }
Jackson2JsonRedisSerializerを使う場合は、対象のクラスを指定する必要があるので、今回作成したクラスの
Classクラスを指定しています。
保存対象のクラスに、LocalDateTimeを使っていたのでJacksonのモジュールを追加します。
<dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency>
コードの方も、ちょっと変更。
src/main/java/org/littlewings/spring/cache/Message.java
package org.littlewings.spring.cache; import java.io.Serializable; import java.time.LocalDateTime; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; public class Message implements Serializable { private static final long serialVersionUID = 1L; private String value; @JsonSerialize(using = LocalDateTimeSerializer.class) @JsonDeserialize(using = LocalDateTimeDeserializer.class) private LocalDateTime now; public Message() { } public Message(String value) { this.value = value; now = LocalDateTime.now(); } public String getValue() { return value; } public LocalDateTime getNow() { return now; } }
で、先ほどのテストコード(有効期限を指定した時に使ったもの)を実行した時に、CLIで見るとこのように。
127.0.0.1:6379> keys * 1) "myCache:Hello World" 2) "anotherCache:Hello World Another" 127.0.0.1:6379> get "myCache:Hello World" "{\"value\":\"Hello World\",\"now\":[2017,2,1,0,22,26,982000000]}" 127.0.0.1:6379> get "anotherCache:Hello World Another" "{\"value\":\"Hello World Another\",\"now\":[2017,2,1,0,22,38,118000000]}"
ちゃんと保存形式が変わりましたね?
今回、キーをStringにしましたが、こうすると今回のServiceクラスの定義だと引数を複数にすると
@Cacheable(cacheNames = "decorateCache") public Message decorate(String message, String prefix, String suffix) { try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { // ignore } return new Message(prefix + message + suffix); }
エラーになったりします。
java.lang.ClassCastException: org.springframework.cache.interceptor.SimpleKey cannot be cast to java.lang.String
まあ、その場合はStringにまとめられるように、なんとかしましょうということで。
@Cacheable(cacheNames = "decorateCache", key = "#prefix + #message + #suffix") public Message decorate(String message, String prefix, String suffix) { try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { // ignore } return new Message(prefix + message + suffix); }
まとめ
SpringのCache Abstractionで、Redisをキャッシュの保存先として使ってみました。
使うにあたっていくつか気になるところがあったので試しつつな感じでしたが、ちょっとずつ使い方がわかってきたので
まあ良しとしましょう。