Вивчення Spring MVC нагадує процес "очеловічення" робота: ти додаєш вуха (контролери для прийому запитів), мозок (сервіси для обробки логіки) і руки (репозиторії для роботи з базою даних). Сьогодні ми зосередимося на вухах і мозку. Репозиторії вже були створені раніше, тому варто доручити їм безпосередню взаємодію з базою даних.
Що таке контролери і сервіси в Spring MVC?
Контролери отримують вхідні запити і вирішують, як їх обробити, передаючи задачі сервісам. Сервіси, в свою чергу, реалізують бізнес-логіку, а якщо потрібно, звертаються до репозиторіїв (аналог складу з інгредієнтами).
Controller:
- Відповідає за маршрутизацію запитів до правильного обробника.
- Обробляє вхідні дані (наприклад, JSON з HTTP-запиту).
- Повертає відповідь клієнту.
Service:
- Виконує основну бізнес-логіку.
- Декларує і/або використовує методи репозиторія для роботи з базою даних.
- Гарантує, що функціональність є ізольованою і повторно використовуваною.
Створення REST-контролерів: обробка вхідних HTTP-запитів
REST-контролери — це серце твого вебзастосунку. Вони приймають HTTP-запити (GET, POST, PUT, DELETE) і повертають відповіді. Для їх створення використовується анотація @RestController, а ендпоінти задаються за допомогою анотацій @RequestMapping, @GetMapping, @PostMapping тощо.
Приклад мінімального контролера:
@RestController // Каже Spring, що цей клас — контролер
@RequestMapping("/api/v1/users") // Визначає базовий шлях для всіх ендпоінтів у цьому контролері
public class UserController {
@GetMapping("/{id}") // Обробка GET-запитів за шляхом /api/v1/users/{id}
public String getUserById(@PathVariable Long id) {
return "Користувач з ID: " + id; // Повертає рядок з ID користувача
}
@PostMapping // Обробка POST-запитів на /api/v1/users
public String createUser(@RequestBody String userName) {
return "Створено користувача: " + userName; // Повертає повідомлення
}
}
@RestController: вказує, що повернені дані будуть у форматі JSON або XML.@RequestMapping: встановлює базовий URL для всіх обробників у цьому контролері.@GetMapping,@PostMappingі т.д.: налаштовують обробку певних HTTP-методів (GET, POST, DELETE, PUT).@PathVariable: дозволяє витягати параметри з частини URL.@RequestBody: читає тіло запиту, перетворюючи його з JSON в об'єкт Java.
Розробка сервісного шару для бізнес-логіки
Як ми вже зрозуміли, контролери не повинні містити бізнес-логіку — це зона відповідальності сервісів. Вони виступають містком між контролерами і репозиторіями. Сервіси створюють як компоненти Spring з використанням анотації @Service.
Приклад сервісу:
@Service // Позначає клас як Spring-компонент
public class UserService {
private final UserRepository userRepository; // Репозиторій для взаємодії з базою даних
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(Long id) {
return userRepository.findById(id) // Отримуємо користувача з бази
.orElseThrow(() -> new RuntimeException("Користувача не знайдено!")); // Помилка, якщо користувача не знайдено
}
public void createUser(User user) {
userRepository.save(user); // Зберігаємо користувача в базу даних
}
}
@Service: повідомляє Spring, що це компонент, що містить бізнес-логіку.UserRepository: автоматично інжектиться через конструктор (Dependency Injection, DI).findByIdіsave: стандартні методи репозиторія для CRUD-операцій.
Інтеграція контролерів з сервісами
Контролери і сервіси працюють у зв'язці. Контролер викликає методи сервісу для обробки запитів. Для цього ми передаємо сервіс як залежність у контролер через DI.
Приклад інтеграції:
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService; // Впровадження залежності
}
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return userService.getUserById(id); // Виклик методу сервісу
}
@PostMapping
public void createUser(@RequestBody User user) {
userService.createUser(user); // Виклик методу сервісу
}
}
Покращення структури проєкту через використання патернів
Щоб твій проєкт був зрозумілим і підтримуваним, варто слідувати таким патернам:
- Controller-Service-Repository: ніколи не змішуй логіку з контролерів і сервісів.
- DTO (Data Transfer Object): використовуй DTO для передачі даних між шарами. Це особливо важливо, коли структура сутності бази даних і даних, яких очікує клієнт, відрізняються.
- Розподіл обов'язків: контролери займаються тільки запитами, сервіси реалізують логіку, а репозиторії працюють з базою даних.
Покроковий приклад розробки
Створимо просту функціональність для роботи з користувачами.
1. Сутність:
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// Геттери і сеттери...
}
2. Репозиторій:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Можливі додаткові кастомні запити
}
3. Сервіс:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(Long id) {
return userRepository.findById(id).orElseThrow(() -> new RuntimeException("Користувача не знайдено"));
}
public List<User> getAllUsers() {
return userRepository.findAll();
}
public void saveUser(User user) {
userRepository.save(user);
}
}
4. Контролер:
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUserById(id));
}
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
return ResponseEntity.ok(userService.getAllUsers());
}
@PostMapping
public ResponseEntity<String> createUser(@RequestBody User user) {
userService.saveUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body("Користувача успішно створено");
}
}
Типові помилки та способи їх усунення
- Змішування логіки в контролері: не пиши бізнес-логіку в контролері. Сервіси краще для цього підходять.
- Відсутність обробки помилок: переконайся, що обробляєш виключення (наприклад, через
@ExceptionHandlerабо@ControllerAdvice). - Відсутня валідація даних: перед використанням даних з запитів застосуй валідацію через анотацію
@Valid. - Циклічні залежності: використовуй DI коректно, уникаючи взаємних посилань між компонентами.
Тепер твої контролери і сервіси готові обробляти запити, а мозок і вуха твого застосунку чудово відпрацювали. У наступній лекції ми продовжимо будівництво нашого full-stack і зануримося в питання безпеки та аутентифікації через Spring Security!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ