CodeGym /Courses /Module 5. Spring /Different ways to inject dependencies: constructors, sett...

Different ways to inject dependencies: constructors, setters, fields

Module 5. Spring
Level 2 , Lesson 2
Available

Now that we know why we need DI and how Spring manages dependencies, it's time to dive deeper into the different ways to inject dependencies. We already touched on this a bit in the last lesson.

Dependency injection is the process of handing an object (or a reference to it) to another object that needs it, instead of letting the object create its own dependencies. Spring gives you three main ways to do DI:

  1. Via constructors.
  2. Via setters.
  3. Via fields.

Each approach has pros and cons, and the choice depends on the context. Let's look closely at each one and see when to use them.


Dependency injection via constructors

Pros:

  • Immutability: the dependency is provided when the object is created and can't be changed after instantiation.
  • Required dependency: if the constructor requires parameters, Spring will have to provide them, otherwise you'll get an error.

Cons:

  • A lot of constructor parameters can make the code harder to read.
  • With many dependencies, constructors can get bulky.

Example:


import org.springframework.stereotype.Component;

// The dependency we'll inject
@Component
public class Engine {
    public void start() {
        System.out.println("Engine is starting...");
    }
}

// Main class
@Component
public class Car {
    private final Engine engine;

    // Dependency injection via constructor
    public Car(Engine engine) {
        this.engine = engine;
    }

    public void drive() {
        engine.start();
        System.out.println("Car is driving...");
    }
}

How it works:

Spring automatically finds a suitable bean of type Engine and passes it into the Car constructor. The constructor approach is also great when an object should be strictly dependent on another.


Dependency injection via setters

Pros:

  • Allows setting the dependency after the object is created.
  • More flexible: you can change the dependency at runtime (though that's rarely needed).

Cons:

  • It's not obvious whether the dependency is required. If you forget to call the setter, the app might fail unexpectedly.
  • Less suitable for immutable objects.

Code example:


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

// The dependency we'll inject
@Component
public class Transmission {
    public void engage() {
        System.out.println("Transmission engaged.");
    }
}

// Main class
@Component
public class Truck {
    private Transmission transmission;

    // Dependency injection via setter
    @Autowired
    public void setTransmission(Transmission transmission) {
        this.transmission = transmission;
    }

    public void haul() {
        transmission.engage();
        System.out.println("Truck is hauling cargo...");
    }
}

How it works: Spring calls setTransmission() after creating the Truck instance. @Autowired tells the IoC container to inject the dependency.

This approach is handy if the dependency might change or the object needs complex initialization.


Dependency injection via fields

Pros:

  • Most compact and concise code.
  • Less boilerplate required to inject dependencies.

Cons:

  • Not great for unit testing, since dependencies aren't easy to pass via constructor.
  • Breaks encapsulation a bit, because fields are injected directly.

Example:


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

// The dependency we'll inject
@Component
public class BrakeSystem {
    public void applyBrakes() {
        System.out.println("Brakes applied!");
    }
}

// Main class
@Component
public class Bicycle {
    @Autowired
    private BrakeSystem brakeSystem; // Dependency injection via field

    public void stop() {
        brakeSystem.applyBrakes();
        System.out.println("Bicycle has stopped.");
    }
}

How it works: Spring injects BrakeSystem directly into the private field brakeSystem. This magic happens thanks to the @Autowired annotation.


Comparing the injection approaches

Approach Pros Cons
Constructor DI Immutability, required dependency Unwieldy for classes with many dependencies
Setter DI Flexibility, good for changing dependencies at runtime Possible to run without setting the dependency, not good for immutable objects
Field DI Concise code, minimal effort Breaks encapsulation, less convenient for tests

Practical example: all together!

Let's build a small example that uses all three approaches.


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

// Dependencies
@Component
class Processor {
    public void process() {
        System.out.println("Processing data...");
    }
}

@Component
class Memory {
    public void load() {
        System.out.println("Memory loaded!");
    }
}

@Component
class HardDrive {
    public void save() {
        System.out.println("Data saved to hard drive.");
    }
}

// Main class
@Component
class Computer {
    private final Processor processor; // Injection via constructor
    private Memory memory;             // Injection via setter
    @Autowired
    private HardDrive hardDrive;       // Injection via field

    @Autowired
    public Computer(Processor processor) {
        this.processor = processor;
    }

    @Autowired
    public void setMemory(Memory memory) {
        this.memory = memory;
    }

    public void start() {
        processor.process();
        memory.load();
        hardDrive.save();
        System.out.println("Computer is running!");
    }
}

This example shows how you can use different DI styles at the same time.


When to use each approach?

  1. Constructor DI:

    • The dependency is required for the class to work.
    • The object is immutable.
    • You want to ensure the dependency is provided at creation time.
  2. Setter DI:

    • The dependency is optional.
    • You need flexibility to change the dependency after instantiation.
  3. Field DI:

    • When concise code matters.
    • When testing isn't the top priority.

Pitfalls and common mistakes

When injecting dependencies you might run into some common issues:

  1. Circular dependencies. For example, if bean A depends on bean B and bean B depends on bean A, Spring won't be able to initialize them. To fix this you can use @Lazy or rethink the app architecture.

  2. Missing dependency. If you forgot to declare a bean or @Autowired, Spring will throw NoSuchBeanDefinitionException. Always check your configuration.

  3. Ambiguous dependencies. If Spring finds multiple beans of the same type, it won't know which one to inject. Use @Qualifier to point to the correct bean.


@Autowired
@Qualifier("specificBeanName")
private MyService myService;

Now you know how to inject dependencies in Spring and can confidently pick the right approach for each situation. In the next lecture we'll cover bean lifecycle management and some extra configuration options!

Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION