2

I have a Repository interface that has two implementations. One reads data from a locally stored CSV file while the other reads from an Amazon Dynamo DB. I would like to be able to switch between which implementation I'm using based on an application property or custom build profile. I would normally use a Factory to retrieve the correct class at runtime, but I would like to do this with injection if possible.

I found a similar question using Spring boot but couldn't find an equivalent that would work in Quarkus Spring choose bean implementation at runtime

I also tried implementing a Configuration class similar to what is found in the docs here but again didn't have much luck. https://quarkus.io/guides/cdi-reference#default_beans

It feels like I'm missing something obvious so any pointers would be much appreciated.

Here is a simple example of my classes:

@ApplicationScoped
public class ExampleService {

    @Inject
    ExampleRepository repository;

    public List<Data> retrieveData() {
        return repository.retrieveData();
    }
}
public interface ExampleRepository {
    List<Data> retrieveData();
}
@ApplicationScoped
public class DynamoRepository implements ExampleRepository {

    @Override
    public List<Data> retrieveData() {
        //Get Data from DynamoDb
    }
}
@ApplicationScoped
public class CsvRepository implements ExampleRepository {
    @Inject
    CsvBeanHandler csvBeanHandler;
    @Inject
    LocalFileReader fileReader;

    @Override
    public List<Data> retrieveData() {
        // Get data from CSV
    }
}

I currently also have the following in my application.yml:

com:
    example:
        application:
            storage-type: 'CSV' # OR AMAZON_DYNAMO_DB
ijm3
  • 71
  • 1
  • 8

2 Answers2

4

It looks like they've added this directly to the documentation:

https://quarkus.io/guides/cdi-reference#declaratively-choose-beans-that-can-be-obtained-by-programmatic-lookup

I feel a bit guilty pasting this much, but it's the SO way.

I can add that it is NOT like a Guice 'binding'; BOTH classes will be instantiated, but only one will be injected. Also unlike Guice, you cannot inject the interface (or I did it wrong) - you have to do what's shown below, with Instance. Personally I just use constructor injection and then drop the value of the Instance wrapper into a final field, so I'm not crying about the extra step. I do miss the power and explicit bindings possible with Modules ala Guice, but the simplicity here has its own value.

5.16. Declaratively Choose Beans That Can Be Obtained by Programmatic Lookup

It is sometimes useful to narrow down the set of beans that can be obtained by programmatic lookup via javax.enterprise.inject.Instance. Typically, a user needs to choose the appropriate implementation of an interface based on a runtime configuration property.

Imagine that we have two beans implementing the interface org.acme.Service. You can’t inject the org.acme.Service directly unless your implementations declare a CDI qualifier. However, you can inject the Instance instead, then iterate over all implementations and choose the correct one manually. Alternatively, you can use the @LookupIfProperty and @LookupUnlessProperty annotations. @LookupIfProperty indicates that a bean should only be obtained if a runtime configuration property matches the provided value. @LookupUnlessProperty, on the other hand, indicates that a bean should only be obtained if a runtime configuration property does not match the provided value.

@LookupIfProperty Example

 interface Service {
    String name();
 }

 @LookupIfProperty(name = "service.foo.enabled", stringValue = "true")
 @ApplicationScoped
 class ServiceFoo implements Service {

    public String name() {
       return "foo";
    }
 }

 @ApplicationScoped
 class ServiceBar implements Service {

    public String name() {
       return "bar";
    }
 }

 @ApplicationScoped
 class Client {

    @Inject
    Instance<Service> service;

    void printServiceName() {
       // This will print "bar" if the property "service.foo.enabled" is NOT set to "true"
       // If "service.foo.enabled" is set to "true" then service.get() would result in an AmbiguousResolutionException
       System.out.println(service.get().name());
    }
 }
ggranum
  • 999
  • 8
  • 10
3

If your request is to bind at startup time the right implementation based on a configuration property, I suppose your problem may be resolved used @Produces annotation:

public class ExampleRepositoryFactory {
  @Config("storage-type")
  String storageType;

  @Produces
  public ExampleRepository dynamoInstance() {
    return storageType == "CSV" ? new CsvRepository() : new DynamoRepository();
  }
}
Luca Basso Ricci
  • 17,829
  • 2
  • 47
  • 69
  • 2
    Indeed using a producer is one of the best ways to do this. Recently, a PR was merged that provides a more declarative way: https://github.com/quarkusio/quarkus/pull/20902 Should be present in the next Quarkus version. – Ladicek Oct 22 '21 at 07:19
  • Thanks, I've tried adding this but find I'm still getting an AmbiguousResolutionException which lists both implementations and the Producer method as available beans. Currently, all my classes are using the ApplicationScoped annotation. Is there something better to use here? I tried removing ApplicationScoped from my implementations, but then the Injected classes in the implementations will be null – ijm3 Oct 22 '21 at 08:11
  • The `@Inject` problem could be solved by injecting the fields into the `ExampleRepositoryFactory` and pass the fields into the CsvRepository. If you don't have too many injections. – TomasZ. Oct 27 '21 at 10:15