Dependency injection
Application classes rely on each other: a controller uses a service, a service uses a data store. These links are called dependencies. With dependency injection a class does not create them itself but receives them ready-made from outside — in Spring the container passes them in.
A post store
We have no database yet, so posts will live in memory. First a record for a post:
// src/main/java/com/example/blog/Post.java
package com.example.blog;
import java.time.Instant;
public record Post(Long id, String title, String body, String author, Instant createdAt) {
}Now the store. The @Repository annotation makes it a bean:
// src/main/java/com/example/blog/PostRepository.java
package com.example.blog;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Repository;
@Repository
public class PostRepository {
private final Map<Long, Post> posts = new ConcurrentHashMap<>();
private final AtomicLong nextId = new AtomicLong(1);
public List<Post> findAll() {
return new ArrayList<>(posts.values());
}
public Optional<Post> findById(Long id) {
return Optional.ofNullable(posts.get(id));
}
public Post save(Post post) {
Long id = post.id() != null ? post.id() : nextId.getAndIncrement();
Post saved = new Post(id, post.title(), post.body(), post.author(), post.createdAt());
posts.put(id, saved);
return saved;
}
public void deleteById(Long id) {
posts.remove(id);
}
}ConcurrentHashMap and AtomicLong are safe when requests arrive at the same time from different threads. The data lives while the program runs; in chapter four a database takes its place.
Constructor injection
// src/main/java/com/example/blog/PostService.java
package com.example.blog;
import java.time.Clock;
import java.time.Instant;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class PostService {
private final PostRepository repository;
private final Clock clock;
public PostService(PostRepository repository, Clock clock) {
this.repository = repository;
this.clock = clock;
}
public List<Post> findAll() {
return repository.findAll();
}
public Post create(String title, String body, String author) {
return repository.save(new Post(null, title, body, author, Instant.now(clock)));
}
}When creating PostService, Spring looks at the constructor parameters, finds the PostRepository and Clock beans (the latter from AppConfig in the previous lesson) and passes them in. With a single constructor no @Autowired annotation is needed. This service replaces the practice PostService from the previous lesson, and TitleController can be deleted.
Why the constructor
- Fields can be
final— nobody can swap a dependency after creation. - You see right away what the class needs to work.
- In a test the object is easy to create by hand:
new PostService(repository, clock).
You will also see field injection — @Autowired private PostRepository repository;. It is shorter, but it hides dependencies and gets in the way of tests. Write a constructor.
When several beans match
When two beans fit, Spring does not know which one to choose. Mark the main one with @Primary:
// src/main/java/com/example/blog/AppConfig.java
@Bean
@Primary
public Clock utcClock() {
return Clock.systemUTC();
}
@Bean
public Clock moscowClock() {
return Clock.system(ZoneId.of("Europe/Moscow"));
}Where you need the second one specifically, name it on the parameter: @Qualifier("moscowClock") Clock clock.
Common mistake
Without @Primary or @Qualifier, two Clock beans stop the startup:
Parameter 1 of constructor in com.example.blog.PostService required a single bean, but 2 were found:
- utcClock: defined by method 'utcClock' in class path resource [com/example/blog/AppConfig.class]
- moscowClock: defined by method 'moscowClock' in class path resource [com/example/blog/AppConfig.class]Spring lists the candidates and suggests adding @Primary or @Qualifier.