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

Как хендлить ошибки пользователя и отправлять ему необходимый результат вместо ошибки в Spring

1.8 Middle🔥 251 комментариев
#REST API и микросервисы#Spring Framework#Безопасность

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

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

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

Как хендлить ошибки пользователя и отправлять ему необходимый результат вместо ошибки в Spring

Правильная обработка ошибок — критично для пользовательского опыта. В Spring есть несколько способов красиво обрабатывать исключения и возвращать понятные ошибки вместо stack trace.

1. Custom Exception классы

Сначала определи собственные исключения:

public class ApiException extends RuntimeException {
    private final String errorCode;
    private final int httpStatus;
    
    public ApiException(String message, String errorCode, int httpStatus) {
        super(message);
        this.errorCode = errorCode;
        this.httpStatus = httpStatus;
    }
    
    public String getErrorCode() {
        return errorCode;
    }
    
    public int getHttpStatus() {
        return httpStatus;
    }
}

public class UserNotFoundException extends ApiException {
    public UserNotFoundException(String userId) {
        super("User not found: " + userId, "USER_NOT_FOUND", 404);
    }
}

public class InvalidInputException extends ApiException {
    public InvalidInputException(String message) {
        super(message, "INVALID_INPUT", 400);
    }
}

2. Unified Error Response DTO

Создай стандартный формат ответа об ошибке:

public class ErrorResponse {
    private String errorCode;
    private String message;
    private long timestamp;
    private String path;
    private Map<String, String> details;
    
    public ErrorResponse(String errorCode, String message, String path) {
        this.errorCode = errorCode;
        this.message = message;
        this.timestamp = System.currentTimeMillis();
        this.path = path;
    }
    
    // Getters и setters
    public String getErrorCode() { return errorCode; }
    public String getMessage() { return message; }
    public long getTimestamp() { return timestamp; }
    public String getPath() { return path; }
    public Map<String, String> getDetails() { return details; }
    
    public void setDetails(Map<String, String> details) {
        this.details = details;
    }
}

3. Global Exception Handler с @ControllerAdvice

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

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;

@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleUserNotFound(
            UserNotFoundException ex,
            WebRequest request) {
        
        ErrorResponse response = new ErrorResponse(
            ex.getErrorCode(),
            ex.getMessage(),
            request.getDescription(false).replace("uri=", "")
        );
        
        return new ResponseEntity<>(response, 
            HttpStatus.valueOf(ex.getHttpStatus()));
    }
    
    @ExceptionHandler(InvalidInputException.class)
    public ResponseEntity<ErrorResponse> handleInvalidInput(
            InvalidInputException ex,
            WebRequest request) {
        
        ErrorResponse response = new ErrorResponse(
            ex.getErrorCode(),
            ex.getMessage(),
            request.getDescription(false).replace("uri=", "")
        );
        
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGlobalException(
            Exception ex,
            WebRequest request) {
        
        ErrorResponse response = new ErrorResponse(
            "INTERNAL_ERROR",
            "An unexpected error occurred",
            request.getDescription(false).replace("uri=", "")
        );
        
        ex.printStackTrace();
        
        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

4. Валидация с @Valid и BindingResult

Для обработки ошибок валидации входных данных:

import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import org.springframework.validation.BindingResult;

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @PostMapping
    public ResponseEntity<?> createUser(
            @Valid @RequestBody CreateUserRequest request,
            BindingResult bindingResult) {
        
        if (bindingResult.hasErrors()) {
            Map<String, String> errors = new HashMap<>();
            bindingResult.getFieldErrors().forEach(error ->
                errors.put(error.getField(), error.getDefaultMessage())
            );
            
            ErrorResponse response = new ErrorResponse(
                "VALIDATION_ERROR",
                "Input validation failed",
                "/api/users"
            );
            response.setDetails(errors);
            
            return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
        }
        
        return new ResponseEntity<>(new UserDto(), HttpStatus.CREATED);
    }
}

5. DTO для валидации

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;

public class CreateUserRequest {
    @NotBlank(message = "Name is required")
    private String name;
    
    @Email(message = "Invalid email format")
    private String email;
    
    @Min(value = 18, message = "Age must be at least 18")
    private int age;
    
    // Getters и setters
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

6. Пример использования в сервисе

@Service
public class UserService {
    
    private final UserRepository userRepository;
    
    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    public UserDto getUserById(String userId) {
        return userRepository.findById(userId)
            .map(user -> new UserDto(user))
            .orElseThrow(() -> new UserNotFoundException(userId));
    }
    
    public UserDto createUser(CreateUserRequest request) {
        if (userRepository.existsByEmail(request.getEmail())) {
            throw new InvalidInputException("Email already exists");
        }
        
        User user = new User();
        user.setName(request.getName());
        user.setEmail(request.getEmail());
        user.setAge(request.getAge());
        
        User saved = userRepository.save(user);
        return new UserDto(saved);
    }
}

Важные моменты

  • Не отправляй stack trace пользователю — это уязвимость безопасности
  • Используй логирование — логируй полные ошибки в бэкенде для отладки
  • Стандартный формат — всегда возвращай один формат для ошибок
  • HTTP статусы — используй правильные статусы (400 для валидации, 404 для not found, 500 для server errors)
  • Информативные сообщения — помогай пользователю понять, что пошло не так

Рекомендация: используй @ControllerAdvice для глобальной обработки всех исключений, @Valid для валидации входных данных, и собственные исключения для бизнес-логики.