← Назад к вопросам

Какая область ответственности у Controller в Spring?

1.6 Junior🔥 241 комментариев
#Spring Framework

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Область ответственности 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 и бизнес-логикой.

Какая область ответственности у Controller в Spring? | PrepBro