Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I find all singletons on Guice which implement a specific type?

Imagine I have a type Disposable which some classes implement:

class FactoryImpl implements Disposable {}

I can bind this class as a singleton:

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

or as an eager singleton:

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

Note that the implementation has the type, not the interface.

How can I find all singletons which Guice has actually created and which implement the type Disposable?

Note that I don't want to blindly call get() in the provider to avoid to create stuff which I don't need (especially since I'm destroying singletons, so creating new ones might cause problems).

This is the opposite of questions like How can I get all singleton instances from a Guice Injector? which only work then the interface contains the keys that you need.

[EDIT] This is how far I got. Is this code correct?

First, I need my interface.

public interface Disposable {
    public void dispose();
}

The magic happens here:

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);
        }
    }
}

The code wraps the other modules and makes sure the DisposableListener is installed in the injector early on. Then it listens for new instances which are created and collects them in a list.

The code probably should check that these are all singletons but I don't know how to do that.

Here are the unit tests:

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
}
like image 371
Aaron Digulla Avatar asked Feb 10 '16 12:02

Aaron Digulla


People also ask

Is Guice a provider Singleton?

Guice comes with a built-in @Singleton scope that reuses the same instance during the lifetime of an application within a single injector. Both javax. inject. Singleton and com.

What is @inject annotation in Guice?

Annotation Type Inject. @Target(value={METHOD,CONSTRUCTOR,FIELD}) @Retention(value=RUNTIME) @Documented public @interface Inject. Annotates members of your implementation class (constructors, methods and fields) into which the Injector should inject values.

What does @inject do Guice?

Note that the only Guice-specific code in the above is the @Inject annotation. This annotation marks an injection point. Guice will attempt to reconcile the dependencies implied by the annotated constructor, method, or field.

What is binding in Google Guice?

A binding is an object that corresponds to an entry in the Guice map. You add new entries into the Guice map by creating bindings.


1 Answers

Don't re-invent scopes

First off, manually "disposing" of singleton Guice bindings is somewhat putting the cart before the horse. Instead of binding objects as singletons and then needing to regularly clean them up, you should use a more appropriate scope (or define your own) so that these objects have natural life cycles for the duration of time that they're expected to exist, such as for a single request or test.

This is evidenced by the documentation on DisposableListener.dispose():

Good places to call this is at the end of main(), in an destroy listener of a ServletContext, or after a test

None of those are place you should need something like this:

  1. When .main() terminates the JVM will soon terminate too (and presumably your injector will go out of scope) so there's generally no need to do any such cleanup before letting the binary terminate.

  2. Similarly when a ServletContext has been destroyed you're generally just about to terminate the JVM, so just let it exit normally.

  3. In tests you should normally be constructing isolated injectors for each test, thereby avoiding any cross-test pollution. When the test ends the injector and all its bindings goes out of scope, and there should be nothing to clean up.

Manage resources separately from Guice

Of course, you could be creating objects that need to be cleaned up, such as an AutoCloseable instance, but that shouldn't be the responsibility of Guice. Generally the .getInstance() call site that obtains the closeable resource should be responsible for cleaning it up. Alternatively the module(s) could be responsible for creating and managing these resources. Then you construct the injector inside a try-with-resources block that manages the lifecycle of the resources module(s).

If those options aren't sufficient and you really need more powerful life cycle semantics use a proper life cycle framework such as Guava's ServiceManager, rather than co-opting Guice into one.

That said, having objects that require cleanup in Guice is itself generally not a good idea. Consider instead binding a wrapper type that allows the caller to open (and close) resources as needed, rather than binding a long-lived stateful resource object directly.

Prefer explicit bindings over Guice listeners

If you really, really need to collect several unrelated objects bound in a Guice injector, do so explicitly at .configure() time, rather than implicitly via introspection. Using a Multibinder allows your modules to explicitly declare which objects need to be disposed of, by binding them to a Multibinder<Disposable> instance which aggregates them all. Then your cleanup step is simply:

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

This avoids the "magic" of a listener that silently comes in and cleans up after you, instead allowing module authors to determine how best to handle the resources they bind, and optionally taking advantage of this cleanup functionality if necessary.

like image 184
dimo414 Avatar answered Oct 04 '22 23:10

dimo414