最近は Java を使っていても余り DI を使わないようにしているのだけど、たまに「インスタンスを構築するのが面倒くさいな〜」ということがある。具体的には、ちょっとした小さなアプリケーションを作っているとき、「コンポーネントは interface
ベースで書いているけど、DI コンテナは大袈裟だし、ファクトリを作るのも面倒くさいな〜(あるいは static
な Factory
に依存したくない)」みたいなケースがそれに当たる。
そういうときは簡易なコンテナを作って適当に済ますことが多い。コンテナと言っても勝手に登録されているインスタンスを組み合わせてくれるだけのシンプルなもの。AOP なども出来ないので、本当にただのファクトリの代わりに使っている。具象クラスを登録しておけば interface
ベースで任意のコンポーネントを取得できる。
/**
* 簡易なコンポーネントコンテナです。
*/
public interface ComponentProvider {
/**
* クラスを指定してコンポーネントを取得します。
* <p>
* 対応するコンポーネントが複数存在する場合・もしくはコンポーネントが存在しない場合には例外が送出されます。
*
* @param componentClass コンポーネントの型
* @param <T> コンポーネントの型
* @return コンポーネント
*/
@Nonnull
<T> T getComponent(@Nonnull Class<? extends T> componentClass);
/**
* クラスを指定して複数のコンポーネントを取得します。
*
* @param componentClass コンポーネントの型
* @param <T> コンポーネントの型
* @return コンポーネント
*/
@Nonnull
<T> List<? extends T> getComponents(@Nonnull Class<? extends T> componentClass);
}
だいたい Singleton なのでここでは Singleton 限定。
/**
* 簡易なコンポーネントコンテナの実装です。
*/
@Slf4j
public class Container implements ComponentProvider {
private final Map<String, Provider<?>> cache;
public Container() {
cache = new ConcurrentHashMap<>();
}
public <T> void registerComponent(@Nonnull Class<? super T> componentName, @Nonnull Provider<? extends T> provider) {
registerComponent(componentName.getName(), provider);
}
public <T> void registerComponent(@Nonnull String componentName, @Nonnull Provider<? extends T> provider) {
cache.putIfAbsent(componentName, provider);
}
public <T> void registerComponent(@Nonnull T component) {
registerComponent(component.getClass().getName(), component);
}
public <T> void registerComponent(@Nonnull Class<? super T> componentName, @Nonnull T component) {
registerComponent(componentName.getName(), component);
}
public <T> void registerComponent(@Nonnull String componentName, @Nonnull T component) {
registerComponent(componentName, new SingletonProvider<>((Class<? super T>) component.getClass(), component));
}
public <T> void registerComponent(@Nonnull Class<? super T> componentName, @Nonnull Class<? extends T> componentClass) {
registerComponent(componentName.getName(), componentClass);
}
public <T> void registerComponent(@Nonnull String componentName, @Nonnull Class<? extends T> componentClass) {
cache.putIfAbsent(componentName, new LazySingletonProvider<>(componentClass));
}
@Override
@Nonnull
public <T> T getComponent(@Nonnull Class<? extends T> componentClass) {
return getComponent(componentClass, true);
}
@Nullable
private <T> T getComponent(@Nonnull Class<? extends T> componentClass, boolean throwException) {
Provider<?> provider = cache.get(componentClass);
if (provider != null) {
return (T) provider.getComponent();
}
List<? extends T> components = getComponents(componentClass);
if (components.isEmpty() && throwException) {
throw new ComponentNotFoundException(componentClass);
}
if (components.size() == 1) {
return components.iterator().next();
}
if (throwException) {
throw new TooManyComponentException(componentClass);
}
return null;
}
@Override
@Nonnull
public <T> List<? extends T> getComponents(@Nonnull Class<? extends T> componentClass) {
return (List<? extends T>) cache.values().stream().filter(provider -> provider.test(componentClass)).map(provider -> provider.getComponent()).collect(Collectors.toList());
}
public interface Provider<T> extends Predicate<Class<?>> {
@Nonnull
Class<? super T> getComponentClass();
@Nonnull
T getComponent();
default boolean test(@Nonnull Class<?> clazz) {
Class<?> componentClass = getComponentClass();
do {
if (clazz.isAssignableFrom(componentClass)) {
return true;
}
for (Class<?> interfaceClass : componentClass.getInterfaces()) {
if (clazz.isAssignableFrom(interfaceClass)) {
return true;
}
}
componentClass = componentClass.getSuperclass();
} while (componentClass != null);
return false;
}
}
@RequiredArgsConstructor
private class SingletonProvider<T> implements Provider<T> {
@Nonnull
private final Class<? super T> componentClass;
@Nonnull
private final T component;
@Nonnull
@Override
public Class<? super T> getComponentClass() {
return componentClass;
}
@Nonnull
@Override
public T getComponent() {
return component;
}
}
@RequiredArgsConstructor
private class LazySingletonProvider<T> implements Provider<T> {
@Nonnull
private final Class<T> componentClass;
@Nonnull
private final AtomicReference<T> componentReference = new AtomicReference<>();
@Override
@Nonnull
public Class<? super T> getComponentClass() {
return componentClass;
}
@Nonnull
@Override
public T getComponent() {
T component = componentReference.get();
if (component == null) {
component = instantiate();
componentReference.compareAndSet(null, component);
}
return component;
}
private T instantiate() {
// コンストラクタ引数の長い順に走査してインスタンス化を試みる
for (Constructor<?> constructor : collect(getComponentClass())) {
Class<?>[] types = constructor.getParameterTypes();
List<?> args = Arrays.stream(types).map(type -> Container.this.getComponent(type, false)).filter(Objects::nonNull).collect(Collectors.toList());
if (types.length == args.size()) {
try {
return (T) constructor.newInstance(args.toArray());
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
log.warn("Failed to create a component: " + componentClass.getName(), e);
continue;
}
}
}
throw new ComponentInstantiationException(getComponentClass());
}
private List<Constructor<?>> collect(Class<?> componentClass) {
return Arrays
.stream(componentClass.getConstructors())
.filter(constructor -> Modifier.isPublic(constructor.getModifiers()))
.sorted(Comparator.comparing(constructor -> constructor.getParameterTypes().length, Comparator.reverseOrder()))
.collect(Collectors.toList());
}
}
private static class ComponentNotFoundException extends RuntimeException {
ComponentNotFoundException(@Nonnull Class<?> componentClass) {
super(String.format("Failed to get a component: class=%s", componentClass.getName()));
}
}
private static class TooManyComponentException extends RuntimeException {
TooManyComponentException(@Nonnull Class<?> componentClass) {
super(String.format("Failed to get a component. Too many components has been registered to %s", componentClass.getName()));
}
}
private static class ComponentInstantiationException extends RuntimeException {
ComponentInstantiationException(@Nonnull Class<?> componentClass) {
super(String.format("Failed to instantiate a component: class=%s", componentClass.getName()));
}
}
}
これで適当にインスタンスを作ることが出来て楽。先日は Vert.x を使っている時に必要性を感じて似たようなものを作りました。使い方はこんな感じ。
public class ContainerTest {
private Container subject;
@Before
public void setup() {
subject = new Container();
}
@Test
public void testContainer() {
VertxOptions options = new VertxOptions();
CustomVertx vertx = new CustomVertx(options, subject);
RESTClientManager rest = new RESTClientManager(vertx);
JDBCClientManager jdbc = new JDBCClientManager(vertx);
subject.registerComponent(rest);
subject.registerComponent(JDBCClientProvider.class, jdbc);
subject.registerComponent(CountryRepository.class, CountryRepositoryImpl.class);
{
JDBCClientProvider component = subject.getComponent(JDBCClientProvider.class);
assertNotNull(component);
assertTrue(component instanceof JDBCClientManager);
}
{
JDBCClientManager component = subject.getComponent(JDBCClientManager.class);
assertNotNull(component);
assertTrue(component instanceof JDBCClientManager);
}
{
RESTClientManager component = subject.getComponent(RESTClientManager.class);
assertNotNull(component);
assertTrue(component instanceof RESTClientManager);
}
{
// lazy
CountryRepository component = subject.getComponent(CountryRepository.class);
assertNotNull(component);
assertTrue(component instanceof CountryRepositoryImpl);
}
{
// components
List<? extends ResourceManager> components = subject.getComponents(ResourceManager.class);
assertNotNull(components);
assertThat(components.size(), is(2));
assertThat(components, hasItems(is(instanceOf(JDBCClientManager.class)), is(instanceOf(RESTClientManager.class))));
}
}
}
真面目に書いてるわけではないので性能とかはご察し。小さいアプリを作る度に似たようなものを作っている気がするので、自分で真面目にチューニングしたマイクロコンテナみたいなのを作っても良いかもしれないですねと書きながら思いました。