معالجة الاستثناءات العامة
معالجة الاستثناءات العامة تُوحّد تنسيق استجابات أخطاء API—لم يعد العملاء يضطرون للتعامل مع تشكيلة واسعة من تنسيقات الأخطاء، مما يجعل التصحيح والدمج أكثر كفاءة.
1. ما ستتعلمه
@RestControllerAdvice+@ExceptionHandlerمعالجة الاستثناءات العامة- نظام استثناءات الأعمال المخصص:
BusinessException/ResourceNotFoundException/ValidationException - تصميم DTO لاستجابة الخطأ: code / message / timestamp / details
- معالجة خاصة لـ
MethodArgumentNotValidExceptionلأخطاء التحقق - تسجيل الاستثناءات وتوليد مُعرّف تتبع الأخطاء
2. قصة حقيقية لمطوّر واجهة أمامية
(1) نقطة الألم: تنسيقات استجابة الخطأ غير متسقة
Bob هو مطوّر واجهة أمامية واجه مشاكل أثناء الدمج مع OrderFlow API: بعض نقاط النهاية تُرجع 404 بنص عادي "Not Found"، وبعضها يُرجع 500 بصفحة خطأ HTML، وبعضها يُرجع استثناء أعمال {"error": "xxx"}، وآخرون يُرجعون {"message": "xxx"}. كان عليه كتابة منطق معالجة أخطاء مختلف لكل نقطة نهاية، مما أدى إلى كتل try-catch متناثرة في كوده—وكثيراً ما كان يغفل عنها، مما يتسبب في شاشة بيضاء.
(2) حل @RestControllerAdvice
معالج استثناءات موحد يضمن أن جميع استجابات الأخطاء تتبع تنسيقاً متسقاً:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("NOT_FOUND", ex.getMessage(), Instant.now()));
}
}
(3) العوائد
بعد أن نفّذت Alice معالجة الاستثناءات العامة، أصبحت جميع تنسيقات استجابات الأخطاء موحدة كـ {code, message, timestamp, details}، واحتاجت واجهة Bob الأمامية لوظيفة معالجة أخطاء واحدة موحدة فقط، مما ضاعف كفاءة الدمج خمس مرات.
3. آلية @RestControllerAdvice
(1) عملية توجيه معالجة الاستثناءات
graph TD
A[المتحكم يرمي استثناءً] --> B{Spring DispatcherServlet}
B --> C["@RestControllerAdvice<br/>يمسح @ExceptionHandler"]
C --> D{نوع الاستثناء مطابق؟}
D -->|نعم| E["تنفيذ @ExceptionHandler<br/>إرجاع ErrorResponse"]
D -->|لا| F["Spring الافتراضي<br/>استجابة خطأ"]
E --> G["العميل يتلقى<br/>JSON متسق"]
F --> H["العميل يتلقى<br/>استجابة غير متسقة"]
| التعليق | الغرض | الموقع |
|---|---|---|
@RestControllerAdvice |
صنف معالجة الاستثناءات العامة | على الصنف |
@ExceptionHandler |
معالجة أنواع استثناءات محددة | على الطريقة |
@ResponseStatus |
تحديد رمز حالة الاستجابة | على صنف الاستثناء أو الطريقة |
(1) ▶ مثال: معالج استثناءات عام أساسي
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(
ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
"RESOURCE_NOT_FOUND", ex.getMessage(), Instant.now(), null);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusiness(
BusinessException ex) {
ErrorResponse error = new ErrorResponse(
"BUSINESS_ERROR", ex.getMessage(), Instant.now(), null);
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
ErrorResponse error = new ErrorResponse(
"INTERNAL_ERROR", "An unexpected error occurred", Instant.now(), null);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
الناتج:
// التنفيذ ناجح
4. نظام استثناءات الأعمال المخصص
(1) التسلسل الهرمي لأصناف الاستثناءات
graph TD
A[RuntimeException] --> B[BusinessException<br/>استثناء أعمال أساسي]
B --> C[ResourceNotFoundException<br/>404 غير موجود]
B --> D[InsufficientStockException<br/>422 انتهاك قاعدة أعمال]
B --> E[OrderStateException<br/>422 انتقال حالة غير صالح]
A --> F[ValidationException<br/>400 طلب غير صالح]
(1) ▶ مثال: أصناف استثناءات مخصصة
// استثناء أعمال أساسي
public class BusinessException extends RuntimeException {
private final String errorCode;
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() { return errorCode; }
}
// المورد غير موجود
public class ResourceNotFoundException extends BusinessException {
public ResourceNotFoundException(String resource, Long id) {
super("RESOURCE_NOT_FOUND",
resource + " not found with id: " + id);
}
}
// مخزون غير كافٍ
public class InsufficientStockException extends BusinessException {
public InsufficientStockException(Long productId, int available, int requested) {
super("INSUFFICIENT_STOCK",
String.format("Product %d: available=%d, requested=%d",
productId, available, requested));
}
}
// حالة طلب غير صالحة
public class OrderStateException extends BusinessException {
public OrderStateException(Long orderId, String current, String target) {
super("INVALID_ORDER_STATE",
String.format("Order %d: cannot transition from %s to %s",
orderId, current, target));
}
}
الناتج:
// التنفيذ ناجح
5. تصميم DTO لاستجابة الخطأ
(1) مبادئ تصميم ErrorResponse
(1) ▶ أمثلة: ErrorResponse وValidationErrorResponse
public record ErrorResponse(
String code,
String message,
Instant timestamp,
String traceId,
List<FieldError> details
) {
public record FieldError(
String field,
String message,
Object rejectedValue
) {}
}
// طرق المصنع الملائمة
public class ErrorResponse {
public static ErrorResponse of(String code, String message, String traceId) {
return new ErrorResponse(code, message, Instant.now(), traceId, null);
}
public static ErrorResponse withDetails(String code, String message,
String traceId, List<FieldError> details) {
return new ErrorResponse(code, message, Instant.now(), traceId, details);
}
}
الناتج:
// التنفيذ ناجح
| الحقل | النوع | الوصف |
|---|---|---|
code |
String | رمز الخطأ (قابل للقراءة آلياً) |
message |
String | رسالة الخطأ (قابلة للقراءة البشرية) |
timestamp |
Instant | وقت الحدوث |
traceId |
String | مُعرّف التتبع (مرتبط بالسجل) |
details |
List | تفاصيل الخطأ على مستوى الحقل (لأخطاء التحقق) |
6. معالجة خاصة لأخطاء التحقق
(1) ▶ مثال: معالجة MethodArgumentNotValidException
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(
MethodArgumentNotValidException ex,
HttpServletRequest request) {
String traceId = generateTraceId();
List<ErrorResponse.FieldError> details = ex.getBindingResult()
.getFieldErrors().stream()
.map(fe -> new ErrorResponse.FieldError(
fe.getField(),
fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "Invalid value",
fe.getRejectedValue()
))
.toList();
ErrorResponse error = ErrorResponse.withDetails(
"VALIDATION_ERROR",
"Input validation failed",
traceId,
details
);
log.warn("Validation failed [traceId={}]: {}", traceId, details);
return ResponseEntity.badRequest().body(error);
}
}
الناتج:
// التنفيذ ناجح
{
"code": "VALIDATION_ERROR",
"message": "Input validation failed",
"timestamp": "2024-01-15T10:00:00Z",
"traceId": "abc-123-def",
"details": [
{"field": "quantity", "message": "Quantity must be at least 1", "rejectedValue": 0},
{"field": "customerEmail", "message": "Invalid email format", "rejectedValue": "abc"}
]
}
7. سجلات الاستثناءات ومُعرّفات التتبع
(1) ▶ مثال: معالج استثناءات كامل مع مُعرّف تتبع
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(
ResourceNotFoundException ex,
HttpServletRequest request) {
String traceId = generateTraceId();
log.warn("Resource not found [traceId={}]: {}", traceId, ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage(), traceId));
}
@ExceptionHandler(InsufficientStockException.class)
public ResponseEntity<ErrorResponse> handleInsufficientStock(
InsufficientStockException ex,
HttpServletRequest request) {
String traceId = generateTraceId();
log.warn("Insufficient stock [traceId={}]: {}", traceId, ex.getMessage());
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage(), traceId));
}
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusiness(
BusinessException ex,
HttpServletRequest request) {
String traceId = generateTraceId();
log.warn("Business error [traceId={}]: {}", traceId, ex.getMessage());
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY)
.body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage(), traceId));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneral(
Exception ex,
HttpServletRequest request) {
String traceId = generateTraceId();
log.error("Unexpected error [traceId={}]", traceId, ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ErrorResponse.of("INTERNAL_ERROR",
"An unexpected error occurred. TraceId: " + traceId, traceId));
}
private String generateTraceId() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
}
الناتج:
// التنفيذ ناجح
| نوع الخطأ | رمز حالة HTTP | مستوى السجل | الوصف |
|---|---|---|---|
ResourceNotFoundException |
404 | WARN | المورد غير موجود؛ مشكلة من العميل |
InsufficientStockException |
409 | WARN | تعارض أعمال |
BusinessException |
422 | WARN | انتهاك قاعدة أعمال |
MethodArgumentNotValidException |
400 | WARN | فشل التحقق من الإدخال |
Exception |
500 | ERROR | خطأ غير متوقع؛ يحتاج إلى تحقيق |
8. مثال شامل: نظام معالجة الاستثناءات الكامل لـ OrderFlow
// ErrorResponse.java
package com.orderflow.exception;
import java.time.Instant;
import java.util.List;
public record ErrorResponse(
String code, String message, Instant timestamp,
String traceId, List<FieldError> details
) {
public record FieldError(String field, String message, Object rejectedValue) {}
public static ErrorResponse of(String code, String message, String traceId) {
return new ErrorResponse(code, message, Instant.now(), traceId, null);
}
public static ErrorResponse withDetails(String code, String message,
String traceId, List<FieldError> details) {
return new ErrorResponse(code, message, Instant.now(), traceId, details);
}
}
// التسلسل الهرمي لـ BusinessException
public class BusinessException extends RuntimeException {
private final String errorCode;
public BusinessException(String errorCode, String message) {
super(message); this.errorCode = errorCode;
}
public String getErrorCode() { return errorCode; }
}
public class ResourceNotFoundException extends BusinessException {
public ResourceNotFoundException(String resource, Long id) {
super("RESOURCE_NOT_FOUND", resource + " not found with id: " + id);
}
}
public class InsufficientStockException extends BusinessException {
public InsufficientStockException(Long productId, int avail, int req) {
super("INSUFFICIENT_STOCK",
"Product " + productId + ": available=" + avail + ", requested=" + req);
}
}
// GlobalExceptionHandler.java
package com.orderflow.exception;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
String tid = tid();
log.warn("[{}] {}", tid, ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage(), tid));
}
@ExceptionHandler(InsufficientStockException.class)
public ResponseEntity<ErrorResponse> handleStock(InsufficientStockException ex) {
String tid = tid();
log.warn("[{}] {}", tid, ex.getMessage());
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage(), tid));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
String tid = tid();
List<ErrorResponse.FieldError> details = ex.getBindingResult()
.getFieldErrors().stream()
.map(f -> new ErrorResponse.FieldError(f.getField(),
f.getDefaultMessage() != null ? f.getDefaultMessage() : "", f.getRejectedValue()))
.toList();
log.warn("[{}] Validation failed: {}", tid, details);
return ResponseEntity.badRequest()
.body(ErrorResponse.withDetails("VALIDATION_ERROR", "Validation failed", tid, details));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
String tid = tid();
log.error("[{}] Unexpected error", tid, ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ErrorResponse.of("INTERNAL_ERROR", "Unexpected error. Ref: " + tid, tid));
}
private String tid() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
}
❓ أسئلة شائعة
📖 ملخص
@RestControllerAdvice+@ExceptionHandlerيعالجان جميع استثناءات المتحكم بشكل موحد- التسلسل الهرمي للاستثناءات المخصصة:
BusinessExceptionكصنف أساسي، مع أصناف فرعية تميز سيناريوهات الأعمال المختلفة ErrorResponseيحتوي على خمسة حقول:code،message،timestamp،traceId، وdetails- معالجة خاصة لأخطاء التحقق: استخراج تفاصيل الخطأ على مستوى الحقل إلى مصفوفة
details - استخدم WARN وERROR للتمييز بين استثناءات الأعمال واستثناءات النظام في سجل الاستثناءات
- traceId يربط الاستجابات بالسجلات، مما يسهل استكشاف المشاكل
📝 تمارين
-
تمرين أساسي (الصعوبة: ⭐): نفّذ
GlobalExceptionHandlerلـOrderFlowلمعالجةResourceNotFoundExceptionوBusinessException، وأرجع تنسيقErrorResponseالمعياري. -
تمرين متقدم (الصعوبة: ⭐⭐): أضف معالجاً لـ
MethodArgumentNotValidExceptionواستخرج تفاصيل الخطأ على مستوى الحقل. نفّذ توليدtraceIdواستخدمه في السجل. -
تحدٍ (الصعوبة: ⭐⭐⭐): أنشئ Servlet Filter يُولّد traceID عند نقطة دخول الطلب ويضعه في MDC، بحيث تتضمن جميع السجلات تلقائياً traceID. يقرأ GlobalExceptionHandler traceID من MDC لتمكين تتبع السجلات من النهاية إلى النهاية.



