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

Какие знаешь способы обработки ошибок в RestController?

1.7 Middle🔥 171 комментариев
#Spring Boot и Spring Data#Spring Framework

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

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

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

Способы обработки ошибок в RestController

Обзор подходов

В Spring есть несколько способов обработки ошибок в REST контроллерах:

  1. @ExceptionHandler — обработчики в контроллере
  2. @ControllerAdvice / @RestControllerAdvice — глобальная обработка
  3. ProblemDetail — стандартизированный формат ошибок (Spring 6.0+)
  4. ResponseEntity — явное возвращение HTTP ответов
  5. Встроенные исключения Spring — BadRequestException, NotFound и т.д.

1. @ExceptionHandler в контроллере

Обработчики для конкретного контроллера:

@RestController
@RequestMapping("/users")
public class UserController {
    
    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        if (id <= 0) {
            throw new IllegalArgumentException("ID must be positive");
        }
        // ... логика
        return ResponseEntity.ok(user);
    }
    
    // Обработчик для IllegalArgumentException
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleIllegalArgument(
        IllegalArgumentException ex,
        HttpServletRequest request
    ) {
        ErrorResponse error = ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(HttpStatus.BAD_REQUEST.value())
            .error("Invalid Argument")
            .message(ex.getMessage())
            .path(request.getRequestURI())
            .build();
        
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(error);
    }
}

2. @RestControllerAdvice — глобальная обработка (рекомендуется)

Централизованная обработка всех ошибок приложения:

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    // Обработка NotFoundException
    @ExceptionHandler(NotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(
        NotFoundException ex,
        HttpServletRequest request
    ) {
        log.warn("Resource not found: {}", ex.getMessage());
        
        ErrorResponse error = ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(HttpStatus.NOT_FOUND.value())
            .error("Not Found")
            .message(ex.getMessage())
            .path(request.getRequestURI())
            .build();
        
        return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(error);
    }
    
    // Обработка IllegalArgumentException
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleBadRequest(
        IllegalArgumentException ex,
        HttpServletRequest request
    ) {
        ErrorResponse error = ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(HttpStatus.BAD_REQUEST.value())
            .error("Bad Request")
            .message(ex.getMessage())
            .path(request.getRequestURI())
            .build();
        
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(error);
    }
    
    // Обработка MethodArgumentNotValidException (валидация)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(
        MethodArgumentNotValidException ex,
        HttpServletRequest request
    ) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(
            error -> errors.put(error.getField(), error.getDefaultMessage())
        );
        
        ErrorResponse error = ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(HttpStatus.BAD_REQUEST.value())
            .error("Validation Failed")
            .message("Input validation failed")
            .validationErrors(errors)
            .path(request.getRequestURI())
            .build();
        
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(error);
    }
    
    // Обработка всех остальных исключений
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(
        Exception ex,
        HttpServletRequest request
    ) {
        log.error("Unexpected error", ex);
        
        ErrorResponse error = ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(HttpStatus.INTERNAL_SERVER_ERROR.value())
            .error("Internal Server Error")
            .message("An unexpected error occurred")
            .path(request.getRequestURI())
            .build();
        
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(error);
    }
}

3. Модель ответа об ошибке

@Data
@Builder
public class ErrorResponse {
    private LocalDateTime timestamp;
    private int status;
    private String error;
    private String message;
    private String path;
    private Map<String, String> validationErrors;
    
    public static ErrorResponse of(
        int status,
        String error,
        String message,
        String path
    ) {
        return ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(status)
            .error(error)
            .message(message)
            .path(path)
            .build();
    }
}

4. Пользовательские исключения

// Базовое приложением исключение
public abstract class ApplicationException extends RuntimeException {
    private final HttpStatus status;
    
    public ApplicationException(String message, HttpStatus status) {
        super(message);
        this.status = status;
    }
    
    public HttpStatus getStatus() {
        return status;
    }
}

// Не найдено
public class NotFoundException extends ApplicationException {
    public NotFoundException(String message) {
        super(message, HttpStatus.NOT_FOUND);
    }
}

// Конфликт
public class ConflictException extends ApplicationException {
    public ConflictException(String message) {
        super(message, HttpStatus.CONFLICT);
    }
}

// Использование
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
    User user = userRepository.findById(id)
        .orElseThrow(() -> new NotFoundException(
            "User with id " + id + " not found"
        ));
    return ResponseEntity.ok(UserDto.from(user));
}

5. ProblemDetail (Spring 6.0+)

Стандартизированный формат RFC 7807:

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(NotFoundException.class)
    public ProblemDetail handleNotFound(NotFoundException ex) {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(
            HttpStatus.NOT_FOUND,
            ex.getMessage()
        );
        problem.setTitle("Resource Not Found");
        problem.setType(URI.create("https://api.example.com/errors/not-found"));
        problem.setProperty("timestamp", LocalDateTime.now());
        
        return problem;
    }
}

6. ResponseStatusException

Простой способ для простых случаев:

@GetMapping("/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
    User user = userRepository.findById(id)
        .orElseThrow(() -> new ResponseStatusException(
            HttpStatus.NOT_FOUND,
            "User not found with id: " + id
        ));
    return ResponseEntity.ok(UserDto.from(user));
}

7. Валидация с аннотациями

public class UserCreateRequest {
    @NotBlank(message = "Name cannot be blank")
    private String name;
    
    @Email(message = "Email should be valid")
    private String email;
    
    @Min(value = 18, message = "Age must be at least 18")
    private int age;
}

@PostMapping
public ResponseEntity<UserDto> createUser(
    @Valid @RequestBody UserCreateRequest request
) {
    // request.getName(), request.getEmail() гарантированно валидны
    User user = userService.create(request);
    return ResponseEntity.status(HttpStatus.CREATED).body(UserDto.from(user));
}

8. Логирование ошибок

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handle(Exception ex, HttpServletRequest request) {
        String correlationId = UUID.randomUUID().toString();
        
        log.error(
            "Error [{}] at {} {}: {}",
            correlationId,
            request.getMethod(),
            request.getRequestURI(),
            ex.getMessage(),
            ex
        );
        
        ErrorResponse error = ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(HttpStatus.INTERNAL_SERVER_ERROR.value())
            .error("Internal Server Error")
            .message("Please contact support with ID: " + correlationId)
            .build();
        
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(error);
    }
}

9. Порядок приоритета обработчиков

  1. @ExceptionHandler в контроллере
  2. @RestControllerAdvice с наиболее конкретным типом исключения
  3. Spring встроенные обработчики (400, 404, 500 и т.д.)
  4. Сервер возвращает стандартный HTML ответ

Лучшие практики

  1. Используй @RestControllerAdvice для централизованной обработки
  2. Создавай пользовательские исключения с нужным HTTP статусом
  3. Логируй все ошибки с корреляционным ID
  4. Возвращай консистентный формат ошибок
  5. Не выдавай деталей реализации в ошибках (защита от инъекций)
  6. Используй правильные HTTP статусы (400, 404, 409, 500 и т.д.)
  7. Валидируй входные данные с @Valid
  8. Избегай множественных @ExceptionHandler в одном контроллере

Пример полного REST контроллера

@RestController
@RequestMapping("/api/v1/users")
@Validated
public class UserController {
    
    @GetMapping("/{id}")
    public ResponseEntity<UserDto> getUser(@PathVariable @Positive Long id) {
        User user = userService.findById(id)
            .orElseThrow(() -> new NotFoundException(
                "User with id " + id + " not found"
            ));
        return ResponseEntity.ok(UserDto.from(user));
    }
    
    @PostMapping
    public ResponseEntity<UserDto> createUser(
        @Valid @RequestBody UserCreateRequest request
    ) {
        User user = userService.create(request);
        return ResponseEntity
            .status(HttpStatus.CREATED)
            .body(UserDto.from(user));
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<UserDto> updateUser(
        @PathVariable @Positive Long id,
        @Valid @RequestBody UserUpdateRequest request
    ) {
        User user = userService.update(id, request)
            .orElseThrow(() -> new NotFoundException(
                "User with id " + id + " not found"
            ));
        return ResponseEntity.ok(UserDto.from(user));
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable @Positive Long id) {
        userService.delete(id);
        return ResponseEntity.noContent().build();
    }
}

Правильная обработка ошибок — ключ к надёжному и понятному REST API.

Какие знаешь способы обработки ошибок в RestController? | PrepBro