Issue
I am attempting to dynamically load some functionality based off of the environment my application is running in and was wondering if there is a pattern in spring to support this.
Currently my code looks something like this:
public interface DoThingInterface {
void doThing() {}
}
@Conditional(DoThingCondition.class)
@Component
public class DoThingService implements DoThingInterface {
@Override
public doThing() {
// business logic
}
}
@Conditional(DoNotDoThingCondition.class)
@Component
public class NoopService implement DoThingInterface {
@Override
public doThing() {
// noop
}
}
public AppController {
@Autowire
private DoThingInterface doThingService;
public businessLogicMethod() {
doThingService.doThing();
}
}
I appoligise for typing doThing so many times.
But as it currently stands with this, Spring cannot differentiate between the the NoopService and the DothingService since I am autowiring in an interface that both use. The conditionals that they use are directly opposed so there will only ever be one, but Spring does not know this. I had considered using @Profile() instead of conditional, but both will be used in a lot of environment. Is there a correct way to do this so that spring will load only one of these depending on the environment it is in?
Edit: For clarification this functionality is only available in certain deployment regions which is why I chose to use the conditional annotation as the conditions check profile, region, and properties.
As requested, the Conditions are as follows:
public class DoNotDoTheThingCondition implements Condition {
@Override
public boolean matches(ConditionalContext context) {
return !(region.equals(region) && profile.contains("prod"))
}
}
public class DoThingCondition implements Condition {
@Override
public boolean matches(ConditionalContext context) {
return (region.equals(region) && profile.contains("prod"))
}
}
I have simplified the conditions a bit, but that is the general idea. With the code in the state outlined here, Spring throws the following error: no qualifying bean of type DoThingInterface available: expected single matching bean, but found two: DoThingService, NoopService
Solution
The solution I came to was to use the condition and manually create the beans as per the comment by Thomas Kasene. I am still unsure why the original did not work, but the key bit was moving the @Conditional annotations onto the beans inside the config. My biggest problem with this method is that you have to maintain parody between the two conditions. That aside it makes for incredibly easy testing as you do not have to stub the noop service if you add your testing profile to the conditions.
The solution ended up looking like this: Conditions
public class DoNotDoTheThingCondition implements Condition {
@Override
public boolean matches(ConditionalContext context) {
return !(region.equals(region) && profile.contains("prod"))
}
}
public class DoThingCondition implements Condition {
@Override
public boolean matches(ConditionalContext context) {
return (region.equals(region) && profile.contains("prod"))
}
}
Config
@Configuration
public class DoThingConfiguration {
@Conditional(DoThingCondition.class)
@Bean
public DoThingService doThingService() { return new DoThingService(); }
@Conditional(DoNotDoThingCondition.class)
@Bean
public NoopService noopService() { return new NoopService(); }
}
Services
public interface DoThingInterface {
void doThing() {};
}
public class DoThingService {
public void doThing() { // business logic }
}
public class NoopService {
public void doThing() { //Noop }
}
Controller
public class AppController {
private DoThingInterface doThingService;
public businessLogicMethod() {
doThingService.doThing();
}
}
Answered By - Ethalot
Answer Checked By - Marie Seifert (JavaFixing Admin)