Dependency Injection

4 minute read

Dependency Injection Concepts using Spring 5

IOC(Inversion Of Control)

Giving control to the container to get instance of object is called Inversion of Control.

  • instead of you are creating object using new operator, let the container do that for you.

DI(Dependency Injection): Way of injecting properties to an object is called Dependency injection.

We have three types of Dependency injection

  • Constructor Injection
  • Setter/Getter Injection
  • Interface Injection

Spring support only Constructor Injection and Setter/Getter Injection.

Dependency Injection is done in 3 ways

  1. By class properties - least preferred
  • Using private properties is **EVIL**
  1. By Setters - Area of much debate
private GreetingService greetingService;
@Autowired
//@Qualifier("setterGreetingService")
public void setGreetingService(@Qualifier("setterGreetingService") GreetingService greetingService) {
    this.greetingService = greetingService;
}
  1. By Constructor - Most Preferred
private GreetingService greetingService;
//Constructor, With Spring 5 no need to explicitly mention @Autowired, but its a good practice
public A3ConstructorInjectedController(GreetingService greetingService) {
    this.greetingService = greetingService;
}

DI via Interfaces is highly preferred

  • Allows runtime to decide implementation to inject
  • Follows Interface Segregation Principle of SOLID
  • Also, makes your code more testable

Types of Injection:

Field Injection

AVOID THIS

it’s generally not recommended because it makes testing and mocking dependencies more challenging.

Constructor injection is preferred for better testability.

You can annotate a class field directly with @Autowired. Spring will find the appropriate bean to inject based on the field’s type.

@Service
public class StudentServiceWithDb {
  @Autowired
  private StudentRepository studentRepository;

  @Autowired
  private StudentMapper studentMapper;

  // Other methods of StudentServiceWithDb
}
Setter Injection

AVOID THIS

Like field injection, it’s less recommended than constructor injection for the same reasons—it can make testing and mocking dependencies more complex.

Annotate a setter method with @Autowired. Spring will call this method and pass the required dependency when initializing the bean.

@Service
public class StudentServiceWithDb {
  private StudentRepository studentRepository;
  private StudentMapper studentMapper;

  //Setter Injection
  @Autowired
  public void setStudentRepository(StudentRepository studentRepository) {
    this.studentRepository = studentRepository;
  }

  @Autowired
  public void setStudentMapper(StudentMapper studentMapper) {
    this.studentMapper = studentMapper;
  }

  // Other methods of StudentServiceWithDb
}
Constructor Injection

Annotate a constructor with @Autowired. Spring will use this constructor to create the bean and pass the required dependencies as constructor arguments.

Constructor injection is considered a best practice

because it ensures that a bean is fully initialized when created.

@Service
public class StudentServiceWithDb {

    private final StudentRepository studentRepository;
    private final StudentMapper studentMapper;

    @Autowired // Constructor Injection
    public StudentServiceWithDb(StudentRepository studentRepository, StudentMapper studentMapper) {
        this.studentRepository = studentRepository;
        this.studentMapper = studentMapper;
    }

    // Other methods of StudentServiceWithDb
}

Dependency Resolution: If there are multiple beans of the same type that can be injected, Spring will perform dependency resolution based on the bean’s name (if provided) or type.

You can also use @Qualifier in conjunction
with @Autowired to specify which bean to inject if there are multiple candidates.

  • @Primary - Multiple beans of the same type and one is intended to go in by * *default**
  • @Qualifier - Used to specify which exact bean should be injected when multiple beans of the same type are available. It allows to explicitly select a bean by name or identifier
public interface PaymentService {
    String processPayment();
}

The two implementations are

@Service("creditCardService")
public class CreditCardPaymentService implements PaymentService{
    @Override
    public String processPayment() {
        return "Paid via credit card";
    }
}
@Service("onlineBankingService")
public class OnlineBankingService implements PaymentService{
    @Override
    public String processPayment() {
        return "Paid via Online Banking";
    }
}

The use of @Qualifier

@RestController
@RequestMapping("/payment")
public class PaymentController {
    private PaymentService creditCardpaymentService;//Can be resolved byNAme or with @Qualifier
    private final PaymentService onlineBankingpaymentService;

    @Autowired
    public PaymentController(@Qualifier("creditCardService") PaymentService creditCardPaymentService,
                             @Qualifier("onlineBankingService") PaymentService onlineBankingpaymentService) {
        this.creditCardpaymentService = creditCardPaymentService;
        this.onlineBankingpaymentService = onlineBankingpaymentService;
    }
    // Other methods of PaymentController
}

Autowiring

byType - Class or Interface

  • By Type (Interface): Use @Qualifier when there are multiple implementations of an interface and you need to specify which implementation should be injected.
    private final SortAlgorithm sortAlgorithm;//SortAlgorithm Interface is implemented by multiple classes
    @Autowired//Optional for Constructor injection 
    public ComplexAlgorithmImpl(@Qualifier("bubbleSort") SortAlgorithm sortAlgorithm) {
        this.sortAlgorithm = sortAlgorithm;
    }
    
  • By Type (Class): Directly inject beans of a specific class when there is no ambiguity or when dealing with distinct classes.
    private final StringService stringService;//Class
    private final NumberService numberService;//Class
      
    @Autowired
    public ServiceUser(StringService stringService, NumberService numberService) {
        this.stringService = stringService;
        this.numberService = numberService;
    }
    

byName

  • if two Classes implement the same interface, the name is used to resolve the dependency
  • or by the @Qualifier - use @Qualifier to specify which bean to inject when there are multiple implementations of an interface.
@Component
public class QuickSortAlgorithm implements SortAlgorithm {
  ...
}
@Component
@Qualifier("bubbleSort")//Qualifying byNAme
public class BubbleSortAlgorithm implements SortAlgorithm {
  ...      
}
@Component
public class ComplexAlgorithmImpl { 
  @Autowired 
  private SortAlgorithm quickSortAlgorithm;//Resolution byName
    
  private final SortAlgorithm sortAlgorithm;
  @Autowired //Constructor Injection using @Qualifier 
  public ComplexAlgorithmImpl(@Qualifier("bubbleSort") SortAlgorithm sortAlgorithm) {
      this.sortAlgorithm = sortAlgorithm;
  }
  ...
}

constructor

  • similar to byType, but through constuctor

Exceptions

NoSuchBeanDefinitionException

  • @Component missing
  • or @ComponentScan not defined properly

NoUniqueBeanDefinitionException

When there are multiple implementations of a single interface and is no declared

  • @Primary or
  • with @Qualifier
@Component
public class QuickSortAlgorithm implements SortAlgorithm{}

@Component
public class BubbleSortAlgorithm implements SortAlgorithm {}

@Component
public class ComplexAlgorithmImpl {

  @Autowired
  private SortAlgorithm sortAlgorithm;
  }