Spring Dependency Injection: Constructor vs Setter vs Field
Introduction
This tutorial explains dependency injection in Spring, and compares its three styles: constructor injection, setter injection and field injection. We will start with what Spring does when it injects a dependency and what@Autowired means. We will then write the same class in each of the three styles and see what each one makes easy or hard, including what happens when the class is used in a unit test without Spring. Next we will look at optional dependencies, injecting all beans of a type as a list or a map, and classes with more than one constructor. Lastly, we will see how each style behaves with circular dependencies, and end with a clear recommendation.(The idea of dependency injection itself, independent of Spring, is explained in Dependency Injection vs Dependency Inversion vs IoCClick to read: Dependency Injection vs Dependency Inversion vs IoC. This tutorial is about how Spring does it.)
What Spring does when it injects a dependency
A dependency is an object that another object needs to do its work - a service needs a repository, a controller needs a service. When the container creates a bean, it looks at what the bean asks for, finds a matching bean (by default by type), and passes it in. A bean can ask for its dependencies in three places -- Constructor parameters: Spring calls the constructor with the matching beans as arguments.
- Setter methods: Spring creates the bean first, then calls a setter for each dependency.
- Fields: Spring creates the bean, then writes each dependency straight into a field using reflection.
@Autowired is the annotation that marks a constructor, a setter or a field as an injection point. It is optional on a constructor when the class has only one, which is the most common case.
Constructor injection
With constructor injection, the dependencies are constructor parameters, stored infinal fields. The example has a PayrollService that needs a SalaryRepository and a TaxCalculator -
Constructor injection - dependencies are final fields set by the constructor
//ConstructorInjectionApp.java
package com.javabrahman.spring.di.constructor;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
@Repository
class SalaryRepository {
double grossSalary(String employee) {
return 5000.0;
}
}
@Component
class TaxCalculator {
double tax(double gross) {
return gross * 0.2;
}
}
@Service
class PayrollService {
private final SalaryRepository salaryRepository;
private final TaxCalculator taxCalculator;
// The only constructor, so @Autowired is not needed
PayrollService(SalaryRepository salaryRepository, TaxCalculator taxCalculator) {
this.salaryRepository = salaryRepository;
this.taxCalculator = taxCalculator;
}
String payslip(String employee) {
double gross = salaryRepository.grossSalary(employee);
return employee + ": gross " + gross + ", net " + (gross - taxCalculator.tax(gross));
}
}
@Configuration
@ComponentScan
class AppConfig {
}
public class ConstructorInjectionApp {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
System.out.println(context.getBean(PayrollService.class).payslip("Tom Jones"));
}
}
}
Tom Jones: gross 5000.0, net 4000.0
Explanation of the code
PayrollServicehas a single constructor, so Spring uses it without any annotation, finds theSalaryRepositoryandTaxCalculatorbeans by type, and passes them in.- The fields are
final: once the object exists, its dependencies are set and can never change or benull. - The constructor is the complete list of what the class needs. Anyone reading the class, or creating it in a test, sees it at once.
- The class has no Spring import except the
@Serviceannotation; the constructor is plain Java.
Setter injection
With setter injection, Spring first creates the bean with its no-argument constructor, then calls each setter annotated with@Autowired. Setter injection suits optional dependencies, which can be left out without breaking the bean. With @Autowired(required = false), Spring simply skips the setter when there is no matching bean -
Setter injection, with one required and one optional dependency
//SetterInjectionApp.java
package com.javabrahman.spring.di.setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
@Component
class ReportStore {
void save(String report) {
System.out.println("Saved: " + report);
}
}
interface AuditTrail { // no bean implements this interface in this example
void record(String event);
}
@Service
class ReportService {
private ReportStore reportStore;
private AuditTrail auditTrail;
@Autowired
void setReportStore(ReportStore reportStore) {
this.reportStore = reportStore;
}
@Autowired(required = false) // skipped if there is no AuditTrail bean
void setAuditTrail(AuditTrail auditTrail) {
this.auditTrail = auditTrail;
}
void generate(String name) {
reportStore.save(name);
if (auditTrail != null) {
auditTrail.record("generated " + name);
} else {
System.out.println("No audit trail configured - skipping audit");
}
}
}
@Configuration
@ComponentScan
class AppConfig {
}
public class SetterInjectionApp {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
context.getBean(ReportService.class).generate("Q3 sales report");
}
}
}
Saved: Q3 sales report No audit trail configured - skipping audit
Explanation of the code
- Spring created
ReportServicewith its default constructor, then calledsetReportStore()with theReportStorebean. - No bean implements
AuditTrail, andrequired = falsetold Spring that this is fine, sosetAuditTrail()was never called and the field stayednull. - The cost: the fields cannot be
final, and between construction and the setter calls the object exists in an incomplete state. The class has to cope withnulldependencies.
Field injection
Field injection puts@Autowired directly on the fields. It is the shortest to write, which is why it is common in older code and quick examples -
Field injection - @Autowired directly on the fields
@Service
class PayrollService {
@Autowired
private SalaryRepository salaryRepository;
@Autowired
private TaxCalculator taxCalculator;
String payslip(String employee) { ... }
}
new and passes in test doubles. The example below does exactly that with a field-injected class and a constructor-injected class -
Creating field-injected and constructor-injected classes without Spring, as a unit test would
//FieldInjectionTestApp.java
package com.javabrahman.spring.di.field;
import org.springframework.beans.factory.annotation.Autowired;
class TaxCalculator {
double tax(double gross) {
return gross * 0.2;
}
}
class FieldInjectedPayroll {
@Autowired
private TaxCalculator taxCalculator;
double net(double gross) {
return gross - taxCalculator.tax(gross);
}
}
class ConstructorInjectedPayroll {
private final TaxCalculator taxCalculator;
ConstructorInjectedPayroll(TaxCalculator taxCalculator) {
this.taxCalculator = taxCalculator;
}
double net(double gross) {
return gross - taxCalculator.tax(gross);
}
}
public class FieldInjectionTestApp {
public static void main(String[] args) {
// A test double: a calculator with a fixed 10% rate
TaxCalculator tenPercent = new TaxCalculator() {
@Override
double tax(double gross) {
return gross * 0.1;
}
};
System.out.println("Constructor injection: net = " + new ConstructorInjectedPayroll(tenPercent).net(1000));
try {
// There is no way to pass tenPercent in - the field is private and has no setter
System.out.println("Field injection: net = " + new FieldInjectedPayroll().net(1000));
} catch (NullPointerException e) {
System.out.println("Field injection: " + e.getMessage());
}
}
}
Constructor injection: net = 900.0 Field injection: Cannot invoke "com.javabrahman.spring.di.field.TaxCalculator.tax(double)" because "this.taxCalculator" is null
Explanation of the code
- The constructor-injected class took the test double through its constructor and worked: 10% tax on 1000 leaves 900.
- The field-injected class could only be created with its empty constructor. Nothing set the private field, so the first call failed with a
NullPointerException. The helpful message (a Java 14 feature) names the field. - To test a field-injected class you need reflection tools, a mocking framework that writes into private fields, or a full Spring context - all slower and more fragile than calling a constructor.
- Field injection also hides how many dependencies a class has. A constructor with eight parameters is an obvious sign that a class does too much; eight annotated fields are easy to overlook.
Optional dependencies and ObjectProvider
Besidesrequired = false, Spring offers three ways to make a constructor parameter optional: java.util.Optional<T>, a parameter annotated @Nullable, and ObjectProvider<T>. ObjectProvider is the most flexible: it lets the bean decide at runtime whether, and when, to get the dependency -
Optional and ObjectProvider for dependencies that may be missing
//OptionalDependencyApp.java
package com.javabrahman.spring.di.optional;
import java.util.Optional;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
interface DiscountPolicy { // no implementation is a bean
double apply(double price);
}
@Component
class PriceFormatter {
String format(double price) {
return String.format("$%.2f", price);
}
}
@Service
class CheckoutService {
private final Optional<DiscountPolicy> discountPolicy;
private final ObjectProvider<PriceFormatter> formatterProvider;
CheckoutService(Optional<DiscountPolicy> discountPolicy, ObjectProvider<PriceFormatter> formatterProvider) {
this.discountPolicy = discountPolicy;
this.formatterProvider = formatterProvider;
}
String total(double price) {
double discounted = discountPolicy.map(policy -> policy.apply(price)).orElse(price);
PriceFormatter formatter = formatterProvider.getIfAvailable(PriceFormatter::new);
return "Discount policy present: " + discountPolicy.isPresent() + ", total " + formatter.format(discounted);
}
}
@Configuration
@ComponentScan
class AppConfig {
}
public class OptionalDependencyApp {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
System.out.println(context.getBean(CheckoutService.class).total(59.5));
}
}
}
Discount policy present: false, total $59.50
Explanation of the code
- There is no
DiscountPolicybean, so Spring injected an emptyOptionalinstead of failing at startup. The constructor and thefinalfield remain - optional dependencies do not force setter injection. ObjectProvider<PriceFormatter>is injected even though the bean is only fetched later.getIfAvailable(PriceFormatter::new)returns the bean if it exists and otherwise uses the supplied fallback.ObjectProvideralso hasgetObject(),getIfUnique()andstream(), and it fetches the bean each time it is called - useful for beans that should be looked up lazily, or for getting a fresh prototype bean (covered in the tutorial on bean scopesClick to read tutorial: Spring Bean Scopes: Singleton, Prototype, Request, Session).
Injecting all beans of a type
When several beans implement the same interface, a constructor that asks for one of them fails, as we saw in the tutorial on the IoC containerClick to read tutorial: Spring IoC Container and ApplicationContext Explained. A constructor that asks for aList or a Map of that type gets all of them - the basis of plug-in designs, where adding a class adds a feature -
Injecting every bean of a type as a List and as a Map
//CollectionInjectionApp.java
package com.javabrahman.spring.di.collections;
import java.util.List;
import java.util.Map;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
interface Notifier {
void send(String message);
}
@Component
@Order(2)
class EmailNotifier implements Notifier {
public void send(String message) {
System.out.println(" email: " + message);
}
}
@Component
@Order(1)
class SmsNotifier implements Notifier {
public void send(String message) {
System.out.println(" sms: " + message);
}
}
@Component
@Order(3)
class SlackNotifier implements Notifier {
public void send(String message) {
System.out.println(" slack: " + message);
}
}
@Service
class AlertService {
private final List<Notifier> notifiers;
private final Map<String, Notifier> notifiersByName;
AlertService(List<Notifier> notifiers, Map<String, Notifier> notifiersByName) {
this.notifiers = notifiers;
this.notifiersByName = notifiersByName;
}
void alertEveryone(String message) {
System.out.println("Sending to all " + notifiers.size() + " notifiers, in @Order order:");
notifiers.forEach(n -> n.send(message));
System.out.println("Bean names in the map: " + notifiersByName.keySet());
notifiersByName.get("emailNotifier").send("only by email");
}
}
@Configuration
@ComponentScan
class AppConfig {
}
public class CollectionInjectionApp {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
context.getBean(AlertService.class).alertEveryone("Server disk 90% full");
}
}
}
Sending to all 3 notifiers, in @Order order: sms: Server disk 90% full email: Server disk 90% full slack: Server disk 90% full Bean names in the map: [emailNotifier, slackNotifier, smsNotifier] email: only by email
Explanation of the code
List<Notifier>received all three beans.@Ordersets their order in the list - lower values first - so SMS came first even though it is not first in the file.Map<String, Notifier>received the same beans keyed by bean name, so one can be chosen by name at runtime.- Adding a fourth
Notifierclass is enough forAlertServiceto start using it - no other change is needed.
Classes with more than one constructor
If a bean class has several constructors, Spring cannot guess which one to use. Mark the one it should use with@Autowired; without a marked constructor, Spring falls back to the no-argument constructor if there is one, and fails if there is not -
Telling Spring which of several constructors to use
@Service
class ExportService {
private final Formatter formatter;
@Autowired // Spring uses this one
ExportService(Formatter formatter) {
this.formatter = formatter;
}
ExportService() { // used by older code
this(new PlainFormatter());
}
}
Circular dependencies
A circular dependency is when bean A needs bean B and bean B needs bean A. With constructor injection, Spring cannot create either one first, so startup fails and the error names the cycle -A circular dependency between two constructor-injected beans
//CircularDependencyApp.java
package com.javabrahman.spring.di.circular;
import org.springframework.beans.factory.BeanCurrentlyInCreationException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;
@Service
class OrderService {
OrderService(InvoiceService invoiceService) {
}
}
@Service
class InvoiceService {
InvoiceService(OrderService orderService) {
}
}
@Configuration
@ComponentScan
class AppConfig {
}
public class CircularDependencyApp {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
System.out.println("Started with " + context.getBeanDefinitionCount() + " beans");
} catch (Exception e) {
Throwable cause = e;
while (!(cause instanceof BeanCurrentlyInCreationException) && cause.getCause() != null) {
cause = cause.getCause();
}
System.out.println(cause.getClass().getSimpleName() + ": " + cause.getMessage());
}
}
}
BeanCurrentlyInCreationException: Error creating bean with name 'invoiceService': Requested bean is currently in creation: Is there an unresolvable circular reference or an asynchronous initialization dependency?
Explanation of the code
- To create
InvoiceService, Spring needs anOrderService, whose constructor needs theInvoiceServicethat is still being created. Spring detects the cycle and throwsBeanCurrentlyInCreationException, wrapped in the startup exception; the example unwraps it. - With setter or field injection, the plain Spring Framework can resolve such a cycle by creating both objects first and injecting them afterwards. That hides the design problem rather than solving it. Spring Boot refuses circular references by default even then, unless
spring.main.allow-circular-references=trueis set. - The real fix is a better design: move the shared logic into a third bean that both use, or let one side publish an event instead of calling the other.
Which injection style to use
- Constructor injection - the default for required dependencies. Fields can be
final, the object is complete as soon as it exists, the dependencies are visible in one place, unit tests just call the constructor, and circular dependencies are caught at startup. The Spring team recommends it. - Setter injection - for genuinely optional dependencies that have a sensible default, or for the rare dependency that must be changeable after construction.
- Field injection - avoid in application code. It is acceptable in test classes, where the test framework creates the object, such as
@Autowiredfields in a Spring test. - For optional dependencies in a constructor, prefer
Optional<T>orObjectProvider<T>over switching to setters.
Summary
In this tutorial we first saw what Spring does when it injects a dependency, and the three places a bean can ask for one. We wrote a service with constructor injection, then with setter injection, including an optional dependency that was skipped. We saw that field injection works inside the container but leaves a class impossible to create correctly in a plain unit test. Next we made constructor parameters optional withOptional and ObjectProvider, injected all beans of a type as an ordered list and as a map, and told Spring which of several constructors to use. Lastly, we saw a circular dependency fail fast with constructor injection, and concluded that constructor injection should be the default. The next tutorial covers how many instances of a bean exist and for how long: Spring bean scopesClick to read tutorial: Spring Bean Scopes: Singleton, Prototype, Request, Session.Spring Tutorials
Spring Beans: @Component, @Bean and @ConfigurationClick to read tutorial: Spring Beans: @Component, @Bean and @Configuration Spring Bean Scopes: Singleton, Prototype, Request, SessionClick to read tutorial: Spring Bean Scopes: Singleton, Prototype, Request, Session What is Spring? Spring Framework vs Spring Boot ExplainedClick to read tutorial: What is Spring? Spring Framework vs Spring Boot Explained Spring IoC Container and ApplicationContext ExplainedClick to read tutorial: Spring IoC Container and ApplicationContext Explained Spring Bean Lifecycle: @PostConstruct, @PreDestroy and MoreClick to read tutorial: Spring Bean Lifecycle: @PostConstruct, @PreDestroy and More Spring @Qualifier, @Primary and Conditional BeansClick to read tutorial: Spring @Qualifier, @Primary and Conditional Beans Spring Expression Language (SpEL) and @Value ExplainedClick to read tutorial: Spring Expression Language (SpEL) and @Value Explained
Spring Beans: @Component, @Bean and @ConfigurationClick to read tutorial: Spring Beans: @Component, @Bean and @Configuration Spring Bean Scopes: Singleton, Prototype, Request, SessionClick to read tutorial: Spring Bean Scopes: Singleton, Prototype, Request, Session What is Spring? Spring Framework vs Spring Boot ExplainedClick to read tutorial: What is Spring? Spring Framework vs Spring Boot Explained Spring IoC Container and ApplicationContext ExplainedClick to read tutorial: Spring IoC Container and ApplicationContext Explained Spring Bean Lifecycle: @PostConstruct, @PreDestroy and MoreClick to read tutorial: Spring Bean Lifecycle: @PostConstruct, @PreDestroy and More Spring @Qualifier, @Primary and Conditional BeansClick to read tutorial: Spring @Qualifier, @Primary and Conditional Beans Spring Expression Language (SpEL) and @Value ExplainedClick to read tutorial: Spring Expression Language (SpEL) and @Value Explained