Spring choose bean implementation at runtime

JavaSpringSpring Bean

Java Problem Overview


I'm using Spring Beans with annotations and I need to choose different implementation at runtime.

@Service
public class MyService {
   public void test(){...}
}

For example for windows's platform I need MyServiceWin extending MyService, for linux platform I need MyServiceLnx extending MyService.

For now I know only one horrible solution:

@Service
public class MyService {

	private MyService impl;
   
   @PostInit
   public void init(){
		if(windows) impl=new MyServiceWin();
		else impl=new MyServiceLnx();
   }
   
   public void test(){
		impl.test();
   }
}

Please consider that I'm using annotation only and not XML config.

Java Solutions


Solution 1 - Java

1. Implement a custom Condition
public class LinuxCondition implements Condition {
  @Override
  public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    return context.getEnvironment().getProperty("os.name").contains("Linux");  }
}

Same for Windows.

2. Use @Conditional in your Configuration class
@Configuration
public class MyConfiguration {
   @Bean
   @Conditional(LinuxCondition.class)
   public MyService getMyLinuxService() {
      return new LinuxService();
   }

   @Bean
   @Conditional(WindowsCondition.class)
   public MyService getMyWindowsService() {
      return new WindowsService();
   }
}
3. Use @Autowired as usual
@Service
public class SomeOtherServiceUsingMyService {
    
    @Autowired    
    private MyService impl;
    
    // ... 
}

Solution 2 - Java

You can move the bean injection into the configuration, as:

@Configuration
public class AppConfig {

	@Bean
	public MyService getMyService() {
		if(windows) return new MyServiceWin();
        else return new MyServiceLnx();
	}
}

Alternatively, you may use profiles windows and linux, then annotate your service implementations with the @Profile annotation, like @Profile("linux") or @Profile("windows"), and provide one of this profiles for your application.

Solution 3 - Java

Let's create beautiful config.

Imagine that we have Animal interface and we have Dog and Cat implementation. We want to write write:

@Autowired
Animal animal;

but which implementation should we return?

enter image description here

So what is solution? There are many ways to solve problem. I will write how to use @Qualifier and Custom Conditions together.

So First off all let's create our custom annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE})
public @interface AnimalType {
    String value() default "";
}

and config:

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class AnimalFactoryConfig {

    @Bean(name = "AnimalBean")
    @AnimalType("Dog")
    @Conditional(AnimalCondition.class)
    public Animal getDog() {
        return new Dog();
    }

    @Bean(name = "AnimalBean")
    @AnimalType("Cat")
    @Conditional(AnimalCondition.class)
    public Animal getCat() {
        return new Cat();
    }

}

Note our bean name is AnimalBean. why do we need this bean? because when we inject Animal interface we will write just @Qualifier("AnimalBean")

Also we crated custom annotation to pass the value to our custom Condition.

Now our conditions look like this (imagine that "Dog" name comes from config file or JVM parameter or...)

   public class AnimalCondition implements Condition {

    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        if (annotatedTypeMetadata.isAnnotated(AnimalType.class.getCanonicalName())){
           return annotatedTypeMetadata.getAnnotationAttributes(AnimalType.class.getCanonicalName())
                   .entrySet().stream().anyMatch(f -> f.getValue().equals("Dog"));
        }
        return false;
    }
}

and finally injection:

@Qualifier("AnimalBean")
@Autowired
Animal animal;

Solution 4 - Java

Autowire all your implementations into a factory with @Qualifier annotations, then return the service class you need from the factory.

public class MyService {
    private void doStuff();
}

My Windows Service:

@Service("myWindowsService")
public class MyWindowsService implements MyService {

    @Override
    private void doStuff() {
        //Windows specific stuff happens here.
    }
}

My Mac Service:

@Service("myMacService")
public class MyMacService implements MyService {

    @Override
    private void doStuff() {
        //Mac specific stuff happens here
    }
}

My factory:

@Component
public class MyFactory {
    @Autowired
    @Qualifier("myWindowsService")
    private MyService windowsService;
    
    @Autowired
    @Qualifier("myMacService")
    private MyService macService;

    public MyService getService(String serviceNeeded){
        //This logic is ugly
        if(serviceNeeded == "Windows"){
            return windowsService;
        } else {
            return macService;
        }
    }
}

If you want to get really tricky you can use an enum to store your implementation class types, and then use the enum value to choose which implementation you want to return.

public enum ServiceStore {
    MAC("myMacService", MyMacService.class),
    WINDOWS("myWindowsService", MyWindowsService.class);

    private String serviceName;
    private Class<?> clazz;

    private static final Map<Class<?>, ServiceStore> mapOfClassTypes = new HashMap<Class<?>, ServiceStore>();
    
    static {
        //This little bit of black magic, basically sets up your 
        //static map and allows you to get an enum value based on a classtype
        ServiceStore[] namesArray = ServiceStore.values();
        for(ServiceStore name : namesArray){
            mapOfClassTypes.put(name.getClassType, name);
        }
    }
    
    private ServiceStore(String serviceName, Class<?> clazz){
        this.serviceName = serviceName;
        this.clazz = clazz;
    }

    public String getServiceBeanName() {
        return serviceName;
    }

    public static <T> ServiceStore getOrdinalFromValue(Class<?> clazz) {
        return mapOfClassTypes.get(clazz);
    }
}

Then your factory can tap into the Application context and pull instances into it's own map. When you add a new service class, just add another entry to the enum, and that's all you have to do.

 public class ServiceFactory implements ApplicationContextAware {

     private final Map<String, MyService> myServices = new Hashmap<String, MyService>();

     public MyService getInstance(Class<?> clazz) {
         return myServices.get(ServiceStore.getOrdinalFromValue(clazz).getServiceName());
     }

      public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
          myServices.putAll(applicationContext.getBeansofType(MyService.class));
      }
 }

Now you can just pass the class type you want into the factory, and it will provide you back the instance you need. Very helpful especially if you want to the make the services generic.

Solution 5 - Java

Simply make the @Service annotated classes conditional: That's all. No need for other explicit @Bean methods.

public enum Implementation {
    FOO, BAR
}

@Configuration
public class FooCondition implements Condition {
	@Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		Implementation implementation = Implementation.valueOf(context.getEnvironment().getProperty("implementation"));
	    return Implementation.FOO == implementation;
    }
}

@Configuration
public class BarCondition implements Condition {
	@Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		Implementation implementation = Implementation.valueOf(context.getEnvironment().getProperty("implementation"));
	    return Implementation.BAR == implementation;
    }
}

Here happens the magic. The condition is right where it belongs: At the implementating classes.

@Conditional(FooCondition.class)
@Service
class MyServiceFooImpl implements MyService {
    // ...
}

@Conditional(BarCondition.class)
@Service
class MyServiceBarImpl implements MyService {
    // ...
}

You can then use Dependency Injection as usual, e.g. via Lombok's @RequiredArgsConstructor or @Autowired.

@Service
@RequiredArgsConstructor
public class MyApp {
    private final MyService myService;
    // ...
}

Put this in your application.yml:

implementation: FOO

ļ‘ Only the implementations annotated with the FooCondition will be instantiated. No phantom instantiations. ļ‘

Solution 6 - Java

MyService.java:

public interface MyService {
  String message();
}

MyServiceConfig.java:

@Configuration
public class MyServiceConfig {

  @Value("${service-type}")
  MyServiceTypes myServiceType;

  @Bean
  public MyService getMyService() {
    if (myServiceType == MyServiceTypes.One) {
      return new MyServiceImp1();
    } else {
      return new MyServiceImp2();
    }
  }
}

application.properties:

service-type=one

MyServiceTypes.java

public enum MyServiceTypes {
  One,
  Two
}

Use in any Bean/Component/Service/etc. like:

	@Autowired
	MyService myService;
    ...
    String message = myService.message()

Solution 7 - Java

Just adding my 2 cents to this question. Note that one doesn't have to implement so many java classes as the other answers are showing. One can simply use the @ConditionalOnProperty. Example:

@Service
@ConditionalOnProperty(
  value="property.my.service", 
  havingValue = "foo", 
  matchIfMissing = true)
class MyServiceFooImpl implements MyService {
    // ...
}

@ConditionalOnProperty(
  value="property.my.service", 
  havingValue = "bar")
class MyServiceBarImpl implements MyService {
    // ...
}

Put this in your application.yml:

property.my.service: foo

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionTobiaView Question on Stackoverflow
Solution 1 - JavanobehView Answer on Stackoverflow
Solution 2 - JavaStanislavView Answer on Stackoverflow
Solution 3 - JavagrepView Answer on Stackoverflow
Solution 4 - JavaJamesENLView Answer on Stackoverflow
Solution 5 - JavalilalinuxView Answer on Stackoverflow
Solution 6 - JavaGuillaume BlanchetView Answer on Stackoverflow
Solution 7 - JavaKnoblauchView Answer on Stackoverflow