概要
Spring Data RedisでSubscribeするときに、下記のように@BeanでRedisMessageListenerContainerを定義します。
これはこれで動作するのですが、あまりに宣言的であり、柔軟性に欠けるので、もう少し動的に出来ないか?というあいまいな要求から調査した結果を書き留める記事です。
※たぶん大半の人には役に立たないゴミ記事であることを宣言しておきます。
package com.example.demo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
@Configuration
public class RedisConfig {
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(new RedisStandaloneConfiguration("aa.bb.cc.dd", 6379));
}
// Subscriber
@Bean
RedisMessageListenerContainer redisContainer() {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory());
container.addMessageListener(new Subscriber(), new ChannelTopic("channel1"));
return container;
}
}
RedisConfig.java以外のソース
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class Publisher {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public void publish(String channel, String message) {
redisTemplate.convertAndSend(channel, message);
}
}
package com.example.demo;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
public class Subscriber implements MessageListener {
public void onMessage(Message message, byte[] pattern) {
System.out.println(new String(pattern) + "/" + message.toString());
}
}
これで下記のテストを動かす。
package com.example.demo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class ApplicationTests {
@Autowired
Publisher publisher;
@Test
void contextLoads() {
publisher.publish("channel1", "message1");
}
}
出力
channel1/message1
ということで、動作するのが確認できた。
@Beanを消したい
もろもろググった結果(参考文献.1参照)、GenericApplicationContext#registerBeanというのをCommandLineRunnerで使えば良さそうなことが分かった。
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;
@Component
public class Runner implements CommandLineRunner {
@Autowired
GenericApplicationContext context;
@Autowired
LettuceConnectionFactory redisConnectionFactory;
@Override
public void run(String... args) throws Exception {
context.registerBean(RedisMessageListenerContainer.class, () -> {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
container.addMessageListener(new Subscriber(), new ChannelTopic("channel1"));
return container;
});
}
}
RedisConfig.javaから、@Beanを消してみる。
package com.example.demo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
@Configuration
public class RedisConfig {
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(new RedisStandaloneConfiguration("13.231.239.64", 6379));
}
// Subscriber
// @Bean
// RedisMessageListenerContainer redisContainer() {
// RedisMessageListenerContainer container = new RedisMessageListenerContainer();
// container.setConnectionFactory(redisConnectionFactory());
// container.addMessageListener(new Subscriber(), new ChannelTopic("channel1"));
// return container;
// }
}
出力なし
なぜか正しく動作しない。context.getBean(RedisMessageListenerContainer.class)したところ、Beanは登録はされているようだ。そこで、@Beanした場合と、registerBeanした場合のオブジェクトをそれぞれ比べてみることにした。すると、RedisMessageListenerContainerのstartというフィールドに違いがあることが分かった。
- @Beanした場合はstart=true
- registerBeanした場合はstart=false
startしてみる
RedisMessageListenerContainer#startというメソッドを呼べば良さそうな雰囲気を感じたので呼んでみることに。
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;
@Component
public class Runner implements CommandLineRunner {
@Autowired
GenericApplicationContext context;
@Autowired
LettuceConnectionFactory redisConnectionFactory;
@Override
public void run(String... args) throws Exception {
context.registerBean(RedisMessageListenerContainer.class, () -> {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
container.addMessageListener(new Subscriber(), new ChannelTopic("channel1"));
return container;
});
RedisMessageListenerContainer obj = context.getBean(RedisMessageListenerContainer.class);
obj.start();
}
}
return container;の前にも入れてみたのだが、このケースは正しく動作しなかった。
上記のように、一度registerしてからgetBeanで取り出して、その後startメソッドを呼ぶとうまくいった。
出力
channel1/message1
結論
一応@Beanを消すという当初の目的は達成したのと、GenericApplicationContext#registerBeanを使うことで代替出来ることが分かりました。
ただ、startする・しないの挙動の違いもあるので、RedisMessageListenerContainerについては、Spring Data Redisのドキュメント通り、@Beanで登録する方法を推奨されると思います。
参考文献.2によると、Spring Framework 5.0からの機能だそうですが、これが具体的にどう役に立つのかは良く分かりません。。@Beanとか雰囲気で使ってたので、その辺の理解が深まったのが収穫でしょうか。
参考
- Spring Boot GenericApplicationContext tutorial - using GenericApplicationContext in a Spring application
- 関数型インタフェース(Supplier)を利用してBean登録できる
- spring-data-redis/RedisMessageListenerContainer.java at master · spring-projects/spring-data-redis
- Java implementation redis storage timing data aging event posting subscription - Programmer Sought