Spring Session 1.1.0で、Hazelcastのサポートが追加されました。
このHazelcastのサポートまわりのコードを眺めていて、これなら他のものでもやれそうでは?と思い、Spring SessionのInfinispan版を書いてみました。
https://github.com/kazuhira-r/spring-session-infinispan
まあ、言ってしまうとほぼHazelcastでのコードをInfinispanに書き換えたものなので、まんまと言えばまんまです。
Embedded ModeとClient/Server Mode(Hot Rod)の両方を書いていて、テストコードも(ほぼHazelcast向けコードまんまですが)書いています。一部、Spring Sessionのテストコードから拝借したものも。
有効化のための各アノテーションの定義はこんな感じで、セッションを保持するCacheの名前はとりあえず「springSessions」にしておきました。
Embedded向け。
src/main/java/org/littlewings/spring/session/infinispan/embedded/config/annotation/web/http/EnableInfinispanEmbeddedHttpSession.java
package org.littlewings.spring.session.infinispan.embedded.config.annotation.web.http; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(InfinispanEmbeddedHttpSessionConfiguration.class) @Configuration public @interface EnableInfinispanEmbeddedHttpSession { int maxInactiveIntervalInSeconds() default 1800; String sessionsCacheName() default "springSessions"; }
Client/Server(Hot Rod)向け。
src/main/java/org/littlewings/spring/session/infinispan/remote/config/annotation/web/http/EnableInfinispanRemoteHttpSession.java
package org.littlewings.spring.session.infinispan.remote.config.annotation.web.http; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(InfinispanRemoteHttpSessionConfiguration.class) @Configuration public @interface EnableInfinispanRemoteHttpSession { int maxInactiveIntervalInSeconds() default 1800; String sessionsCacheName() default "springSessions"; }
使う時には、Embedded Modeならinfinispan-coreを、Client/Server Modeなら「infinispan-client-hotrod」を足す感じで。
<dependency> <groupId>org.infinispan</groupId> <artifactId>infinispan-core</artifactId> <version>8.2.2.Final</version> </dependency> <dependency> <groupId>org.infinispan</groupId> <artifactId>infinispan-client-hotrod</artifactId> <version>8.2.2.Final</version> </dependency>
Embedded Modeを使うサンプルはこんな感じで、EmbeddedCacheManagerをBean定義します。
※HttpSessionを扱ってるところはテキトーですが
@SpringBootApplication @EnableInfinispanEmbeddedHttpSession @RestController public class App { public static void main(String... args) { SpringApplication.run(App.class, args); } @RequestMapping("session") public Map<String, String> session(HttpSession session) { LocalDateTime now = (LocalDateTime) session.getAttribute("now"); if (now == null) { now = LocalDateTime.now(); session.setAttribute("now", now); } Map<String, String> response = new HashMap<>(); response.put("isNew", Boolean.toString(session.isNew())); response.put("now", now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); return response; } @Bean public EmbeddedCacheManager embeddedCacheManager() { EmbeddedCacheManager cacheManager = new DefaultCacheManager(new GlobalConfigurationBuilder().transport().defaultTransport().build()); cacheManager .defineConfiguration("springSessions", new org.infinispan.configuration.cache.ConfigurationBuilder().clustering().cacheMode(CacheMode.DIST_SYNC).build()); return cacheManager; } }
Client/Server Modeならこんな感じ。RemoteCacheManagerをBean定義します。Client/Server Modeの場合は、あらかじめServer側にCacheを定義しておく必要がありますが…。
@SpringBootApplication @EnableInfinispanRemoteHttpSession @RestController public class App { public static void main(String... args) { SpringApplication.run(App.class, args); } @RequestMapping("session") public Map<String, String> session(HttpSession session) { LocalDateTime now = (LocalDateTime) session.getAttribute("now"); if (now == null) { now = LocalDateTime.now(); session.setAttribute("now", now); } Map<String, String> response = new HashMap<>(); response.put("isNew", Boolean.toString(session.isNew())); response.put("now", now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); return response; } @Bean public RemoteCacheManager remoteCacheManager() { return new RemoteCacheManager( new org.infinispan.client.hotrod.configuration.ConfigurationBuilder() .addServers("localhost:11222") .build()); } }
作ってみて
Spring SessionにMapSessionRepositoryというものがあって、こちらを使うとMapベースのものならけっこう簡単に作れるんだなーということがわかりました。
https://github.com/spring-projects/spring-session/blob/1.2.0.RELEASE/spring-session/src/main/java/org/springframework/session/MapSessionRepository.java
まあ、ひととおりプログラム書いて、テストコード書いて、サンプル書いて試してみたところで、初めてSpring Sessionを使ったコードを書いたことにふと気付きましたが…。
気になったこと、悩んだこと
ExpiredEvent発火のタイミング
Infinispanのexpirationのデフォルト設定だと、wake-up-intervalが1分になっていて、expireこそするもののExpiredEventがすぐに発火せずにテストコードではこの値を縮める必要がありました…。
Cluster Listener
Client/Server ModeとEmbedded Mode(Distributed Cache)で、イベントを受け取るNodeの数が違うことがあるんじゃないかなぁと。最初は対称性を取ろうとして、Embedded Modeの場合はCluster Listenerにして全Nodeがイベントを受け取るようにした方がいいんじゃないかな?と思ったのですが、どうなのでしょう。
Spring Sessionの他の実装を見ていると、Client/Server Modeのものかそうでないものか(Distributed/Partitionedなものを使っている場合)で、イベントを受け取るNode数が変わるような気も…。
今回は、通常のListenerにしてDistributed Cacheの時はエントリのOwnerのNodeがイベントを受け取るようになっています。
Eviction
EvictedEventの扱いは、悩みどころでした。Persistenceを考えると、Evictされる=Removeされるは必ずしも成立しないですし。ただ、Client/Server ModeのListenerはEvictedEventをサポートしていないので、今回はEmbedded Modeの場合も外しておきました。
なお、HazelcastのListenerがExpireとEvictをそう区別していないことに、ここで気付きましたが…。