Java · lesson 50 of 66
Beginner
17- 1 Introduction to Java
- 2 Setting Up Java
- 3 Data Types in Java
- 4 Operators in Java
- 5 Variables and Operators in Java
- 6 Control Flow in Java
- 7 Arrays in Java
- 8 Functions (Methods) in Java
- 9 Strings in Java
- 10 Methods in Java
- 11 Java OOP — Complete Guide
- 12 Introduction to Object-Oriented Programming in Java
- 13 Encapsulation in Java
- 14 Inheritance in Java
- 15 Polymorphism in Java
- 17 Abstraction in Java
- 18 Error Types in Java
Intermediate
38- 16 Regular Expressions in Java
- 19 Interfaces in Java — A Deep Dive
- 20 Abstract Classes in Java
- 21 Method Overloading in Java
- 22 Composition vs Inheritance in Java
- 23 Generics in Java
- 27 Exception Handling in Java
- 28 Java Streams API
- 29 Collections Framework in Java
- 30 Optional in Java
- 31 File Handling in Java
- 32 Records in Java
- 33 Java 8 Features
- 34 Sealed Classes and Interfaces in Java
- 35 Multithreading in Java
- 37 JDBC — Database Access in Java
- 39 Maven — Java Build and Dependency Management
- 42 Gradle — Modern Java Build Tool
- 44 Unit Testing with JUnit 5 and Mockito
- 45 Logging in Java with SLF4J and Logback
- 49 Spring Boot — Introduction
- 50 Dependency Injection in Spring Boot
- 51 Java Project Ideas
- 52 Building REST APIs with Spring Boot
- 53 Validation in Spring Boot
- 54 Spring Data JPA — Database Access
- 55 Exception Handling in Spring Boot
- 56 Spring Security — Authentication and Authorization
- 57 JWT Authentication in Spring Boot
- 58 Dockerizing Spring Boot Applications
- 59 Java Enums: Beyond Simple Constants
- 60 Inner and Anonymous Classes in Java
- 61 Java Generics: Bounded Types, Wildcards, and Generic Methods
- 62 Java Date/Time API: LocalDate, LocalDateTime, and Duration
- 63 Java NIO.2: Paths, Files, and WatchService
- 64 Java Annotations: Custom Types and Meta-Information
- 65 Java Reflection: Dynamic Type Inspection and Method Invocation
- 66 Java Localization: ResourceBundle and Locale Support
Advanced
11Dependency Injection in Spring Boot
Run Spring's IoC container: constructor injection, two beans of one type, @Primary and @Qualifier, scopes, @ConfigurationProperties and lifecycle.
By SkillByExample · Updated
Dependency injection (DI) means a class receives its collaborators instead of creating them with
new. In Spring the IoC container (the ApplicationContext) builds every object it manages — a
bean — and passes each one what its constructor asks for. Each example below is a single
DiApplication.java in a Spring Boot 3.5 project with only spring-boot-starter, so there is no web
server: a CommandLineRunner prints the result and the app exits. application.properties turns
off the banner and info logging so the output is just what the program prints:
spring.main.banner-mode=off
logging.level.root=WARN
Constructor injection
package com.example.di;
import java.util.Arrays;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
interface Notifier { String send(String to, String text); }
@Component
class EmailNotifier implements Notifier {
public String send(String to, String text) { return "email to " + to + ": " + text; }
}
@Service
class OrderService {
private final Notifier notifier; // depends on the interface only
OrderService(Notifier notifier) { // single constructor: Spring injects it
this.notifier = notifier;
}
String placeOrder(String customer) { return notifier.send(customer, "order placed"); }
}
@SpringBootApplication
public class DiApplication implements CommandLineRunner {
private final OrderService orders;
private final ApplicationContext ctx;
DiApplication(OrderService orders, ApplicationContext ctx) { this.orders = orders; this.ctx = ctx; }
public static void main(String[] args) { SpringApplication.run(DiApplication.class, args); }
@Override
public void run(String... args) {
System.out.println(orders.placeOrder("ada@example.com"));
System.out.println("Notifier beans: " + Arrays.toString(ctx.getBeanNamesForType(Notifier.class)));
System.out.println("same instance: " + (ctx.getBean(OrderService.class) == orders));
}
}
$ mvn -q -B package -DskipTests
$ java -jar target/di-0.0.1-SNAPSHOT.jar
email to ada@example.com: order placed
Notifier beans: [emailNotifier]
same instance: true
Nothing here calls new. Component scanning registered the three annotated classes as beans named
after the class (emailNotifier, orderService, diApplication). To build DiApplication the
container needed an OrderService, which needed a Notifier; the only bean implementing that
interface was EmailNotifier, so it went in. A class with a single constructor needs no
@Autowired. OrderService only knows the Notifier interface, so swapping email for SMS never
touches it — and same instance: true shows beans are singletons by default: one shared object.
@Service, @Repository and @Controller are @Component with a role name; @Repository also
translates database exceptions into Spring’s DataAccessException hierarchy.
Two beans of the same type
Add a second Notifier and leave OrderService asking for one.
@Component
class SmsNotifier implements Notifier { // a second implementation
public String send(String to, String text) { return "sms to " + to + ": " + text; }
}
$ java -jar target/di-0.0.1-SNAPSHOT.jar
...
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of constructor in com.example.di.OrderService required a single bean, but 2 were found:
- emailNotifier: defined in URL [jar:nested:/tmp/sb/di/target/di-0.0.1-SNAPSHOT.jar/!BOOT-INF/classes/!/com/example/di/EmailNotifier.class]
- smsNotifier: defined in URL [jar:nested:/tmp/sb/di/target/di-0.0.1-SNAPSHOT.jar/!BOOT-INF/classes/!/com/example/di/SmsNotifier.class]
...
Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
...
The container refuses to guess, and it refuses at startup rather than at the first order. The
Action line lists the three fixes, and the next example uses all three.
@Primary, @Qualifier and injecting every bean
@Component
@Primary // the default when a single Notifier is asked for
class EmailNotifier implements Notifier {
public String send(String to, String text) { return "email to " + to + ": " + text; }
}
@Component
class SmsNotifier implements Notifier {
public String send(String to, String text) { return "sms to " + to + ": " + text; }
}
@Service
class OrderService {
private final Notifier notifier;
OrderService(Notifier notifier) { this.notifier = notifier; } // gets the @Primary one
String placeOrder(String c) { return notifier.send(c, "order placed"); }
}
@Service
class AlertService {
private final Notifier notifier;
AlertService(@Qualifier("smsNotifier") Notifier notifier) { this.notifier = notifier; }
String alert(String c) { return notifier.send(c, "card declined"); }
}
@Service
class BroadcastService {
private final List<Notifier> all; // every Notifier bean
BroadcastService(List<Notifier> all) { this.all = all; }
void broadcast(String c) { all.forEach(n -> System.out.println(" " + n.send(c, "maintenance"))); }
}
$ java -jar target/di-0.0.1-SNAPSHOT.jar
email to ada: order placed
sms to ada: card declined
broadcast:
email to ada: maintenance
sms to ada: maintenance
(The runner calls placeOrder, alert and broadcast on the three services; imports and the
@SpringBootApplication class follow the earlier pattern.) @Primary makes EmailNotifier the
answer whenever a single Notifier is requested without further detail, so OrderService starts
again unchanged. @Qualifier("smsNotifier") overrides that for one injection point by bean name.
Asking for List<Notifier> injects every implementation — the basis of plugin-style designs such as
a list of validators or event handlers.
Bean scopes
@Component // default scope: singleton
class Counter {
private final AtomicInteger n = new AtomicInteger();
int next() { return n.incrementAndGet(); }
}
@Component
@Scope("prototype") // a new instance on every request to the container
class ShoppingCart {
private static final AtomicInteger created = new AtomicInteger();
final int id = created.incrementAndGet();
}
@SpringBootApplication
public class DiApplication implements CommandLineRunner {
private final Counter a, b;
private final ObjectProvider<ShoppingCart> carts;
DiApplication(Counter a, Counter b, ObjectProvider<ShoppingCart> carts) { this.a = a; this.b = b; this.carts = carts; }
public static void main(String[] args) { SpringApplication.run(DiApplication.class, args); }
@Override
public void run(String... args) {
System.out.println("a == b: " + (a == b));
System.out.println("a.next()=" + a.next() + ", b.next()=" + b.next());
ShoppingCart c1 = carts.getObject(), c2 = carts.getObject();
System.out.println("cart ids: " + c1.id + ", " + c2.id + " same? " + (c1 == c2));
}
}
$ java -jar target/di-0.0.1-SNAPSHOT.jar
a == b: true
a.next()=1, b.next()=2
cart ids: 1, 2 same? false
a and b are two constructor parameters but one object, so the second next() sees the first
increment: a singleton’s state is shared by every class that injects it, and across every request
thread in a web app, so keep singletons stateless or thread-safe. The prototype ShoppingCart
is created anew on each getObject(). Injecting a prototype straight into a singleton’s constructor
would create it only once — that is why ObjectProvider is used to ask for a fresh one each time.
Web apps add request and session scopes on the same idea.
@Value and @ConfigurationProperties
app.name=shop
mail.host=smtp.example.com
mail.port=587
mail.timeout=5s
mail.admins=ops@example.com,cto@example.com
@ConfigurationProperties(prefix = "mail")
record MailProperties(String host, int port, Duration timeout, List<String> admins) {}
@SpringBootApplication
@ConfigurationPropertiesScan // finds @ConfigurationProperties records
public class DiApplication implements CommandLineRunner {
private final MailProperties mail;
private final String appName;
DiApplication(MailProperties mail, @Value("${app.name:unnamed}") String appName) {
this.mail = mail;
this.appName = appName;
}
public static void main(String[] args) { SpringApplication.run(DiApplication.class, args); }
@Override
public void run(String... args) {
System.out.println("app: " + appName);
System.out.println(mail);
System.out.println("timeout in ms: " + mail.timeout().toMillis());
}
}
$ java -jar target/di-0.0.1-SNAPSHOT.jar
app: shop
MailProperties[host=smtp.example.com, port=587, timeout=PT5S, admins=[ops@example.com, cto@example.com]]
timeout in ms: 5000
Every key under mail. was bound into one immutable record and converted on the way: "587" to an
int, "5s" to a Duration (printed in ISO-8601 as PT5S), and the comma-separated string to a
List. The record is an ordinary bean, injected like any other. @Value("${app.name:unnamed}")
pulls a single key with a fallback after the colon. For a handful of related settings, prefer the
record: one typed object is easier to test and to find than @Value strings scattered over classes.
The bean lifecycle
@Bean methods register objects you cannot annotate — library classes, or ones that need
constructor arguments.
class ConnectionPool { // a class you don't own: no annotations
private final int size;
ConnectionPool(int size) { this.size = size; System.out.println("ConnectionPool constructor, size=" + size); }
void open() { System.out.println("ConnectionPool.open() (initMethod): " + size + " connections"); }
void close() { System.out.println("ConnectionPool.close() (destroyMethod)"); }
}
@Configuration
class PoolConfig {
@Bean(initMethod = "open", destroyMethod = "close")
ConnectionPool pool() { return new ConnectionPool(4); }
@PostConstruct void ready() { System.out.println("PoolConfig @PostConstruct"); }
@PreDestroy void gone() { System.out.println("PoolConfig @PreDestroy"); }
}
@SpringBootApplication
public class DiApplication implements CommandLineRunner {
DiApplication(ConnectionPool pool) { System.out.println("DiApplication constructor got the pool"); }
public static void main(String[] args) {
SpringApplication.run(DiApplication.class, args).close(); // close() triggers destruction
System.out.println("main() returns");
}
@Override
public void run(String... args) { System.out.println("CommandLineRunner.run()"); }
}
$ java -jar target/di-0.0.1-SNAPSHOT.jar
PoolConfig @PostConstruct
ConnectionPool constructor, size=4
ConnectionPool.open() (initMethod): 4 connections
DiApplication constructor got the pool
CommandLineRunner.run()
ConnectionPool.close() (destroyMethod)
PoolConfig @PreDestroy
main() returns
The @Configuration class is itself a bean, built and initialised before its @Bean methods run.
Each bean is fully constructed and initialised before anything that depends on it receives it, so
DiApplication never sees an unopened pool. Runners fire once the context is ready. On close() —
which also runs on a normal JVM shutdown — beans are destroyed in reverse dependency order: the pool
closes before the configuration that created it. @PostConstruct/@PreDestroy (from
jakarta.annotation) do the same job for your own classes.
Testing without Spring
Because OrderService from the first example takes its Notifier in the constructor, a unit test
needs no container at all.
package com.example.di;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class OrderServiceTest {
@Test
void placeOrderNotifiesTheCustomer() {
Notifier fake = (to, text) -> "FAKE " + to + " " + text; // no Spring, no email server
OrderService service = new OrderService(fake);
assertEquals("FAKE bob order placed", service.placeOrder("bob"));
}
}
$ mvn -B test 2>&1 | grep -E 'Tests run|BUILD'
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.046 s -- in com.example.di.OrderServiceTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
Notifier has one abstract method, so a lambda is a complete fake. Field injection
(@Autowired private Notifier notifier;) would make this impossible without reflection or a Spring
test context — the main reason constructor injection is the recommended style.
Practice
- Add a third implementation,
PushNotifier, pointAlertServiceat it, and changeBroadcastServiceto injectMap<String, Notifier>so it prints each bean’s name.
Solution
@Component
class PushNotifier implements Notifier {
public String send(String to, String text) { return "push to " + to + ": " + text; }
}
// in AlertService:
AlertService(@Qualifier("pushNotifier") Notifier notifier) { this.notifier = notifier; }
@Service
class BroadcastService {
private final Map<String, Notifier> byName; // bean name -> bean
BroadcastService(Map<String, Notifier> byName) { this.byName = byName; }
void broadcast(String c) { byName.forEach((name, n) -> System.out.println(" " + name + " -> " + n.send(c, "maintenance"))); }
}
$ java -jar target/di-0.0.1-SNAPSHOT.jar
email to ada: order placed
push to ada: card declined
broadcast:
emailNotifier -> email to ada: maintenance
pushNotifier -> push to ada: maintenance
smsNotifier -> sms to ada: maintenance
@Primary still makes OrderService pick email even though there are now three candidates.
- Put
@Scope("prototype")onCounteras well. Predict all three lines before running.
Solution
@Component
@Scope("prototype")
class Counter {
private final AtomicInteger n = new AtomicInteger();
int next() { return n.incrementAndGet(); }
}
$ java -jar target/di-0.0.1-SNAPSHOT.jar
a == b: false
a.next()=1, b.next()=1
cart ids: 1, 2 same? false
Each constructor parameter now gets its own Counter, so each counts from 1 independently.
- Add a
fromcomponent toMailPropertiesthat defaults tonoreply@example.comwhen nomail.fromkey is set, then override it from the command line.
Solution
// import org.springframework.boot.context.properties.bind.DefaultValue;
@ConfigurationProperties(prefix = "mail")
record MailProperties(String host, int port, Duration timeout, List<String> admins,
@DefaultValue("noreply@example.com") String from) {}
$ java -jar target/di-0.0.1-SNAPSHOT.jar
app: shop
MailProperties[host=smtp.example.com, port=587, timeout=PT5S, admins=[ops@example.com, cto@example.com], from=noreply@example.com]
timeout in ms: 5000
$ java -jar target/di-0.0.1-SNAPSHOT.jar --mail.from=shop@example.com | grep from
MailProperties[host=smtp.example.com, port=587, timeout=PT5S, admins=[ops@example.com, cto@example.com], from=shop@example.com]