Сервисы и слои
За прошлые уроки в блоге сложились три вида классов: контроллер принимает запросы, сервис решает задачу, репозиторий хранит данные. Такое деление называют слоями. Каждый слой знает только о соседнем снизу, и это помогает менять одну часть, не ломая другие: в следующей главе мы заменим хранилище в памяти на базу данных, а контроллер даже не заметит.
Три слоя
| Слой | Аннотация | Отвечает за | Не должен |
|---|---|---|---|
| Контроллер | @RestController | Адреса, разбор запроса, коды ответа | Содержать правила предметной области |
| Сервис | @Service | Логику: проверки, расчёты, транзакции | Знать про HTTP и ResponseEntity |
| Репозиторий | @Repository | Чтение и запись данных | Решать, что можно сохранять |
Запрос идёт сверху вниз: контроллер → сервис → репозиторий. Ответ — обратно.
Сервис блога целиком
// src/main/java/com/example/blog/PostService.java
@Service
public class PostService {
private final PostRepository repository;
private final Clock clock;
private final BlogProperties properties;
public PostService(PostRepository repository, Clock clock, BlogProperties properties) {
this.repository = repository;
this.clock = clock;
this.properties = properties;
}
public List<Post> findAll(String author) {
return repository.findAll().stream()
.filter(post -> author == null || post.author().equals(author))
.sorted(Comparator.comparing(Post::id))
.limit(properties.pageSize())
.toList();
}
public Post findById(Long id) {
return repository.findById(id)
.orElseThrow(() -> new PostNotFoundException(id));
}
public Post create(NewPostRequest request) {
Post post = new Post(null, request.title(), request.body(), request.author(), Instant.now(clock));
return repository.save(post);
}
public Post update(Long id, NewPostRequest request) {
Post old = findById(id);
return repository.save(new Post(id, request.title(), request.body(), request.author(), old.createdAt()));
}
public void delete(Long id) {
findById(id);
repository.deleteById(id);
}
}Обратите внимание: сервис бросает PostNotFoundException, а не возвращает код 404. Он не знает, что его вызывают по HTTP. Превращать исключение в ответ — работа обработчика из прошлого урока.
Файлы проекта
src/main/java/com/example/blog/
├── BlogApplication.java
├── AppConfig.java настройки и бины
├── BlogProperties.java группа настроек
├── PostController.java слой контроллеров
├── ApiExceptionHandler.java ошибки → ответы
├── NewPostRequest.java входные данные
├── PostService.java слой сервисов
├── PostNotFoundException.java
├── PostRepository.java слой данных
└── Post.java модельПока классов немного, им удобно в одном пакете. Когда проект растёт, их раскладывают по пакетам-разделам: com.example.blog.post, com.example.blog.comment, com.example.blog.user. Всё про статьи лежит рядом, и не нужно прыгать по папкам controllers и services.
Как проверить всё сразу
curl -X POST http://localhost:8080/api/posts -H "Content-Type: application/json" \
-d '{"title":"Второй","body":"Текст","author":"oleg"}'
curl -X PUT http://localhost:8080/api/posts/2 -H "Content-Type: application/json" \
-d '{"title":"Второй (исправлено)","body":"Текст","author":"oleg"}'
curl -i -X DELETE http://localhost:8080/api/posts/2Создание вернёт 201, изменение — 200 с новым заголовком, удаление — 204. Повторное удаление той же статьи ответит 404 через обработчик ошибок.
Частая ошибка
Соблазнительно передать репозиторий прямо в контроллер и написать логику там: «найди, проверь, сохрани». Сначала это короче, но затем ту же логику понадобится вызвать из страницы сайта или фоновой задачи — и её придётся копировать. Держите контроллер тонким: принял запрос, вызвал сервис, вернул ответ.