LoginSignup
0
0

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-12-29

やりたいこと

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メソッド)を持たせることを忘れずに。
(アクセスをパッケージにしてるのは、変なとこから呼び出されないようにするためです。)

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を実装したクラスをコンテナに登録しましょう。

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件の処理を実装させます。

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で、次タスクへスコープを切り替えています。

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の実装を用意します。

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を呼び出すだけ!

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:(ちゃんとできます。)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0