82

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.

3
  • What's wrong with @Qualifier if all your class names are different? Commented Dec 18, 2015 at 8:14
  • 5
    Mmh, if I'm not wrong, Qualifier is not evalutated runtime. Commented Dec 18, 2015 at 8:16
  • True that. You probably should look at a factory pattern. See my answer below for details. Commented Dec 18, 2015 at 8:37

8 Answers 8

120

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; // ... } 
Sign up to request clarification or add additional context in comments.

5 Comments

I tried the @ Profile solution and that works for me. I think this can be also a good solution, but with @ Profile I don't need a configurator.
@grep You're right. I fixed the method names. Note that you're autowiring based on type MyService using Conditional so the method names for the bean definition do not take precedence and single configuration with different method names should work.
In this case what happens to your autowiring if both conditions are met?
What if I want to use some variable inside matches?
Is there a solution that doesn't need "new" so you don't have to specify all the constructor parameters but instead use dependency injection, e.g. via Lombok.
31

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; 

2 Comments

I don't think Qualifier should be mixed with Autowired
Why dont you think they should be mixed?
30

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.

5 Comments

I could set the Spring profile via web.xml but is it possibile to change it in a ContextListerner with a bit of code? With this I can change the profile runtime as requested.
Since Spring 4(?) you have the @Conditional annotation at your disposal. @Profile will work 100%, but the Conditional should be better suited.
@demaniak if I'm reading the docs for Conditional correctly, it sounds like it's evaluated at startup, not runtime?
@CharlesWood - yes, you are correct. But, with the addion of "@RefreshScope", and the Actuator starter, it is possible to get a reload at runtime. You would of course have to carefully consider the impact of that on a running system!
Why would this still be called AppConfig? Technically isn't this like a factory pattern?
18

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.

1 Comment

Good application of Delegate pattern - in theory. In practice, it introduces some overhead on extending MyService - each method added should also be implemented by MyFactory (which is BTW not Factory but Proxy). That's OK while MyService only have a method or two, but becomes tedious as MyService grows.
12

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 

2 Comments

This is the cleanest solution that leverages Spring. Notice that value is not an array of string and that the property can be overwritten by an env variable called PROPERTY_MY_SERVICE.
This approach looks cleanest to me as well and is documented in a Spring blog: 4 Solutions for Selective Injection.
10

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. 👍

2 Comments

Nice & clear How would one write integration tests for these? If I had an integration test for Foo, how to set implementation property = foo. Similarly for the case when implementation property = bar.
Probably the same as you would with other tests that rely on application properties
2

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() 

1 Comment

The getMyService() config method needs @Primary to prevent having too many implementations to choose from.
0

A solution with AOP (AspectJ)

@AutowiredCustom public SpeedLimitService speedLimitService; 

Aspect:

import org.aspectj.lang.annotation.Aspect; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; import performance.context.CountryHolder; import java.lang.reflect.Field; /** */ @Aspect @Component public aspect AutowiredCustomFieldAspect implements ApplicationContextAware { private static ApplicationContext applicationContext; pointcut annotatedField(): get(@performance.annotation.AutowiredCustom * *); before(Object object): annotatedField() && target(object) { try { String fieldName = thisJoinPoint.getSignature().getName(); Field field = object.getClass().getDeclaredField(fieldName); field.setAccessible(true); String className = field.getType().getSimpleName() + CountryHolder.getCountry().name(); Object bean = applicationContext.getAutowireCapableBeanFactory().getBean(className); field.set(object, bean); } catch (Exception e) { e.printStackTrace(); } } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } } 

Beans:

@Service("SpeedLimitServiceCH") public class SpeedLimitServiceCH implements SpeedLimitService { @Override public int getHighwaySpeedLimit() { return 120; } } @Service("SpeedLimitServiceDE") public class SpeedLimitServiceDE implements SpeedLimitService { @Override public int getHighwaySpeedLimit() { return 200; } } 

pom.xml configuration

... <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version>${aspectj-maven-plugin.version}</version> <configuration> <complianceLevel>${java.version}</complianceLevel> <source>${maven.compiler.source}</source> <target>${maven.compiler.target}</target> <showWeaveInfo>true</showWeaveInfo> <verbose>true</verbose> <Xlint>ignore</Xlint> <encoding>${project.build.sourceEncoding}</encoding> </configuration> <executions> <execution> <goals> <!-- use this goal to weave all your main classes --> <goal>compile</goal> <!-- use this goal to weave all your test classes --> <goal>test-compile</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project> 

Reference:
https://viktorreinok.medium.com/dependency-injection-pattern-for-cleaner-business-logic-in-your-java-spring-application-f4ace0a3cba7

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.