درس اليوم رح نتطرق على كيف نعمل Error Handling بشكل صحيح. بنبني آلية مركزية تتحكم بشكل كل الـ Responses إن كانت ناجحة أو فيها خطأ.
لما يحصل Error بالتطبيق الآن، Spring Boot بيرجع Response طويل ومعقد فيه تفاصيل داخلية عن الكود. هالشي:
الحل هو بناء Global Exception Handler يضبط كل الـ Responses.
أول خطوة هي بناء كلاس موحد لكل الـ Responses:
public class GlobalResponse<T> {
public static final String SUCCESS = "success";
public static final String ERROR = "error";
private final String status;
private final T data;
private final List<ErrorItem> errors;
// Constructor for successful responses
public GlobalResponse(T data
اشترك في النشرة البريدية
دروس جديدة، مقالات، وأدوات مباشرة لبريدك.
T هنا هو Generic - فكر فيه كـ Variable بيحمل Type. ما بنعرف شو هو مسبقاً، ممكن يكون Employee أو ArrayList<Employee> أو أي شيErrorItem هو Record بسيط:public record ErrorItem(String message) {}الـ Record في Java هو نفس الكلاس بس الفرق أنو Immutable - ما فيك تغير قيمته بعد ما تنشئه.
دائماً الـ Response للـ Client رح يكون بنفس الشكل:
{
"status": "success",
"data": { ... },
"errors": null
}{
"status": "error",
"data": null,
"errors": [
{ "message": "Employee with ID ... not found" }
]
}هاد بيخلي الـ Client يتوقع شو رح يوصله في كل الحالات.
@ControllerAdvice هي الـ Annotation اللي بتعرّف لـ Spring Boot إنو هذا الكلاس مخصص لمعالجة الأخطاء:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NoResourceFoundException.class)
public ResponseEntity<GlobalResponse<?>> handleNoResourceFound(
NoResourceFoundException exception) {
var errors = List.of(new ErrorItem("Resource is not found"));
return new ResponseEntity<>(new GlobalResponse<>(errors), HttpStatus.NOT_FOUND);
}
}@ExceptionHandler - بتحدد نوعية الـ Exception اللي هاد الـ Method يعالجهابدل ما نعمل Return لـ ResponseEntity فاضي عند عدم الإيجاد، بننشئ Custom Exception:
public class CustomResponseException extends RuntimeException {
private final int code;
private final String message;
public static CustomResponseException resourceNotFound(String message) {
return new CustomResponseException(404, message);
}
// constructor...
}وبعدين بالـ Controller، بنستخدم orElseThrow:
@GetMapping("/{employeeId}")
public ResponseEntity<GlobalResponse<Employee>> findOne(@PathVariable UUID employeeId) {
Employee employee = employees.stream()
.filter(emp -> emp.getId().equals(employeeId))
.findFirst()
.orElseThrow(() -> CustomResponseException.resourceNotFound(
"Employee with ID " + employeeId + " not found"
));
return new ResponseEntity<>(new GlobalResponse<>(employee), HttpStatus.OK);
}وبالـ @ControllerAdvice نضيف Handler للـ Custom Exception:
@ExceptionHandler(CustomResponseException.class)
public ResponseEntity<GlobalResponse<?>> handleCustomException(
CustomResponseException exception) {
var errors = List.of(new ErrorItem(exception.getMessage()));
HttpStatus status = HttpStatus.resolve(exception.getCode());
return new ResponseEntity<>(new GlobalResponse<>(errors), status);
}هيك الـ Status Code بيصير Dynamic على حسب الكود اللي جوا الـ Exception.
لما الـ @Valid يرفض الـ Request، Spring بيرمي MethodArgumentNotValidException. بنضيف Handler له:
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<GlobalResponse<?>> handleValidationException(
MethodArgumentNotValidException exception) {
var errors = exception.getFieldErrors().stream()
.map(fieldError -> new ErrorItem(fieldError.getDefaultMessage()))
.toList();
return new ResponseEntity<>(new GlobalResponse<>(errors), HttpStatus.BAD_REQUEST);
}هيك إذا الـ Client بعث Request بدون firstName وlastName:
{
"status": "error",
"data": null,
"errors": [
{ "message": "First name is required" },
{ "message": "Last name is required" }
]
}لازم كمان نعدل الـ Controller لحتى يستخدم GlobalResponse حتى للـ Success Responses:
@GetMapping
public ResponseEntity<GlobalResponse<ArrayList<Employee>>> findAll() {
return new ResponseEntity<>(new GlobalResponse<>(employees), HttpStatus.OK);
}
@PostMapping
public ResponseEntity<GlobalResponse<Employee>> create(
@Valid @RequestBody Employee employee) {
employee.setId(UUID.randomUUID());
employees.add(employee);
return new ResponseEntity<>(new GlobalResponse<>(employee), HttpStatus.CREATED);
}هيك دائماً الـ Response بنفس الشكل سواء كان نجاح أو فشل:
| الحالة | status | data | errors |
|---|---|---|---|
| نجاح | "success" | البيانات المطلوبة | null |
| خطأ | "error" | null | Array of messages |
بالدرس الجاي رح ننتقل على الـ Service Layer ونشوف شو وظيفتها وكيف الـ Controller يحكي معها.