簡易なコンポーネントコンテナをつくる

  • 5
    Like
  • 0
    Comment
More than 1 year has passed since last update.

最近は Java を使っていても余り DI を使わないようにしているのだけど、たまに「インスタンスを構築するのが面倒くさいな〜」ということがある。具体的には、ちょっとした小さなアプリケーションを作っているとき、「コンポーネントは interface ベースで書いているけど、DI コンテナは大袈裟だし、ファクトリを作るのも面倒くさいな〜(あるいは staticFactory に依存したくない)」みたいなケースがそれに当たる。

そういうときは簡易なコンテナを作って適当に済ますことが多い。コンテナと言っても勝手に登録されているインスタンスを組み合わせてくれるだけのシンプルなもの。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))));
        }


    }

}

真面目に書いてるわけではないので性能とかはご察し。小さいアプリを作る度に似たようなものを作っている気がするので、自分で真面目にチューニングしたマイクロコンテナみたいなのを作っても良いかもしれないですねと書きながら思いました。