Reputation: 1029
I have been using dependency injection using @Autowired
in Spring boot. From all the articles that I have read about dependency injection, they mention that dependency injection is very useful when we (if) decide to change the implementing class in the future.
For example, let us deal with a Car
class and a Wheel
interface. The Car
class requires an implementation of the Wheel
interface for it to work. So, we go ahead and use dependency injection in this scenario
// Wheel interface
public interface Wheel{
public int wheelCount();
public void wheelName();
...
}
// Wheel interface implementation
public class MRF impements Wheel{
@Override
public int wheelCount(){
......
}...
}
// Car class
public class Car {
@Autowired
Wheel wheel;
}
Now in the above scenario, ApplicationContext
will figure out that there is an implementation of the Wheel interface and thus bind it to the Car class. In the future, if we change the implementation to say, XYZWheel
implementing class and remove the MRF
implementation, then the same should work.
However, if we decide to keep both the implementations of Wheel
interface in our application, then we will need to specifically mention the dependency we are interested in while Autowiring it. So, the changes would be as follows -
// Wheel interface
public interface Wheel{
public int wheelCount();
public void wheelName();
...
}
@Qualifier("MRF")
// Wheel interface implementation
public class MRF impements Wheel{
@Override
public int wheelCount(){
......
}...
}
// Wheel interface implementation
@Qualifier("XYZWheel")
public class XYZWheel impements Wheel{
@Override
public int wheelCount(){
......
}...
}
// Car class
public class Car {
@Autowired
@Qualifier("XYZWheel")
Wheel wheel;
}
So, now I have to manually define the specific implementation that I want to Autowire. So, how does dependency injection help here ? I can very well use the new
operator to actually instantiate the implementing class that I need instead of relying on Spring to autowire it for me.
So my question is, what are the benefit of autowiring/dependency injection when I have multiple implementing classes and thus I need to manually specify the type I am interested in ?
Upvotes: 2
Views: 2849
Reputation: 5937
You don't have to necessarily hard-wire an implementation if you selectively use the qualifier for @Primary
and @Conditional
for setting up your beans.
A real-world example for this applies to implementation of authentication. For our application, we have a real auth service that integrates to another system, and a mocked one for when we want to do local testing without depending on that system.
This is the base user details service for auth. We do not specify any qualifiers for it, even though there are potentially two @Service
targets for it, Mock and Real.
@Autowired
BaseUserDetailsService userDetailsService;
This base service is abstract and has all the implementations of methods that are shared between mock and real auth, and two methods related specifically to mock that throw exceptions by default, so our Real auth service can't accidentally be used to mock.
public abstract class BaseUserDetailsService implements UserDetailsService {
public void mockUser(AuthorizedUserPrincipal authorizedUserPrincipal) {
throw new AuthException("Default service cannot mock users!");
}
public UserDetails getMockedUser() {
throw new AuthException("Default service cannot fetch mock users!");
}
//... other methods related to user details
}
From there, we have the real auth service extending this base class, and being @Primary
.
@Service
@Primary
@ConditionalOnProperty(
value="app.mockAuthenticationEnabled",
havingValue = "false",
matchIfMissing = true)
public class RealUserDetailsService extends BaseUserDetailsService {
}
This class may seem sparse, because it is. The base service this implements was originally the only authentication service at one point, and we extended it to support mock auth, and have an extended class become the "real" auth. Real auth is the primary auth and is always enabled unless mock auth is enabled.
We also have the mocked auth service, which has a few overrides to actually mock, and a warning:
@Slf4j
@Service
@ConditionalOnProperty(value = "app.mockAuthenticationEnabled")
public class MockUserDetailsService extends BaseUserDetailsService {
private User mockedUser;
@PostConstruct
public void sendMessage() {
log.warn("!!! Mock user authentication is enabled !!!");
}
@Override
public void mockUser(AuthorizedUserPrincipal authorizedUserPrincipal) {
log.warn("Mocked user is being created: " + authorizedUserPrincipal.toString());
user = authorizedUserPrincipal;
}
@Override
public UserDetails getMockedUser() {
log.warn("Mocked user is being fetched from the system! ");
return mockedUser;
}
}
We use these methods in an endpoint dedicated to mocking, which is also conditional:
@RestController
@RequestMapping("/api/mockUser")
@ConditionalOnProperty(value = "app.mockAuthenticationEnabled")
public class MockAuthController {
//...
}
In our application settings, we can toggle mock auth with a simple property.
app:
mockAuthenticationEnabled: true
With the conditional properties, we should never have more than one auth service ready, but even if we do, we don't have any conflicts.
Upvotes: 2
Reputation: 2740
The best way (I think) to understand Dependency Injection
(DI) is like this :
DI
is a mecanism that allows you to dynamically replace your@autowired
interface by your implementation at run time. This is the role of yourDI
framework (Spring, Guice etc...) to perform this action.
In your Car
example, you create an instance of your Wheel
as an interface, but during the execution, Spring creates an instance of your implementation such as MRF
or XYZWheel
.
To answer your question:
I think it depends on the logic you want to implement. This is not the role of your
DI
framework to choose which kind ofWheel
you want for yourCar
. Somehow you will have to define the interfaces you want to inject as dependencies.
Please any other answer will be useful, because DI
is sometimes source of confusion. Thanks in advance.
Upvotes: 1