Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Guice - Bind generic class to instance at runtime based on defined InjectionPoints

Tags:

java

guice

I have a class Property<T> that I'd like to bind after my application has started. Property<T> represents a property of type T whose value can be modified at runtime.

I have classes that can be injected as such:

public class MyClass {
    public MyClass(@Named("someName") Property<String> property) {
       ...
    }
}

I can bind these instances but only once my application is started and I need to know all of the Named annotation values to do so.


I've started looking at Elements and visiting all of the Element instances to find all of its Bindings. Once I have the Bindings, I can use InjectionPoint.forConstructor() to get the constructor.

The thing that's bothering me is that the various types of bindings all have their differences. I have to handle visiting LinkedKeyBinding, UntargettedBinding, etc.. Is there a simpler way to obtain all InjectionPoint for a given list of modules? Or perhaps I'm going at this wrong?

Thanks!

like image 960
GuiSim Avatar asked Jun 01 '26 08:06

GuiSim


2 Answers

It sounds like you're trying to inject @Named(foo) Property<bar> for all foo and bar and resolving them after the fact, which Guice doesn't do particularly well. Because Guice configuration happens at runtime (the configure method has to run), the only way you could use Guice's SPI for what you're doing is to pre-emptively bind all keys so that Guice doesn't complain of an incomplete object graph and then inspect that to figure out what to populate. That seems laborious and overengineered.

Furthermore, as long as Guice allows JIT or "implicit" bindings, you cannot know every key that Guice may try to resolve until the object is requested. This may make it difficult to bind everything you need, even if you had a perfect Module or Injector reflection library. You'd also be bypassing some of Guice's dependency graph validation features, if you don't know what you'll need or what's available until after Injector creation.

Though it's not a perfectly-ideal use of Guice and the concept of dependency injection, I would adapt my code by writing a PropertyOracle:

public class MyClass {
    private final Property<String> someNameProperty;

    public MyClass(PropertyOracle propertyOracle) {
        someNameProperty = propertyOracle.getString("someName");
        // The act of getting tells the oracle which properties to get. You can also
        // separate those steps without holding onto an instance:
        propertyOracle.prepare("keyToBeRequestedLater");
    }
}

Though this type of provision duplicates Guice to some degree, unlike with Guice, you can inject a Property with any type or key you want and then resolve it later--possibly asynchronously. You'd then write a fake or mock PropertyOracle to use in tests, which isn't quite as easy as injecting the Property instances directly, but it maybe be the easiest way to hook into requests without the complications of Guice SPI.

like image 131
Jeff Bowman Avatar answered Jun 02 '26 21:06

Jeff Bowman


I ended up using a BindingTargetVisitor which covers the use cases that I need.

Note that this solution works for our specific use case but might be too limited for your use case, depending on the type of bindings and injections that you use.

public class InjectionPointExtractor extends DefaultBindingTargetVisitor<Object, InjectionPoint> {
  private final Predicate<TypeLiteral<?>> filter;

  public InjectionPointExtractor(Predicate<TypeLiteral<?>> filter) {
    this.filter = filter;
  }

  @Override
  public InjectionPoint visit(UntargettedBinding<?> untargettedBinding) {
    return getInjectionPointForKey(untargettedBinding.getKey());
  }

  @Override
  public InjectionPoint visit(LinkedKeyBinding<?> linkedKeyBinding) {
    return getInjectionPointForKey(linkedKeyBinding.getLinkedKey());
  }

  @Override
  public InjectionPoint visit(ProviderKeyBinding<?> providerKeyBinding) {
    return getInjectionPointForKey(providerKeyBinding.getProviderKey());
  }

  private InjectionPoint getInjectionPointForKey(Key<?> key) {
    if (filter.test(key.getTypeLiteral())) {
      return InjectionPoint.forConstructorOf(key.getTypeLiteral());
    }

    return null;
  }
}

We use the filter to filter only on classes defined in our packages. This gets the job done in a nice and clean fashion.

If you don't use @Inject constructors but instead use Guice to set fields directly, you could use TypeListener

like image 34
GuiSim Avatar answered Jun 02 '26 20:06

GuiSim



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!