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

Как организовать работу с исключениями в проекте с несколькими REST-контроллерами, каждый из которых реализует CRUD-операции для разных сущностей

2.3 Middle🔥 221 комментариев
#REST API и микросервисы#Spring Framework

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

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

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

Организация обработки исключений в REST API с несколькими контроллерами

Правильная организация обработки исключений - ключевой аспект надежного REST API. Вот лучшие практики для проекта с несколькими контроллерами.

1. Базовые domain исключения

// Базовое исключение для приложения
public abstract class ApplicationException extends RuntimeException {
    private final ErrorCode errorCode;
    
    public ApplicationException(ErrorCode errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }
    
    public ApplicationException(ErrorCode errorCode, String message, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }
    
    public ErrorCode getErrorCode() {
        return errorCode;
    }
}

// Коды ошибок
public enum ErrorCode {
    RESOURCE_NOT_FOUND("ERR_001", "Resource not found"),
    INVALID_INPUT("ERR_002", "Invalid input data"),
    RESOURCE_ALREADY_EXISTS("ERR_003", "Resource already exists"),
    UNAUTHORIZED("ERR_004", "Unauthorized access"),
    FORBIDDEN("ERR_005", "Forbidden operation"),
    INTERNAL_ERROR("ERR_006", "Internal server error");
    
    private final String code;
    private final String message;
    
    ErrorCode(String code, String message) {
        this.code = code;
        this.message = message;
    }
    
    public String getCode() { return code; }
    public String getMessage() { return message; }
}

2. Специфичные исключения для разных сущностей

// Исключение для User
public class UserNotFoundException extends ApplicationException {
    public UserNotFoundException(String userId) {
        super(ErrorCode.RESOURCE_NOT_FOUND, 
              String.format("User with id %s not found", userId));
    }
}

public class UserAlreadyExistsException extends ApplicationException {
    public UserAlreadyExistsException(String email) {
        super(ErrorCode.RESOURCE_ALREADY_EXISTS, 
              String.format("User with email %s already exists", email));
    }
}

// Исключение для Product
public class ProductNotFoundException extends ApplicationException {
    public ProductNotFoundException(String productId) {
        super(ErrorCode.RESOURCE_NOT_FOUND, 
              String.format("Product with id %s not found", productId));
    }
}

public class InvalidProductDataException extends ApplicationException {
    public InvalidProductDataException(String message) {
        super(ErrorCode.INVALID_INPUT, message);
    }
}

// Исключение для Order
public class OrderNotFoundException extends ApplicationException {
    public OrderNotFoundException(String orderId) {
        super(ErrorCode.RESOURCE_NOT_FOUND, 
              String.format("Order with id %s not found", orderId));
    }
}

public class InsufficientInventoryException extends ApplicationException {
    public InsufficientInventoryException(String productId) {
        super(ErrorCode.INVALID_INPUT, 
              String.format("Insufficient inventory for product %s", productId));
    }
}

3. DTO для ошибок

public class ErrorResponse {
    private String timestamp;
    private int status;
    private String error;
    private String code;
    private String message;
    private String path;
    private Map<String, String> validationErrors;  // Для валидации
    
    public ErrorResponse(int status, String error, String code, String message, String path) {
        this.timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);
        this.status = status;
        this.error = error;
        this.code = code;
        this.message = message;
        this.path = path;
        this.validationErrors = new HashMap<>();
    }
    
    // Getters and setters
    public String getTimestamp() { return timestamp; }
    public int getStatus() { return status; }
    public String getError() { return error; }
    public String getCode() { return code; }
    public String getMessage() { return message; }
    public String getPath() { return path; }
    public Map<String, String> getValidationErrors() { return validationErrors; }
    public void addValidationError(String field, String message) {
        validationErrors.put(field, message);
    }
}

4. Global Exception Handler

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
    @ExceptionHandler(ApplicationException.class)
    public ResponseEntity<ErrorResponse> handleApplicationException(
            ApplicationException ex,
            HttpServletRequest request) {
        
        ErrorCode errorCode = ex.getErrorCode();
        HttpStatus status = mapErrorCodeToStatus(errorCode);
        
        ErrorResponse response = new ErrorResponse(
            status.value(),
            status.getReasonPhrase(),
            errorCode.getCode(),
            ex.getMessage(),
            request.getRequestURI()
        );
        
        logger.warn("Application exception: {} - {}", errorCode.getCode(), ex.getMessage());
        return new ResponseEntity<>(response, status);
    }
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(
            MethodArgumentNotValidException ex,
            HttpServletRequest request) {
        
        ErrorResponse response = new ErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            "Validation Failed",
            ErrorCode.INVALID_INPUT.getCode(),
            "Input validation failed",
            request.getRequestURI()
        );
        
        // Добавляем детали валидации
        ex.getBindingResult().getFieldErrors().forEach(error ->
            response.addValidationError(error.getField(), error.getDefaultMessage())
        );
        
        logger.warn("Validation error: {}", response.getValidationErrors());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }
    
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public ResponseEntity<ErrorResponse> handleHttpMessageNotReadable(
            HttpMessageNotReadableException ex,
            HttpServletRequest request) {
        
        ErrorResponse response = new ErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            "Bad Request",
            ErrorCode.INVALID_INPUT.getCode(),
            "Invalid request format",
            request.getRequestURI()
        );
        
        logger.warn("Invalid request format: {}", ex.getMessage());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(
            Exception ex,
            HttpServletRequest request) {
        
        ErrorResponse response = new ErrorResponse(
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            "Internal Server Error",
            ErrorCode.INTERNAL_ERROR.getCode(),
            "An unexpected error occurred",
            request.getRequestURI()
        );
        
        logger.error("Unexpected error:", ex);
        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }
    
    private HttpStatus mapErrorCodeToStatus(ErrorCode errorCode) {
        return switch (errorCode) {
            case RESOURCE_NOT_FOUND -> HttpStatus.NOT_FOUND;
            case INVALID_INPUT, RESOURCE_ALREADY_EXISTS -> HttpStatus.BAD_REQUEST;
            case UNAUTHORIZED -> HttpStatus.UNAUTHORIZED;
            case FORBIDDEN -> HttpStatus.FORBIDDEN;
            case INTERNAL_ERROR -> HttpStatus.INTERNAL_SERVER_ERROR;
        };
    }
}

5. REST контроллер User

@RestController
@RequestMapping("/api/v1/users")
public class UserController {
    
    private final UserService userService;
    
    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<UserDto> getUser(@PathVariable String id) {
        // UserService выбросит UserNotFoundException если пользователь не найден
        UserDto user = userService.getUserById(id);
        return ResponseEntity.ok(user);
    }
    
    @PostMapping
    public ResponseEntity<UserDto> createUser(@Valid @RequestBody CreateUserRequest request) {
        // UserService выбросит UserAlreadyExistsException если такой email существует
        UserDto user = userService.createUser(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<UserDto> updateUser(
            @PathVariable String id,
            @Valid @RequestBody UpdateUserRequest request) {
        // UserService выбросит UserNotFoundException если пользователь не найден
        UserDto user = userService.updateUser(id, request);
        return ResponseEntity.ok(user);
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable String id) {
        // UserService выбросит UserNotFoundException если пользователь не найден
        userService.deleteUser(id);
        return ResponseEntity.noContent().build();
    }
}

6. REST контроллер Product

@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
    
    private final ProductService productService;
    
    @Autowired
    public ProductController(ProductService productService) {
        this.productService = productService;
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<ProductDto> getProduct(@PathVariable String id) {
        ProductDto product = productService.getProductById(id);
        return ResponseEntity.ok(product);
    }
    
    @PostMapping
    public ResponseEntity<ProductDto> createProduct(
            @Valid @RequestBody CreateProductRequest request) {
        ProductDto product = productService.createProduct(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(product);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<ProductDto> updateProduct(
            @PathVariable String id,
            @Valid @RequestBody UpdateProductRequest request) {
        ProductDto product = productService.updateProduct(id, request);
        return ResponseEntity.ok(product);
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteProduct(@PathVariable String id) {
        productService.deleteProduct(id);
        return ResponseEntity.noContent().build();
    }
}

7. Service слой с правильной обработкой ошибок

@Service
public class UserService {
    
    private final UserRepository userRepository;
    private static final Logger logger = LoggerFactory.getLogger(UserService.class);
    
    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    public UserDto getUserById(String id) {
        return userRepository.findById(id)
            .map(this::convertToDto)
            .orElseThrow(() -> new UserNotFoundException(id));
    }
    
    public UserDto createUser(CreateUserRequest request) {
        if (userRepository.existsByEmail(request.getEmail())) {
            throw new UserAlreadyExistsException(request.getEmail());
        }
        
        try {
            User user = new User();
            user.setEmail(request.getEmail());
            user.setName(request.getName());
            
            User savedUser = userRepository.save(user);
            logger.info("User created: {}", savedUser.getId());
            
            return convertToDto(savedUser);
        } catch (DataIntegrityViolationException e) {
            logger.error("Data integrity violation while creating user", e);
            throw new UserAlreadyExistsException(request.getEmail());
        }
    }
    
    public UserDto updateUser(String id, UpdateUserRequest request) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException(id));
        
        user.setName(request.getName());
        User updated = userRepository.save(user);
        
        logger.info("User updated: {}", id);
        return convertToDto(updated);
    }
    
    public void deleteUser(String id) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException(id));
        
        userRepository.delete(user);
        logger.info("User deleted: {}", id);
    }
    
    private UserDto convertToDto(User user) {
        return new UserDto(user.getId(), user.getEmail(), user.getName());
    }
}

8. Best Practices

  1. Иерархия исключений - все domain исключения расширяют ApplicationException
  2. Global Exception Handler - один @RestControllerAdvice для всех контроллеров
  3. Логирование - логируйте все исключения с правильным уровнем
  4. Понятные сообщения об ошибках - включайте достаточно контекста для отладки
  5. Коды ошибок - используйте enum для стандартизации
  6. Валидация на входе - используйте @Valid и @RequestBody
  7. HTTP статусы - правильно маппируйте ошибки на HTTP статусы
  8. Никогда не выбрасывайте null - всегда выбрасывайте исключение
  9. Документируйте исключения - указывайте какие исключения может выбросить метод
  10. Тестируйте обработку ошибок - пишите тесты для всех путей ошибок

Тестирование обработки ошибок

@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerExceptionHandlingTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    public void testGetNonExistentUser() throws Exception {
        mockMvc.perform(get("/api/v1/users/nonexistent"))
            .andExpect(status().isNotFound())
            .andExpect(jsonPath("$.code").value("ERR_001"))
            .andExpect(jsonPath("$.message").exists());
    }
    
    @Test
    public void testCreateUserWithInvalidEmail() throws Exception {
        mockMvc.perform(post("/api/v1/users")
            .contentType(MediaType.APPLICATION_JSON)
            .content("{\"email\": \"invalid\", \"name\": \"Test\"}"))
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.code").value("ERR_002"));
    }
}

Этот подход обеспечивает консистентную, масштабируемую и легко поддерживаемую систему обработки ошибок для REST API.

Как организовать работу с исключениями в проекте с несколькими REST-контроллерами, каждый из которых реализует CRUD-операции для разных сущностей | PrepBro