5

I have an interface called StatsStore. I have 2 implementations of this store. An in-memory and an SQL implementation called InMemoryStatsStore and SqlStatsStore. In order to inject them I've create 2 annotations @InMemoryStore and @SqlStore. the injections are:

bind(StatsStore.class)
    .annotatedWith(InMemoryStore.class)
    .to(InMemoryStatsStore.class);

bind(StatsStore.class)
    .annotatedWith(SqlStore.class)
    .to(SqlStatsStore.class);   

Now I want to add a new layer of annotation to separate between InMemoryStringStore and InMemoryNumberStore but I can't add more than one annotation to the binding lines e.g. the following does not compile:

bind(StatsStore.class)
    .annotatedWith(InMemoryStore.class)
    .annotatedWith(NumberStoreAnnotation.class) // using named doesn't work as well
    .to(InMemoryNumberStore.class);  

How can I add more than one annotation without using a single named one which would be quite complicated the more layers I add to it?

The other solution I had in mind is Injecting twice:

bind(StatsStore.class)
    .annotatedWith(InMemoryStore.class)
    .to(InMemoryStatsStore.class);

 bind(InMemoryStatsStore.class)
    .annotatedWith(NumberStoreAnnotation.class)
    .to(InMemoryNumberStore.class);

Thanks all.

Guy Grin
  • 1,968
  • 2
  • 17
  • 38

2 Answers2

5

As Amit said, you can't have more than one @BindingAnnotation apply to any given injection. Internally, Guice works like a Map<Key, Provider> where a Key is a possibly-parameterized class with an optional single annotation instance. However, because these are instances, you're welcome to create your own instantiable annotation that works the way Named works.

@Inject @InMemoryStore(NUMBER) StatsStore inMemoryNumberStore;
@Inject @SqlStore(STRING) StatsStore sqlStringStore;
// or
@Inject @Store(dataType=NUMBER, backend=SQL) sqlNumberStore;

The annotation must have the fields defined like so. (If you have one element named value, you can omit the property name per JLS 9.7.3.) Equal annotations are defined as in the Annotation.equals docs.

public enum DataType { NUMBER, STRING; }
public enum Backend { SQL, IN_MEMORY; }

@BindingAnnotation @Retention(SOURCE) @Target({ FIELD, PARAMETER, METHOD })
public @interface Store {
  DataType dataType();
  Backend backend();
}

That works nicely for @Provides, when you can invoke the annotation the same way you inject it, but how can you create a factory method for instances like Names.named? For that, you'll need to do one of the following:

  1. Create an anonymous implementation, with accessors for each attribute as well as correct implementations of equals and hashCode. Note that the hashCode contract is much stricter than for Object, but you can get compatible implementations from Apache annotation utils or similar libraries.
  2. Use AnnotationLiteral, which provides equals and hashCode implementations for arbitrary subclasses.
  3. Use Google Auto or a similar code generator to generate code for a compatible implementation for you. Familiarity with this type of solution is particularly useful for Android and other memory-constrained environments for which reflection is slow, though such environments usually preclude you from using Guice. (@Qualifier annotations work the same way in other JSR-330 compatible dependency injection frameworks, though, including Dagger.)

If the above seems a little complicated, or if you want more complex logic than Guice's map-based implementation can accomplish, one alternative is to add a layer of indirection that you control:

public class StoreStore {
  @Inject Provider<InMemoryNumberStore> inMemoryNumberStoreProvider;
  // ...
  // You can also inject the Injector to call getInstance with a class literal.

  public StatsStore getStore(DataType dataType, Backend backend) {
    // This can also be a switch or any other sort of lookup, of course.
    if (dataType == NUMBER && backend == IN_MEMORY) {
      return inMemoryNumberStoreProvider.get();
    } // ...
  }
}
Community
  • 1
  • 1
Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251
1

You can't do that:

@BindingAnnotation tells Guice that this is a binding annotation. Guice will produce an error if ever multiple binding annotations apply to the same member.

You could use named bindings instead, or you should consider redesigning your solution.

Amit
  • 45,440
  • 9
  • 78
  • 110
  • Can I add multiple names and not just one long name? I want to avoid, if possible, from having names like `states_tore.in_memory.number`... – Guy Grin Sep 20 '16 at 22:39