やりたいこと
SpringBootで簡易的なコマンドラインツールを作成するにあたって、順次処理していくタスク1件ごとに処理で使うBeanのインスタンスを切り換えたい。つまり使用するBeanのライフサイクルをタスクと同一にしたいのですが、そこでScopeを自作してはどうか、と思い至りました。
Scopeとは
一応説明。DIコンテナにBeanを登録する際、そのBeanがどのライフサイクルで生成・破棄されるかの指定のことです。
Springが元から用意している代表的なもので、ライフサイクルがWebアプリへのリクエスト単位となる、request
スコープ、セッション単位となるsession
などがあり、Scope
アノテーションを使って表現します。
@Scope("request")
実装してみた。
今回はtask
スコープを作ってみます。
スコープの解決を実装
org.springframework.beans.factory.config.Scope
インターフェースのget
メソッドにて、コンテナがBeanを要求されたときの処理を書いてゆくのですが、ここでは要求されているBeanの名称が文字列として渡ってくるので、
- 要求されたBeanがそのスコープ内でまだ生成されていないなら、ObjectFactoryから生成
- 要求されたBeanがそのスコープ内で既に生成されているなら、それを返却
という処理にすることで同一のスコープ内でインスタンスを共有しつつ、他のスコープとは取得できるインスタンスを明確に分離することができます。
後に説明しますが、TaskScopeResolverに外からタスク番号を操作できる振る舞い(ここではnextTaskメソッド)を持たせることを忘れずに。
(アクセスをパッケージにしてるのは、変なとこから呼び出されないようにするためです。)
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
を実装したクラスをコンテナに登録しましょう。
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件の処理を実装させます。
package com.demo.cl.task;
public interface Task<T> {
void exec(T arg) throws Exception;
}
Taskの呼び出し部分を実装
ApplicationContext#getBean
でTaskの実装クラス(フレームワークはそれを知らない)を取得し、タスクを実行するクラス、ScopedTaskRunner
を作ります。
先ほどBeanに登録したTaskScopeResolver#nextTask
で、次タスクへスコープを切り替えています。
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の実装を用意します。
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
を呼び出すだけ!
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される。)でも同じことができてしまいますが、タスク内でインスタンスが共有できているか検証するのは、サンプルコードが少し煩雑になってしまうので割愛します(ちゃんとできます。)