Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Внедрение зависимостей

Классы приложения опираются друг на друга: контроллер использует сервис, сервис — хранилище данных. Такие связи называют зависимостями. При внедрении зависимостей (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.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.