Внедрение зависимостей
Классы приложения опираются друг на друга: контроллер использует сервис, сервис — хранилище данных. Такие связи называют зависимостями. При внедрении зависимостей (dependency injection) класс не создаёт их сам, а получает готовыми снаружи — в Spring их передаёт контейнер.
Хранилище статей
Пока у нас нет базы данных, статьи будем держать в памяти. Сначала запись для статьи:
// 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) {
}Теперь хранилище. Аннотация @Repository делает его бином:
// 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 и AtomicLong безопасны, когда запросы приходят одновременно из разных потоков. Данные живут, пока работает программа; в четвёртой главе их место займёт база.
Внедрение через конструктор
// 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)));
}
}Создавая PostService, Spring смотрит на параметры конструктора, находит бины PostRepository и Clock (из AppConfig прошлого урока) и передаёт их. Если конструктор один, аннотация @Autowired не нужна. Этот сервис заменяет учебный PostService из прошлого урока, а TitleController можно удалить.
Почему именно конструктор
- Поля можно объявить
final— зависимость не подменят после создания. - Сразу видно, что нужно классу для работы.
- В тесте объект легко создать вручную:
new PostService(repository, clock).
Встречается и внедрение в поле — @Autowired private PostRepository repository;. Оно короче, но прячет зависимости и мешает тестам. Пишите конструктор.
Если бинов несколько
Когда подходящих бинов два, Spring не знает, какой выбрать. Пометьте основной аннотацией @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"));
}Там, где нужен именно второй, укажите его имя в параметре: @Qualifier("moscowClock") Clock clock.
Частая ошибка
Без @Primary и @Qualifier при двух бинах Clock запуск остановится:
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 перечисляет кандидатов и советует добавить @Primary или @Qualifier.