← Назад к вопросам
Как организовать работу с исключениями в проекте с несколькими 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
- Иерархия исключений - все domain исключения расширяют ApplicationException
- Global Exception Handler - один @RestControllerAdvice для всех контроллеров
- Логирование - логируйте все исключения с правильным уровнем
- Понятные сообщения об ошибках - включайте достаточно контекста для отладки
- Коды ошибок - используйте enum для стандартизации
- Валидация на входе - используйте @Valid и @RequestBody
- HTTP статусы - правильно маппируйте ошибки на HTTP статусы
- Никогда не выбрасывайте null - всегда выбрасывайте исключение
- Документируйте исключения - указывайте какие исключения может выбросить метод
- Тестируйте обработку ошибок - пишите тесты для всех путей ошибок
Тестирование обработки ошибок
@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.