Spring Bootでは同じインタフェースを持つインスタンス群をDIによりリストにインジェクト可能だが、取り出し方が直感的でない気がする。

ということでリストにインジェクトされたインスタンスを別のマップに移すことにより、プラグインのような形で利用しやすくする方法を調査。

概要

初期起動時

コンフィグレーションクラスにおいて、リストにインジェクトされたインスタンスを@PostConstractを付加したメソッドにより、何らかのキーと共に別のマップに格納する。

利用時

格納時に使ったキーを使ってマップからインスタンスを取り出して利用する。

実装

元々データバインド用に作った複雑なものをサンプルとして簡単にした(つもりだったがあまり簡単にならなかった)。

springboot-study/plugin-implementation-study at master · orimajp/springboot-study

アニマルインタフェースとその実装クラス

インタフェース

package com.example.pluginimplementationstudy.animal;

public interface Animal {

	String getName();

	String getCry();

}

実装クラス(サンプルとして一つのみ)

package com.example.pluginimplementationstudy.animal.impl;

import com.example.pluginimplementationstudy.animal.Animal;

public class Cat implements Animal {

	@Override
	public String getName() {
		return "猫";
	}

	@Override
	public String getCry() {
		return "ニャー";
	}

}

動物鳴かせ機能インタフェースとその実装クラス

インタフェース

package com.example.pluginimplementationstudy.crying;

import com.example.pluginimplementationstudy.animal.Animal;

public interface Crying<T extends Animal> {

	default String getMessage(String name, String cry) {
		return String.format("%s は %s と鳴く。", name, cry);
	}

	String cry(T t);

	// この例では、以下のようにすれば実装クラスでのメソッド実装は必須ではなくなるが...
/*	default String cry(T t) {
		return getMessage(t.getName(), t.getCry());
	}*/

}

実装クラス(サンプルとして一つのみ)

package com.example.pluginimplementationstudy.crying.impl;

import com.example.pluginimplementationstudy.animal.impl.Cat;
import com.example.pluginimplementationstudy.crying.Crying;

public class CatCrying implements Crying<Cat> {

	@Override
	public String cry(Cat cat) {
		return getMessage(cat.getName(), cat.getCry());
	}

}

動物鳴かせ機能インタフェースのコンフィグレーションクラス

このクラスにあるリストに動物鳴かせ機能インタフェースのインスタンスがインジェクトされる。

苦手なリフレクションを使う最高難度クラス。もっといいやり方がありそうだが、ここまでくるまでに数日かかったので挫折。

package com.example.pluginimplementationstudy.crying;

import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;

@Configuration
@RequiredArgsConstructor
public class CryingConfig {

	@NonNull
	private final List<Crying> cryings;

	@NonNull
	private final CryingStore cryingStore;

	@PostConstruct
	public void initializeCryingStore() {
		if (cryings == null) {
			throw new IllegalArgumentException("Crying未発見。");
		}

		for (Crying crying: cryings) {
			Type[] types = crying.getClass().getGenericInterfaces();
			Type[] actualTypeArguments = ((ParameterizedType)types[0]).getActualTypeArguments();

			final String className = actualTypeArguments[0].getTypeName();
			final Class clazz;
			try {
				clazz = Class.forName(className);
			} catch (ClassNotFoundException e) {
				throw new RuntimeException("クラスが見つかりません。class=[".concat(className).concat("]"), e);
			}
			cryingStore.registCrying(clazz, crying);
		}
	}

}

動物鳴かせ機能実装クラスBean定義コンフィグレーションクラス

package com.example.pluginimplementationstudy.crying;

import com.example.pluginimplementationstudy.crying.impl.CatCrying;
import com.example.pluginimplementationstudy.crying.impl.DogCrying;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CryingImplConfig {

	@Bean
	public CatCrying catCrying() {
		return new CatCrying();
	}

	@Bean
	public DogCrying dogCrying() {
		return new DogCrying();
	}

}

動物鳴かせ機能インタフェースマップ保持クラス

クラスとその関連クラスをセットにしているが、定数値など無関係な値をキーに使う用法も考えられる。

package com.example.pluginimplementationstudy.crying;

import org.springframework.stereotype.Component;

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

@Component
public class CryingStore {

	private final Map<Class, Crying> cryingMap = new HashMap<>();

	public void registCrying(Class clazz, Crying crying) {
		cryingMap.put(clazz, crying);
	}

	public Crying getCrying(Class clazz) {
		return cryingMap.get(clazz);
	}

}

動物鳴かせ機能クラスのファクトリクラス

package com.example.pluginimplementationstudy.crying;

import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class CryingFactory {

	@NonNull
	private final CryingStore cryingStore;

	public  Crying getCrying(Class clazz) {
		final Crying crying = cryingStore.getCrying(clazz);
		if (crying == null) {
			throw new IllegalArgumentException(clazz.toString() + "は鳴きません。");
		}
		return crying;
	}

}

動作確認用メッセージクラス

package com.example.pluginimplementationstudy;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Message {

	private String message;

}

利用クラス(コントローラ)

アニマルインタフェース実装クラスに対応する動物鳴かせ機能クラスを取得し、アニマルインタフェース実装クラスのインスタンスを渡している。

本来はどこかで生成したクラスを使えばいいが、処理を短くしようとしてわかりにくくなってしまった。

package com.example.pluginimplementationstudy;

import com.example.pluginimplementationstudy.animal.Animal;
import com.example.pluginimplementationstudy.animal.impl.Cat;
import com.example.pluginimplementationstudy.animal.impl.Dog;
import com.example.pluginimplementationstudy.crying.CryingFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

@RestController
@RequiredArgsConstructor
public class TopController {

	private final CryingFactory cryingFactory;

	@RequestMapping(value = "/", method = RequestMethod.GET)
	public ResponseEntity index() throws IllegalAccessException, InstantiationException {

		final Class[] classes = {Cat.class, Dog.class};

		final List<Message> messages = new ArrayList<>();
		for (Class clazz: classes) {
			final Animal animal= (Animal)clazz.newInstance();
			messages.add(new Message(cryingFactory.getCrying(clazz).cry(animal)));
		}

		return ResponseEntity.ok(messages);
	}

}

動作結果

[
	{
		message: "猫 は ニャー と鳴く。"
	},
	{
		message: "犬 は ワン と鳴く。"
	}
]