انتقل إلى المحتوى الرئيسي

الفصل 1: إدارة الاستثناءات

مقدمة​

الاستثناءات هي آلية Java للإبلاغ عن الأخطاء. استخدامها بشكل جيد ضروري لإنشاء تطبيقات قوية وقابلة للصيانة.


🎯 أهداف الفصل​

في نهاية هذا الفصل، ستعرف:

  1. ✅ التمييز بين Checked و Unchecked exceptions
  2. ✅ إنشاء استثناءات مخصصة
  3. ✅ تنظيم تسلسل هرمي للاستثناءات
  4. ✅ تطبيق أفضل الممارسات

1. تذكير: التسلسل الهرمي للاستثناءات في Java​

Throwable
├── Error (لا تلتقط)
│ ├── OutOfMemoryError
│ └── StackOverflowError
└── Exception
├── Checked Exceptions (يجب أن تُعلن)
│ ├── IOException
│ └── SQLException
└── RuntimeException (Unchecked)
├── NullPointerException
├── IllegalArgumentException
└── استثناءاتك المخصصة

2. Checked vs Unchecked​

Checked Exceptions​

// يجب أن تُعلن مع throws أو تُلتقط
public void readFile(String path) throws IOException {
Files.readString(Path.of(path)); // IOException ممكن
}

// أو
public void readFile(String path) {
try {
Files.readString(Path.of(path));
} catch (IOException e) {
// إدارة الخطأ
}
}

متى تستخدم؟ أخطاء قابلة للاسترداد (ملف مفقود، شبكة).

Unchecked Exceptions (RuntimeException)​

// لا حاجة للإعلان
public User findById(Long id) {
if (id == null) {
throw new IllegalArgumentException("ID cannot be null");
}
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}

متى تستخدم؟ أخطاء البرمجة، انتهاكات العقد.

في Spring Boot: فضل Unchecked​

// ❌ ثقيل الاستخدام
public User findById(Long id) throws UserNotFoundException {
// ...
}

// ✅ أكثر سلاسة
public User findById(Long id) {
// يرفع RuntimeException إذا لم يُعثر عليه
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}

3. إنشاء استثناءات مخصصة​

استثناء بسيط​

public class UserNotFoundException extends RuntimeException {

public UserNotFoundException(Long id) {
super("User not found with ID: " + id);
}

public UserNotFoundException(String message) {
super(message);
}
}

استثناء مع بيانات إضافية​

@Getter
public class ResourceNotFoundException extends RuntimeException {

private final String resourceType;
private final Object resourceId;

public ResourceNotFoundException(String resourceType, Object resourceId) {
super(String.format("%s not found with ID: %s", resourceType, resourceId));
this.resourceType = resourceType;
this.resourceId = resourceId;
}
}

// استخدام
throw new ResourceNotFoundException("User", 123L);
throw new ResourceNotFoundException("Product", "SKU-001");

استثناء مع رمز خطأ​

@Getter
public class BusinessException extends RuntimeException {

private final String errorCode;
private final Map<String, Object> details;

public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
this.details = new HashMap<>();
}

public BusinessException(String errorCode, String message, Map<String, Object> details) {
super(message);
this.errorCode = errorCode;
this.details = details;
}
}

// استخدام
throw new BusinessException(
"INSUFFICIENT_BALANCE",
"Insufficient balance for withdrawal",
Map.of("requested", 500, "available", 100)
);

4. التسلسل الهرمي لاستثناءات الأعمال​

الهيكل الموصى به​

// استثناء أساسي للتطبيق
public abstract class ApplicationException extends RuntimeException {

private final String errorCode;

protected ApplicationException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}

protected ApplicationException(String errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}

public String getErrorCode() {
return errorCode;
}
}

استثناءات محددة​

// مورد غير موجود (→ 404)
public class ResourceNotFoundException extends ApplicationException {

public ResourceNotFoundException(String resourceType, Object id) {
super("RESOURCE_NOT_FOUND",
String.format("%s with ID %s not found", resourceType, id));
}
}

// فشل التحقق (→ 400)
public class ValidationException extends ApplicationException {

private final List<FieldError> errors;

public ValidationException(List<FieldError> errors) {
super("VALIDATION_FAILED", "Validation failed");
this.errors = errors;
}
}

// تعارض (→ 409)
public class ConflictException extends ApplicationException {

public ConflictException(String message) {
super("CONFLICT", message);
}
}

// غير مصرح (→ 403)
public class ForbiddenException extends ApplicationException {

public ForbiddenException(String message) {
super("FORBIDDEN", message);
}
}

// خطأ في العمل (→ 422)
public class BusinessRuleException extends ApplicationException {

public BusinessRuleException(String code, String message) {
super(code, message);
}
}

مخطط​

ApplicationException
├── ResourceNotFoundException → 404
├── ValidationException → 400
├── ConflictException → 409
├── ForbiddenException → 403
├── BusinessRuleException → 422
└── TechnicalException → 500

5. أمثلة على الاستخدام​

في خدمة​

@Service
@RequiredArgsConstructor
public class UserService {

private final UserRepository userRepository;

public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
}

public User create(UserRequest request) {
// تحقق من التفرد
if (userRepository.existsByEmail(request.getEmail())) {
throw new ConflictException("Email already in use: " + request.getEmail());
}

// إنشاء المستخدم
User user = new User();
user.setEmail(request.getEmail());
user.setName(request.getName());
return userRepository.save(user);
}

public void delete(Long id) {
User user = findById(id); // يرفع ResourceNotFoundException إذا لم يُعثر عليه

if (user.hasActiveOrders()) {
throw new BusinessRuleException(
"CANNOT_DELETE_USER_WITH_ORDERS",
"Cannot delete user with active orders"
);
}

userRepository.delete(user);
}
}

في وحدة تحكم (بدون إدارة مركزية)​

@RestController
@RequestMapping("/api/users")
public class UserController {

@GetMapping("/{id}")
public ResponseEntity<User> getById(@PathVariable Long id) {
try {
User user = userService.findById(id);
return ResponseEntity.ok(user);
} catch (ResourceNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
}

المشكلة: تكرار try-catch في كل طريقة!

الحل: @ControllerAdvice (الفصل التالي)


6. أفضل الممارسات​

✅ يجب القيام به​

// 1. تسمية واضحة
public class InsufficientStockException extends BusinessRuleException { }
public class PaymentDeclinedException extends BusinessRuleException { }

// 2. تضمين السياق
throw new ResourceNotFoundException("Product", productId);

// 3. استخدام رموز الخطأ
throw new BusinessRuleException("ORDER_LIMIT_EXCEEDED", "...");

// 4. ربط الأسباب
try {
externalService.call();
} catch (ExternalServiceException e) {
throw new TechnicalException("External service failed", e);
}

❌ يجب تجنبه​

// 1. استثناءات عامة
throw new Exception("Something went wrong"); // ❌

// 2. رسائل فارغة
throw new RuntimeException(); // ❌

// 3. Catch and ignore
try {
riskyOperation();
} catch (Exception e) {
// صامت - ❌ أبداً!
}

// 4. استثناءات لتدفق عادي
try {
user = findById(id);
} catch (UserNotFoundException e) {
user = createUser(id); // ❌ استخدم Optional بدلاً من ذلك
}

النمط: Optional vs Exception​

// لـ "قد لا يوجد"
public Optional<User> findByEmail(String email) {
return userRepository.findByEmail(email);
}

// لـ "يجب أن يوجد"
public User getById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
}

7. استثناءات Spring Boot الأصلية​

ResponseStatusException​

@GetMapping("/{id}")
public User getById(@PathVariable Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND,
"User not found: " + id
));
}

الميزة: بسيط، لا حاجة إلى @ControllerAdvice العيب: أقل مرونة، لا يوجد تنسيق مخصص

@ResponseStatus على استثناء​

@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) {
super("User not found: " + id);
}
}

الميزة: تلقائي العيب: تنسيق الاستجابة الافتراضي لـ Spring


8. ملخص​

أنواع الاستثناءات​

النوعالوراثةالاستخدام
CheckedExceptionأخطاء قابلة للاسترداد
UncheckedRuntimeExceptionأخطاء العمل، البرمجة

التسلسل الهرمي الموصى به​

ApplicationException (abstract)
├── ResourceNotFoundException (404)
├── ValidationException (400)
├── ConflictException (409)
├── BusinessRuleException (422)
└── TechnicalException (500)

قائمة التحقق​

✅ الاستثناءات ترث من RuntimeException
✅ رسائل واضحة ومفيدة
✅ رموز خطأ لـ API
✅ تضمين السياق (ID، نوع المورد)
✅ ربط الأسباب للتصحيح

🎯 الخطوة التالية​

أنت تعرف كيفية إنشاء الاستثناءات. الآن، لنتعلم كيفية إدارتها بشكل شامل باستخدام @ControllerAdvice.

➡️ الفصل 2: @ControllerAdvice


⬅️ العودة إلى الوحدة 10