404 Not Found

404 Not Found


nginx

معالجة الاستثناءات العامة

معالجة الاستثناءات العامة تُوحّد تنسيق استجابات أخطاء API—لم يعد العملاء يضطرون للتعامل مع تشكيلة واسعة من تنسيقات الأخطاء، مما يجعل التصحيح والدمج أكثر كفاءة.

1. ما ستتعلمه


2. قصة حقيقية لمطوّر واجهة أمامية

(1) نقطة الألم: تنسيقات استجابة الخطأ غير متسقة

Bob هو مطوّر واجهة أمامية واجه مشاكل أثناء الدمج مع OrderFlow API: بعض نقاط النهاية تُرجع 404 بنص عادي "Not Found"، وبعضها يُرجع 500 بصفحة خطأ HTML، وبعضها يُرجع استثناء أعمال {"error": "xxx"}، وآخرون يُرجعون {"message": "xxx"}. كان عليه كتابة منطق معالجة أخطاء مختلف لكل نقطة نهاية، مما أدى إلى كتل try-catch متناثرة في كوده—وكثيراً ما كان يغفل عنها، مما يتسبب في شاشة بيضاء.

(2) حل @RestControllerAdvice

معالج استثناءات موحد يضمن أن جميع استجابات الأخطاء تتبع تنسيقاً متسقاً:

JAVA
@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) عملية توجيه معالجة الاستثناءات

100%
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) ▶ مثال: معالج استثناءات عام أساسي

JAVA
@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);
    }
}

الناتج:

TEXT
// التنفيذ ناجح

4. نظام استثناءات الأعمال المخصص

(1) التسلسل الهرمي لأصناف الاستثناءات

100%
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) ▶ مثال: أصناف استثناءات مخصصة

JAVA
// استثناء أعمال أساسي
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));
    }
}

الناتج:

TEXT
// التنفيذ ناجح

5. تصميم DTO لاستجابة الخطأ

(1) مبادئ تصميم ErrorResponse

(1) ▶ أمثلة: ErrorResponse وValidationErrorResponse

JAVA
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);
    }
}

الناتج:

TEXT
// التنفيذ ناجح
الحقل النوع الوصف
code String رمز الخطأ (قابل للقراءة آلياً)
message String رسالة الخطأ (قابلة للقراءة البشرية)
timestamp Instant وقت الحدوث
traceId String مُعرّف التتبع (مرتبط بالسجل)
details List تفاصيل الخطأ على مستوى الحقل (لأخطاء التحقق)

6. معالجة خاصة لأخطاء التحقق

(1) ▶ مثال: معالجة MethodArgumentNotValidException

JAVA
@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);
    }
}

الناتج:

TEXT
// التنفيذ ناجح
💻 الناتج:

JSON
{
  "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) ▶ مثال: معالج استثناءات كامل مع مُعرّف تتبع

JAVA
@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);
    }
}

الناتج:

TEXT
// التنفيذ ناجح
نوع الخطأ رمز حالة HTTP مستوى السجل الوصف
ResourceNotFoundException 404 WARN المورد غير موجود؛ مشكلة من العميل
InsufficientStockException 409 WARN تعارض أعمال
BusinessException 422 WARN انتهاك قاعدة أعمال
MethodArgumentNotValidException 400 WARN فشل التحقق من الإدخال
Exception 500 ERROR خطأ غير متوقع؛ يحتاج إلى تحقيق

8. مثال شامل: نظام معالجة الاستثناءات الكامل لـ OrderFlow

JAVA
// 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);
    }
}

❓ أسئلة شائعة

س ما الفرق بين @ControllerAdvice و@RestControllerAdvice؟
ج @RestControllerAdvice = @ControllerAdvice + @ResponseBody. في مشاريع REST API، استخدم دائماً @RestControllerAdvice؛ قيم إرجاع الطرق تُسلسل تلقائياً إلى JSON.
س ما هو ترتيب المطابقة لـ @ExceptionHandler؟
ج Spring يختار أنسب تطابق لنوع الاستثناء. إذا تم تسجيل معالجات لكل من BusinessException وException، فإن معالج BusinessException له الأسبقية عند رمي BusinessException.
س كيف يمكن لتعدد أصناف @RestControllerAdvice التعايش؟
ج يمكنك استخدام @Order للتحكم في الأولوية. @Order(Ordered.HIGHEST_PRECEDENCE) له الأسبقية في المطابقة. أصناف Advice مختلفة يمكنها معالجة أنواع مختلفة من الاستثناءات.
س هل يجب أن تُرجع بيئة الإنتاج تتبع استثناء؟
ج لا، لا يجب. بيئة الإنتاج يجب أن تُرجع رموز أخطاء ورسائل عامة فقط؛ لا يجب أن تكشف عن التنفيذ الداخلي. معلومات تتبع الاستثناء تُسجل فقط في السجلات. للاستثناءات غير المتوقعة، أرجع traceId حتى تتمكن العمليات من تحديد المشكلة عبر السجلات.
س كيف أُعالج استثناءات Spring Security؟
ج استثناءات Spring Security (مثل AccessDeniedException) تُرمى ضمن سلسلة الفلتر ولا تمر عبر @RestControllerAdvice. تحتاج إلى تخصيص AuthenticationEntryPoint وAccessDeniedHandler.
س ما العلاقة بين traceId وMDC؟
ج traceId يُستخدم لربط السجلات بالعميل في الاستجابة، بينما MDC يُستخدم ضمن إطار التسجيل لربط جميع السجلات من نفس الطلب. يُوصى باستخدام نفس القيمة لكليهما؛ اضبط MDC.put("traceId", id) في الفلتر.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (الصعوبة: ⭐): نفّذ GlobalExceptionHandler لـ OrderFlow لمعالجة ResourceNotFoundException وBusinessException، وأرجع تنسيق ErrorResponse المعياري.

  2. تمرين متقدم (الصعوبة: ⭐⭐): أضف معالجاً لـ MethodArgumentNotValidException واستخرج تفاصيل الخطأ على مستوى الحقل. نفّذ توليد traceId واستخدمه في السجل.

  3. تحدٍ (الصعوبة: ⭐⭐⭐): أنشئ Servlet Filter يُولّد traceID عند نقطة دخول الطلب ويضعه في MDC، بحيث تتضمن جميع السجلات تلقائياً traceID. يقرأ GlobalExceptionHandler traceID من MDC لتمكين تتبع السجلات من النهاية إلى النهاية.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%