Как я могу найти все синглтоны на Guice, которые реализуют определенный тип?

Предположим, у меня есть тип Disposable, который реализуются некоторыми классами:

class FactoryImpl implements Disposable {}

Я могу связать этот класс как singleton:

bind(Factory.class)
.to(FactoryImpl.class)
.in(Singleton.class);

или как одиночный сингл:

bind(Factory.class)
.to(FactoryImpl.class)
.asEagerSingleton();

Обратите внимание, что реализация имеет тип, а не интерфейс.

Как я могу найти все синглеты, созданные на самом деле Guice и которые реализуют тип Disposable?

Обратите внимание, что я не хочу слепо называть get() в провайдере, чтобы не создавать вещи, которые мне не нужны (тем более, что я уничтожаю синглтоны, поэтому создание новых может вызвать проблемы).

Это противоположность вопросов, таких как Как я могу получить все экземпляры singleton из Injector Guice?, которые работают только тогда, когда интерфейс содержит ключи, которые вам нужны.

[EDIT]. Правильно ли этот код?

Сначала мне нужен мой интерфейс.

public interface Disposable {
    public void dispose();
}

Волшебство происходит здесь:

import java.util.Collections;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.beust.jcommander.internal.Lists;
import com.google.inject.AbstractModule;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.spi.InjectionListener;
import com.google.inject.spi.TypeEncounter;
import com.google.inject.spi.TypeListener;
import com.google.inject.util.Modules;

/** Support for disposable beans. */
@Singleton
public class DisposableListener implements InjectionListener<Object> {

    private static final Logger log = LoggerFactory.getLogger(DisposableListener.class);

    /** Use this method to create the injector */
    public static Module createModule(Module ...modules) {
        /* Create a new module with ourself at the start. That way, our listeners will see all bindings. */
        List<Module> list = Lists.newArrayList(new DisposingModule());
        Collections.addAll(list, modules);
        return Modules.combine(list);
    }

    /** To dispose all disposables, call this method.
     * 
     *  <p>Good places to call this is at the end of {@code main()},
     *  in an destroy listener of a {@link javax.servlet.ServletContext}, or after a test.
     */
    public static void dispose(Injector injector) {
        injector.getInstance(DisposableListener.class).disposeAll();
    }

    /** Everything that is disposable */
    private List<Disposable> beans = Lists.newArrayList();

    private void disposeAll() {
        log.debug("Disposing {} beans", beans.size());

        for(Disposable bean: beans) {
            try {
                bean.dispose();
            } catch(Exception e) {
                log.warn("Error disposing {}", bean, e);
            }
        }
    }

    @Override
    public void afterInjection(Object injectee) {
        if(injectee instanceof Disposable) {
            log.debug("Noticed disposable bean {}", injectee);
            beans.add((Disposable) injectee);
        }
    }

    /** Module which creates the {@link DisposableListener} for the injector and sets everything up. */
    private static class DisposingModule extends AbstractModule {

        @Override
        protected void configure() {
            DisposableListener disposableListener = new DisposableListener();

            /* Attach a type listener to Guice which will add disposableListener to all types which extend Disposable */
            bindListener(TypeMatchers.subclassesOf(Disposable.class), new TypeListener() {

                @Override
                public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
                    Class<?> clazz = type.getRawType();
                    log.debug("Found disposable: {}", clazz);
                    encounter.register(disposableListener);
                }
            });

            /* Add the listener instance to the module, so we can get it later */
            bind(DisposableListener.class)
            .toInstance(disposableListener);
        }
    }
}

Код обертывает другие модули и гарантирует, что DisposableListener установлен в инжекторе на ранней стадии. Затем он прослушивает новые экземпляры, которые создаются и собирают их в списке.

Код, вероятно, должен проверить, что это все синглтоны, но я не знаю, как это сделать.

Ниже приведены модульные тесты:

import static org.junit.Assert.*;

import java.util.List;

import org.junit.Before;
import org.junit.Test;

import com.beust.jcommander.internal.Lists;
import com.google.common.base.Joiner;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Singleton;

public class DisposableListenerTest {

    private static List<String> events = Lists.newArrayList();

    @Before
    public void clearEvents() {
        events.clear();
    }

    @Test
    public void testEagerNoGetInstance() {

        Injector injector = Guice.createInjector(DisposableListener.createModule(new TestEagerSingleton()));
        // No call to getInstance()
        DisposableListener.dispose(injector);

        assertEvents("Foo created", "Foo disposed");
    }

    @Test
    public void testEagerGetInstance() {

        Injector injector = Guice.createInjector(DisposableListener.createModule(new TestEagerSingleton()));
        Foo inst1 = injector.getInstance(Foo.class);
        Foo inst2 = injector.getInstance(Foo.class);
        DisposableListener.dispose(injector);

        assertSame(inst1, inst2); // validate singleton

        assertEvents("Foo created", "Foo disposed");
    }

    @Test
    public void testLazyNoGetInstance() {

        Injector injector = Guice.createInjector(DisposableListener.createModule(new TestLazySingleton()));
        // No call to getInstance()
        DisposableListener.dispose(injector);

        assertEvents();
    }

    @Test
    public void testLazyGetInstance() {

        Injector injector = Guice.createInjector(DisposableListener.createModule(new TestLazySingleton()));
        Foo inst1 = injector.getInstance(Foo.class);
        Foo inst2 = injector.getInstance(Foo.class);
        DisposableListener.dispose(injector);

        assertSame(inst1, inst2); // validate singleton

        assertEvents("Foo created", "Foo disposed");
    }

    @Test
    public void testAnnotation() {

        Injector injector = Guice.createInjector(DisposableListener.createModule(new TestLazySingleton()));
        FooWithAnnotation inst1 = injector.getInstance(FooWithAnnotation.class);
        FooWithAnnotation inst2 = injector.getInstance(FooWithAnnotation.class);
        DisposableListener.dispose(injector);

        assertSame(inst1, inst2); // validate singleton

        assertEvents("FooWithAnnotation created", "FooWithAnnotation disposed");
    }

    private void assertEvents(String...expectedEvents) {
        Joiner joiner = Joiner.on('\n');
        String expected = joiner.join(expectedEvents);
        String actual = joiner.join(events);
        assertEquals(expected, actual);
    }

    public static class Foo implements Disposable {

        public Foo() {
            events.add("Foo created");
        }

        @Override
        public void dispose() {
            events.add("Foo disposed");
        }

    }

    @Singleton
    public static class FooWithAnnotation implements Disposable {

        public FooWithAnnotation() {
            events.add("FooWithAnnotation created");
        }

        @Override
        public void dispose() {
            events.add("FooWithAnnotation disposed");
        }

    }

    public static class TestLazySingleton extends AbstractModule {

        @Override
        protected void configure() {
            bind(Foo.class).in(Singleton.class);
        }
    }

    public static class TestEagerSingleton extends AbstractModule {

        @Override
        protected void configure() {
            bind(Foo.class).asEagerSingleton();
        }
    }

    // TODO test when bean isn't a singleton
}

Ответы

Ответ 1

Не изобретать области

Во-первых, вручную "избавление" от одноэлементных привязок Guice несколько ставит телегу перед лошадью. Вместо того, чтобы связывать объекты как одиночные, а затем их нужно регулярно чистить, вы должны использовать более подходящий scope (или определить ваш собственный), чтобы эти объекты имели естественные жизненные циклы в течение времени, в течение которого они должны существовать, например, для одного запроса или теста.

Об этом свидетельствует документация на DisposableListener.dispose():

Хорошие места для вызова в конце main(), в слушателе уничтожения ServletContext или после теста

Ни одно из этих мест вам не понадобится:

  • Когда .main() завершается, JVM также скоро закончится (и предположительно ваш injector выйдет за пределы области видимости), поэтому обычно нет необходимости выполнять такую ​​очистку, прежде чем разрешить завершение двоичного файла.

  • Аналогично, когда a ServletContext был уничтожен, вы обычно собираетесь прекратить JVM, поэтому просто позвольте ему нормально выйти.

  • В тестах вы обычно должны создавать изолированные инжекторы для каждого теста, тем самым избегая любого кросс-теста загрязнения. Когда тест заканчивается инжектором, и все его привязки выходят за пределы видимости, и очистить не должно.

Управление ресурсами отдельно от Guice

Конечно, вы могли бы создавать объекты, которые необходимо очистить, например, AutoCloseable, но это не должно" нести ответственность за Guice. Как правило, сайт вызова .getInstance(), который получает закрытый ресурс, должен нести ответственность за его очистку. В качестве альтернативы модуль (модули) может отвечать за создание и управление этими ресурсами. Затем вы создаете инжектор внутри блока try-with-resources, который управляет жизненным циклом модулей ресурсов.

Если эти параметры недостаточны, и вам действительно нужна более мощная семантика жизненного цикла, используйте правильную структуру жизненного цикла, такую ​​как Guava ServiceManager, а не кооптирование Guice в один.

Тем не менее, наличие объектов, требующих очистки в Guice, само по себе вообще не является хорошей идеей. Рассмотрим вместо этого привязку типа оболочки, которая позволяет вызывающему пользователю открывать (и закрывать) ресурсы по мере необходимости, а не привязывать к объекту ресурса с продолжительным сроком существования.

Предпочитают явные привязки над слушателями Guice

Если вам действительно нужно собрать несколько несвязанных объектов, связанных с инжектором Guice, сделайте это явно в .configure() раз, а не неявно через интроспекцию. Использование Multibinder позволяет вашим модулям явно объявлять, какие объекты нужно удалять, привязывая их к экземпляру Multibinder<Disposable>, который агрегирует торговый центр. Тогда ваш шаг очистки просто:

for (Disposable resource : injector.getInstance(new Key<Set<Disposable>>() {}) {
  resource.dispose();
}

Это позволяет избежать "магии" слушателя, который тихо входит и очищается после вас, вместо этого позволяет авторам модуля определять, как лучше всего обрабатывать ресурсы, которые они связывают, и, при необходимости, используя преимущества этой функции очистки.