For my application, I have a Scale
interface and multiple classes implementing this interface, for example NormalizedScale
, LogScale
,
As for spring 5.x there's a simpler and cleaner way of doing this. I have decided to use @ConditionalOnProperty annotation but you may choose any @Conditional* of your preference.
Here's the thing, I've have simplified to extreme:
public interface MyService {}
@Service
@ConditionalOnProperty(prefix = "myService", name = "Impl", havingValue = "Some")
public class SomeService implements MyService {}
@Service
@ConditionalOnProperty(prefix = "myService", name = "Impl", havingValue = "Foo")
public class FooService implements MyService {}
@Service
public class SimpleService {
@Autowired
SimpleService(MyService service) {
// service instance will depend on configuration
}
}
I'm using springboot so I've decided to use application.properties
in order to set values via environment variables like this:
myService.Impl=${MY_SERVICE_IMPL}
Then, I have a fully dynamic injection based on environment variables that may be passed to a docker container for instance.
You can simply use "Qualifiers" which is basically going to point to a specific "named" bean. By default the bean names are the name of your classes, with the first letter in lower case (MyClass -> myClass). If you want to define your own names you can do as follow :
@Service("customizedBeanName")
You would end up doing something like this :
@Autowired
@Qualifier("logScale")
private Scale logScale;
@Autowired
@Qualifier("anotherScale")
private Scale anotherScale;
There are a couple of different Spring-like ways you can handle this. The approach I have personally gone for looks a bit like this:
public interface ScaleFactory {
public Scale newInstance();
public String type();
}
public class FirstScaleFactory implements ScaleFactory {
public Scale newInstance() {
return new FirstScale();
}
public String type() {
return "first";
}
}
public class SecondScaleFactory implements ScaleFactory {
public Scale newInstance() {
return new SecondScale();
}
public String type() {
return "second";
}
}
public class ScaleManager {
private final Map<String, ScaleFactory> factories;
@Autowired
public ScaleManager(List<ScaleFactory> factories) {
this.factories = factories.stream()
.collect(Collectors.toMap(f -> f.type(), Function::identity));
}
public Scale newInstance(String type) {
return Optional.ofNullable(factories.get(type))
.map(factory -> factory.newInstance())
.orElseThrow(IllegalArgumentException::new);
}
}
With this approach, your ScaleManager
is a standard Spring bean that can be wired into any class that needs a scale instance. At initialization time, it gets all ScaleFactories
that are defined in the Spring context, and autowires them in as a List<ScaleFactory>
, which is then converted to a Map
(where the ScaleFactory
type is the key). This avoids you needing to worry about class names of Scale
, and gives your the ability to change them later (as long as you keep the type
key consistent)`
Your ScaleFactory
implementations can then do whatever they need to do. For example, if you have one type of Scale
that you know is immutable, you can have the factory return the same instance every time. Alternatively you can have every invocation return a separate instance - the instantiation of the Scale
is up to the implementation-dependent factory.