Spring Data RedisでSubscribeするときに@BeanでRedisMessageListenerContainerを作りたくない

# 概要
Spring Data RedisでSubscribeするときに、下記のように@BeanでRedisMessageListenerContainerを定義します。
これはこれで動作するのですが、あまりに宣言的であり、柔軟性に欠けるので、もう少し動的に出来ないか？というあいまいな要求から調査した結果を書き留める記事です。
※たぶん大半の人には役に立たないゴミ記事であることを宣言しておきます。

```java:RedisConfig.java
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以外のソース
```java:Publisher.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);
	}
}
```

```java:Subscriber.java
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());
	}
}
```

これで下記のテストを動かす。

```java:ApplicationTests.java
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で使えば良さそうなことが分かった。

```java:Runner.java
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を消してみる。

```java:RedisConfig.java
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というメソッドを呼べば良さそうな雰囲気を感じたので呼んでみることに。

```java:Runner.java
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とか雰囲気で使ってたので、その辺の理解が深まったのが収穫でしょうか。

# 参考
1. [Spring Boot GenericApplicationContext tutorial - using GenericApplicationContext in a Spring application](http://zetcode.com/springboot/genericapplicationcontext/)
2. [関数型インタフェース(Supplier)を利用してBean登録できる :thumbsup:](https://qiita.com/kazuki43zoo/items/7f4e97978ae6631ad281#%E9%96%A2%E6%95%B0%E5%9E%8B%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%95%E3%82%A7%E3%83%BC%E3%82%B9supplier%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%A6bean%E7%99%BB%E9%8C%B2%E3%81%A7%E3%81%8D%E3%82%8B-thumbsup)
3. [spring-data-redis/RedisMessageListenerContainer.java at master · spring-projects/spring-data-redis](https://github.com/spring-projects/spring-data-redis/blob/master/src/main/java/org/springframework/data/redis/listener/RedisMessageListenerContainer.java)
4. [Java implementation redis storage timing data aging event posting subscription - Programmer Sought](https://programmersought.com/article/4283836217/)