【Spring】Spring bootのCommandLineRunnerで自作スコープを使ってみる。

## やりたいこと
SpringBootで簡易的なコマンドラインツールを作成するにあたって、順次処理していくタスク1件ごとに処理で使うBeanのインスタンスを切り換えたい。つまり使用するBeanのライフサイクルをタスクと同一にしたいのですが、そこで***Scopeを自作してはどうか***、と思い至りました。

## Scopeとは
一応説明。DIコンテナにBeanを登録する際、そのBeanがどのライフサイクルで生成・破棄されるかの指定のことです。
Springが元から用意している代表的なもので、ライフサイクルがWebアプリへのリクエスト単位となる、`request`スコープ、セッション単位となる`session`などがあり、`Scope`アノテーションを使って表現します。

``` java 
@Scope("request")
```

## 実装してみた。
今回は`task`スコープを作ってみます。

### スコープの解決を実装

`org.springframework.beans.factory.config.Scope`インターフェースの`get`メソッドにて、コンテナがBeanを要求されたときの処理を書いてゆくのですが、ここでは要求されているBeanの名称が文字列として渡ってくるので、

- **要求されたBeanがそのスコープ内でまだ生成されていないなら、ObjectFactoryから生成**
- **要求されたBeanがそのスコープ内で既に生成されているなら、それを返却**

という処理にすることで同一のスコープ内でインスタンスを共有しつつ、他のスコープとは取得できるインスタンスを明確に分離することができます。
後に説明しますが、TaskScopeResolverに外からタスク番号を操作できる振る舞い（ここではnextTaskメソッド）を持たせることを忘れずに。
（アクセスをパッケージにしてるのは、変なとこから呼び出されないようにするためです。）

``` java:TaskScopeResolver.java
package com.demo.cl.task;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;

class TaskScopeResolver implements Scope {

	private Map<Key, Object> beanStock = Collections.synchronizedMap(new HashMap<>());

	private int taskNumber = 0;

	/*
	 * タスク切り替えメソッド
	 */
	public void nextTask() {
		taskNumber++;
	}

	/*
	 * ここではMapをストックにして、一度生成したBeanをスコープ識別子とそのbean名をKeyに保存し、
	 * 同一スコープなら保存されたインスタンスを取り出します。
	 * 今回スコープの識別子に変数taskNumberを用いています。
	 * のちに触れますが、この変数が変化すること=スコープが切り替わることを意味します。
	 */
	@Override
	public Object get(String name, ObjectFactory<?> objectFactory) {
		Key key = new Key(name, taskNumber);
		if (beanStock.containsKey(key)) {
			return beanStock.get(key);
		}
		Object bean = objectFactory.getObject();
		beanStock.put(key, bean);
		return bean;
	}

    //割愛
```

### BeanFactoryPostProcessorでBeanFactoryにスコープを登録
作った`TaskScopeResolver`をアプリケーションに適用させます。
`BeanFactoryPostProcessor`を実装したクラスをコンテナに登録しましょう。

``` java:ScopeRegisterBeanFactoryPostProcessor.java
package com.demo.cl.task;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ScopeRegisterBeanFactoryPostProcessor implements BeanFactoryPostProcessor{
	
	@Override
	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
		beanFactory.registerScope("task", getTaskScopeResolver());
	}
	
	@Bean
	public TaskScopeResolver getTaskScopeResolver() {
		return new TaskScopeResolver();
	}

}

```
ここでのミソは、ちゃっかりTaskScopeResolver`をコンテナに登録してるところです。
ここで登録した`TaskScopeResolver`インスタンスは他のところからも参照できなければまずい（後ほど）ので、`Configuration`にしてBeanに登録することも忘れずに。

### 動かしてみる。その前に・・・
スコープを登録したところで、早速ツールの開発！といきたいところですが、やらなくてはならないことが。
当然と言えば当然ですが、実行時にタスクの順次処理に応じて***スコープの切り替えを明示的に行う***必要があります。
可能ならビジネスロジック側にスコープの切り替えを書かせたく無いので、簡単なフレームワークを作ります。

#### インターフェースを用意
このexecメソッドには順次処理のうち1件の処理を実装させます。

``` java:Task.java
package com.demo.cl.task;

public interface Task<T> {
	void exec(T arg) throws Exception;
}
```
#### Taskの呼び出し部分を実装
`ApplicationContext#getBean`でTaskの実装クラス（フレームワークはそれを知らない）を取得し、タスクを実行するクラス、`ScopedTaskRunner`を作ります。
先ほどBeanに登録した`TaskScopeResolver#nextTask`で、次タスクへスコープを切り替えています。

``` java:ScopedTaskRunner.java
package com.demo.cl.task;

import java.util.Collection;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

@Component
public class ScopedTaskRunner<T> {

	@Autowired
	private ApplicationContext context;
	
	@Autowired
	private TaskScopeResolver resolver;

	@SuppressWarnings("unchecked")
	public void run(Collection<T> taskList) {
		taskList.forEach(arg -> {
			try {
				context.getBean(Task.class).exec(arg);
				resolver.nextTask();
			} catch (Exception e) {
				e.printStackTrace();
			}
		});
	}
}
```

### コマンドラインツールを実装
ここでは仮に整数を引数にとって何かしらを実行するTaskを考えます。
面倒なのでここは単にthisを標準出力させて毎回異なるインスタンスが実行されているかを検証しましょう。
Taskの実装を用意します。

``` java:TaskImpl.java
package com.demo.cl.sample;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import com.demo.cl.task.Task;

@Component
@Scope("task")
public class TaskImpl implements Task<Integer>{
	
	@Override
	public void exec(Integer arg) throws InterruptedException {
		System.out.println(arg + " : " + this);
	}

}

```

あとはCommandLineRunnerで`ScopedTaskRunner`を呼び出すだけ！

```java:ClAppDemoApplication.java
package com.demo.cl.sample;

import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import com.demo.cl.task.ScopedTaskRunner;

@SpringBootApplication(scanBasePackages="com.demo.cl")
public class ClAppDemoApplication implements CommandLineRunner {
	
	public static void main(String[] args) {
		SpringApplication.run(ClAppDemoApplication.class, args);
	}
	
	@Autowired
	private ScopedTaskRunner<Integer> runner;
	
	@Override
	public void run(String... args) throws Exception {
		runner.run(IntStream.range(0, 10).boxed().collect(Collectors.toList()));
	}

}

```

```:出力
0 : com.demo.cl.sample.TaskImpl@14a54ef6
1 : com.demo.cl.sample.TaskImpl@20921b9b
2 : com.demo.cl.sample.TaskImpl@867ba60
3 : com.demo.cl.sample.TaskImpl@5ba745bc
4 : com.demo.cl.sample.TaskImpl@654b72c0
5 : com.demo.cl.sample.TaskImpl@55b5e331
6 : com.demo.cl.sample.TaskImpl@6034e75d
7 : com.demo.cl.sample.TaskImpl@15fc442
8 : com.demo.cl.sample.TaskImpl@3f3c7bdb
9 : com.demo.cl.sample.TaskImpl@456abb66
```
ご覧の通り、毎回異なるインスタンスが実行されていることが確認できたと思います。
ただこれ、`prototype`スコープ（要求の度にBeanがnewされる。）でも同じことができてしまいますが、タスク内でインスタンスが共有できているか検証するのは、サンプルコードが少し煩雑になってしまうので割愛します:sweat_smile:（ちゃんとできます。）