Какая область ответственности у Controller в Spring?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Область ответственности Controller в Spring
Controller в Spring — это компонент, ответственный за обработку HTTP запросов и возврат ответов. Это первый слой приложения, который взаимодействует с клиентом. Понимание правильной ответственности Controller критично для архитектуры приложения.
1. Основная ответственность Controller
Controller должен: ✓ Принимать HTTP запросы ✓ Валидировать входные данные ✓ Вызывать бизнес-логику (Service) ✓ Обрабатывать результаты ✓ Возвращать HTTP ответ с правильными кодами
Controller НЕ должен: ✗ Содержать бизнес-логику ✗ Работать напрямую с БД ✗ Содержать трансформации данных ✗ Управлять транзакциями
// ✓ ПРАВИЛЬНО: Controller только обрабатывает запросы
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
User user = userService.findById(id); // Делегируем Service
if (user == null) {
return ResponseEntity.notFound().build();
}
UserDTO dto = new UserDTO(user);
return ResponseEntity.ok(dto);
}
}
// ✗ НЕПРАВИЛЬНО: Controller содержит бизнес-логику
@RestController
public class UserController_BAD {
@Autowired
private UserRepository userRepository;
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userRepository.findById(id).orElse(null);
// НЕПРАВИЛЬНО: бизнес-логика в Controller!
if (user != null) {
user.setLastLoginTime(LocalDateTime.now());
user.incrementLoginCount();
userRepository.save(user);
}
return ResponseEntity.ok(user);
}
}
2. Структура HTTP Request → Response
HTTP Request
↓
┌─────────────────────────────────┐
│ Spring DispatcherServlet │ ← распределяет запрос
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ @RequestMapping / @GetMapping │ ← роутинг
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ Controller Method │ ← обработка запроса
│ 1. Получить параметры │
│ 2. Валидировать данные │
│ 3. Вызвать Service │
│ 4. Обработать результат │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ @ResponseBody / ResponseEntity │ ← сериализация
└─────────────────────────────────┘
↓
HTTP Response (JSON + Status Code)
3. Типы параметров Controller методов
@RestController
@RequestMapping("/api/v1/posts")
public class PostController {
@Autowired
private PostService postService;
// 1. Path Variable - часть URL
@GetMapping("/{id}")
public ResponseEntity<PostDTO> getById(@PathVariable Long id) {
Post post = postService.findById(id);
return ResponseEntity.ok(new PostDTO(post));
}
// 2. Query Parameters - параметры поиска
@GetMapping
public ResponseEntity<List<PostDTO>> searchPosts(
@RequestParam(required = false) String title,
@RequestParam(required = false) String author,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
SearchCriteria criteria = new SearchCriteria(title, author, page, size);
List<Post> posts = postService.search(criteria);
return ResponseEntity.ok(posts.stream()
.map(PostDTO::new)
.collect(Collectors.toList()));
}
// 3. Request Body - JSON тело запроса
@PostMapping
public ResponseEntity<PostDTO> createPost(@RequestBody @Valid CreatePostRequest req) {
Post post = postService.create(req);
return ResponseEntity.status(HttpStatus.CREATED)
.body(new PostDTO(post));
}
// 4. Headers - HTTP заголовки
@GetMapping("/user-posts")
public ResponseEntity<List<PostDTO>> getUserPosts(
@RequestHeader("Authorization") String token,
@RequestHeader(value = "X-Custom-Header", required = false) String custom) {
Long userId = authService.getUserIdFromToken(token);
List<Post> posts = postService.findByUserId(userId);
return ResponseEntity.ok(posts.stream()
.map(PostDTO::new)
.collect(Collectors.toList()));
}
// 5. Request Object целиком
@PostMapping("/with-request")
public ResponseEntity<Void> processRequest(HttpServletRequest request) {
String ip = request.getRemoteAddr();
String method = request.getMethod();
// Используем данные запроса
return ResponseEntity.ok().build();
}
}
4. Правильная обработка ошибок в Controller
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@ExceptionHandler(OrderNotFoundException.class)
public ResponseEntity<ErrorResponse> handleOrderNotFound(
OrderNotFoundException e) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("Order not found", e.getMessage()));
}
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ErrorResponse> handleValidationError(
ValidationException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("Validation failed", e.getMessage()));
}
@GetMapping("/{id}")
public ResponseEntity<OrderDTO> getOrder(@PathVariable Long id) {
try {
Order order = orderService.findById(id);
return ResponseEntity.ok(new OrderDTO(order));
} catch (OrderNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
@PostMapping
public ResponseEntity<OrderDTO> createOrder(
@RequestBody @Valid CreateOrderRequest req) {
try {
Order order = orderService.create(req);
return ResponseEntity
.status(HttpStatus.CREATED)
.body(new OrderDTO(order));
} catch (InsufficientFundsException e) {
return ResponseEntity
.status(HttpStatus.PAYMENT_REQUIRED)
.body(new ErrorResponse("Insufficient funds"));
}
}
}
// Глобальный Exception Handler
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception e) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("Internal server error", e.getMessage()));
}
}
5. Валидация данных в Controller
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public ResponseEntity<UserDTO> registerUser(
@RequestBody @Valid CreateUserRequest req,
BindingResult bindingResult) { // ← результаты валидации
// Spring автоматически валидирует, но можно проверить результат
if (bindingResult.hasErrors()) {
List<String> errors = bindingResult.getAllErrors()
.stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.toList());
return ResponseEntity
.badRequest()
.body(new ErrorResponse("Validation failed", errors));
}
User user = userService.registerUser(req);
return ResponseEntity
.status(HttpStatus.CREATED)
.body(new UserDTO(user));
}
}
@Data
@NotNull
public class CreateUserRequest {
@NotBlank(message = "Email is required")
@Email(message = "Email must be valid")
private String email;
@NotBlank(message = "Password is required")
@Length(min = 8, max = 100, message = "Password must be 8-100 characters")
private String password;
@Min(value = 18, message = "User must be 18 or older")
private Integer age;
@Pattern(regexp = "^\\+?[0-9\\-\\s]{10,}$", message = "Invalid phone number")
private String phone;
}
6. Правильная архитектура: Controller → Service → Repository
// СЛОЙ 1: Controller (HTTP Adapter)
@RestController
@RequestMapping("/api/v1/payments")
public class PaymentController {
@Autowired
private PaymentService paymentService;
@PostMapping("/{orderId}/process")
public ResponseEntity<PaymentResponseDTO> processPayment(
@PathVariable Long orderId,
@RequestBody ProcessPaymentRequest req) {
// 1. Принять данные
// 2. Базовая валидация
// 3. Делегировать Service
PaymentResult result = paymentService.processPayment(orderId, req.getAmount());
// 4. Преобразовать в DTO
// 5. Вернуть ответ
return ResponseEntity.ok(new PaymentResponseDTO(result));
}
}
// СЛОЙ 2: Service (Бизнес-логика)
@Service
@Transactional
public class PaymentService {
@Autowired
private PaymentRepository paymentRepository;
@Autowired
private OrderService orderService;
@Autowired
private NotificationService notificationService;
public PaymentResult processPayment(Long orderId, BigDecimal amount) {
// 1. Получить заказ
Order order = orderService.findById(orderId);
// 2. Выполнить бизнес-логику
if (amount.compareTo(order.getTotalAmount()) != 0) {
throw new PaymentException("Amount mismatch");
}
// 3. Создать платёж
Payment payment = new Payment();
payment.setOrderId(orderId);
payment.setAmount(amount);
payment.setStatus("PENDING");
Payment saved = paymentRepository.save(payment);
// 4. Вызвать другие сервисы
notificationService.notifyPaymentProcessed(order);
return new PaymentResult(saved);
}
}
// СЛОЙ 3: Repository (Доступ к БД)
@Repository
public interface PaymentRepository extends JpaRepository<Payment, Long> {
Optional<Payment> findByOrderId(Long orderId);
}
7. REST API Best Practices в Controller
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
@Autowired
private ProductService productService;
// GET - получение списка
@GetMapping
public ResponseEntity<Page<ProductDTO>> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String category) {
Page<Product> products = productService.findAll(
new PageRequest(page, size),
category);
return ResponseEntity.ok(
products.map(ProductDTO::new));
}
// GET - получение одного
@GetMapping("/{id}")
public ResponseEntity<ProductDTO> getById(@PathVariable Long id) {
Product product = productService.findById(id);
return ResponseEntity.ok(new ProductDTO(product));
}
// POST - создание
@PostMapping
public ResponseEntity<ProductDTO> create(
@RequestBody @Valid CreateProductRequest req) {
Product product = productService.create(req);
return ResponseEntity
.status(HttpStatus.CREATED)
.header("Location", "/api/v1/products/" + product.getId())
.body(new ProductDTO(product));
}
// PUT - полное обновление
@PutMapping("/{id}")
public ResponseEntity<ProductDTO> update(
@PathVariable Long id,
@RequestBody @Valid UpdateProductRequest req) {
Product product = productService.update(id, req);
return ResponseEntity.ok(new ProductDTO(product));
}
// PATCH - частичное обновление
@PatchMapping("/{id}")
public ResponseEntity<ProductDTO> partialUpdate(
@PathVariable Long id,
@RequestBody JsonPatch patch) {
Product product = productService.patchUpdate(id, patch);
return ResponseEntity.ok(new ProductDTO(product));
}
// DELETE - удаление
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
productService.delete(id);
return ResponseEntity.noContent().build();
}
}
8. HTTP Status Codes в Controller
// 2xx Success
ResponseEntity.ok() // 200 OK
ResponseEntity.created(uri) // 201 Created
ResponseEntity.accepted() // 202 Accepted
ResponseEntity.noContent() // 204 No Content
// 3xx Redirection
ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY) // 301
ResponseEntity.status(HttpStatus.FOUND) // 302
// 4xx Client Error
ResponseEntity.badRequest() // 400
ResponseEntity.unauthorized() // 401
ResponseEntity.forbidden() // 403
ResponseEntity.notFound() // 404
ResponseEntity.status(HttpStatus.CONFLICT) // 409
// 5xx Server Error
ResponseEntity.internalServerError() // 500
ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) // 503
Резюме: Ответственность Controller
Controller ДОЛЖЕН:
- Обрабатывать HTTP запросы
- Валидировать входные данные
- Вызывать Service (бизнес-логика)
- Преобразовывать ответы (в DTO)
- Возвращать правильные HTTP статусы
Controller НЕ ДОЛЖЕН:
- Содержать бизнес-логику
- Работать напрямую с Repository
- Управлять транзакциями
- Содержать SQL запросы
- Трансформировать объекты между слоями
Архитектура: Controller → Service → Repository
Правило золота: Controller — это adapter между HTTP и бизнес-логикой.