3

Given two (or more) implementations of a particular service API, what's the best way to pick which one to use at runtime in my app based on an application property?

Example API:

public interface Greeting {
    String sayHello(String username);
}

Implementations:

public class FriendlyGreeting implements Greeting {
    public String sayHello(String username) {
        return "Hello, " + username;
    }
} 

public class HostileGreeting implements Greeting {
    public String sayHello(String username) {
        return "Go away, " + username;
    }
}

I've got a separate service class with an @Autowired constructor that takes an instance of Greeting. What I want, is based upon a configuration property, to decide which greeting implementation gets injected and used. I came up with using a configuration class to make that decision:

@Configuration
public class GreetingConfiguration {
    private String selection;

    @Autowired
    public GreetingConfiguration(@Value("${greeting.type}") String type) {
        this.selection = type;
    }

    @Bean
    public Greeting provideGreeting() {
        if ("friendly".equals(selection)) {
            return new FriendlyGreeting();
        } else {
            return new HostileGreeting();
        }
    }
}

Is this the right way to do what I want? I went down the road of using @Qualifier on the implementations, and ended up with a mess where Spring saw 3 instances of my Greeting API, and I needed a configuration anyway to pick which implementation to use and return it with a unique qualifier name on it, and that feels worse than what I settled on.

David
  • 2,602
  • 1
  • 18
  • 32
  • 2
    Does it have to be a configuration property or can it be a Spring profile? Because using a Profile would be the nice and calm *Springy* way to do this. After that, depending on profile, you would provide either Hostile or Friendly Configuration to handle the bean. – Compass Jan 23 '19 at 20:18
  • 1
    Specifically: have suitable `@Profile` annotations on your components and pass the `spring.profiles.active` and/or `spring.profiles.include` property at runtime... – user268396 Jan 23 '19 at 20:22
  • I'll look into profiles, but I think they're defined by environment that we deploy the app into and not really changeable. – David Jan 23 '19 at 20:22
  • Also possibly helpful is using `@Conditional` https://stackoverflow.com/a/34351004/2958086 – Compass Jan 23 '19 at 20:29

4 Answers4

3

You can use @Conditional annotations described at https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Conditional.html and https://reflectoring.io/spring-boot-conditionals/

@Profile annotations mentioned above are based upon @Conditional(from Spring Framework); see also Spring Boot: org.springframework.boot.autoconfigure.condition

Fuad Efendi
  • 155
  • 1
  • 9
2

You can mark both Greeting as @Service and select the chosen one with @Qualifier("yourServiceHere") like this:

@Autowired
@Qualifier("friendlyGreeting")
private Greeting greeting;

Another way you can do it is with profile. You can mark your FriendlyGreeting service with @Service and @Profile("friendly") and the HostileGreeting service with @Service and @Profile("hostileGreeting") and just put in the application.properties the following:

spring.profiles.active=friendly
  • i doubt, that SpEL works in qualifier... (https://stackoverflow.com/a/29773117/592355) – xerx593 Jan 23 '19 at 21:03
  • Good thought, but the evaluation of `"${app.greeting}"` doesn't happen, and spring goes looking for a bean with the literal qualifier name of `${app.greeting}` – David Jan 23 '19 at 21:18
  • So you can put your qualifier as a string @Qualifier("friendlyGreeting") – Vitor Gabriel Carrilho Jan 23 '19 at 21:33
  • so you cannot manage which implementation to use at runtime without using Profile: Qualifier("friendlyGreeting") is hardcoded; and Qualifier("${app.greetig}") won't work. Is that correct, Profile is the only way (except initial solution in the question)? – Fuad Efendi Dec 01 '20 at 20:14
2

Answering my own question.

@Compass and @user268396 were correct - using Profiles got this working as expected.

I created both implementations, annotated with @Service and @Profile("friendly") or @Profile("hostile"), and could change the property spring.profiles.active to dev,friendly for example, and get what I wanted.

David
  • 2,602
  • 1
  • 18
  • 32
1

Here is a full solution using ideas mentioned by David and Vitor above with @Profile and @Qualifer annotations.

Two beans with same name but Only one is activated based on which profile is defined.

@Profile("profile1")
@Bean("greeting")
public class FriendlyGreeting implements Greeting {

---

@Profile("profile2")
@Bean("greeting")
public class HostileGreeting implements Greeting {

---

@Configuration
public class GreetingConfiguration {

    private Greeting greeting;

    @Autowired
    public GreetingConfiguration(@Qualifier("greeting") Greeting greeting) {
        this.greeting = greeting;
    }

}

Notes:

  • you can remove the intermediate class GreetingConfiguration and stick the "greeting" bean wherever you need
  • i prefer the @Autowired on the constructor instead of the class member to make it easier for unit testing.
toddcscar
  • 1,115
  • 9
  • 12