اختبار: إدارة الاستثناءات والتسجيل
اختبر معرفتك بإدارة الأخطاء والتسجيل.
30 سؤالاً - انقر على كل سؤال لكشف الإجابة.
الأسئلة 1-15: إدارة الاستثناءات
السؤال 1: ما هو @ControllerAdvice؟
الإجابة: مكون يدير الاستثناءات بشكل عام لجميع المتحكمات:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity.status(404).body(new ErrorResponse(ex.getMessage()));
}
}
السؤال 2: ما الفرق بين @ControllerAdvice و@RestControllerAdvice؟
الإجابة:
@ControllerAdvice: يمكن أن يعيد عرض أو كائنات@RestControllerAdvice: يعادل@ControllerAdvice+@ResponseBody
بالنسبة لـ API REST، استخدم @RestControllerAdvice.
السؤال 3: كيف تنشئ استثناء مخصص؟
الإجابة:
public class ResourceNotFoundException extends RuntimeException {
private final String resourceName;
private final Object resourceId;
public ResourceNotFoundException(String resourceName, Object resourceId) {
super(String.format("%s not found with id: %s", resourceName, resourceId));
this.resourceName = resourceName;
this.resourceId = resourceId;
}
// Getters
}
السؤال 4: كيف تربط استثناء بكود HTTP؟
الإجابة:
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(ResourceNotFoundException ex) {
return new ErrorResponse(ex.getMessage());
}
// أو مع ResponseEntity
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(ex.getMessage()));
}
السؤال 5: كيف تدير أخطاء التحقق؟
الإجابة:
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidation(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return ResponseEntity.badRequest().body(errors);
}
السؤال 6: ما هو ProblemDetail (RFC 7807)؟
الإجابة: تنسيق قياسي لردود خطأ HTTP:
@ExceptionHandler(ResourceNotFoundException.class)
public ProblemDetail handleNotFound(ResourceNotFoundException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND, ex.getMessage());
problem.setTitle("Resource Not Found");
problem.setProperty("resourceId", ex.getResourceId());
return problem;
}
نتيجة JSON:
{
"type": "about:blank",
"title": "Resource Not Found",
"status": 404,
"detail": "User not found with id: 123",
"resourceId": 123
}
السؤال 7: كيف تدير جميع الاستثناءات غير المدارة؟
الإجابة:
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
log.error("Unexpected error", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("An unexpected error occurred"));
}
السؤال 8: كيف تضيف معلومات إلى رد الخطأ؟
الإجابة:
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String path;
private Map<String, Object> details;
// Constructeurs, getters
}
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusiness(
BusinessException ex, HttpServletRequest request) {
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.BAD_REQUEST.value())
.error("Business Rule Violation")
.message(ex.getMessage())
.path(request.getRequestURI())
.build();
return ResponseEntity.badRequest().body(error);
}
السؤال 9: كيف تستهدف @ControllerAdvice لمتحكمات معينة؟
الإجابة:
// بواسطة التعليق التوضيحي
@ControllerAdvice(annotations = RestController.class)
// بواسطة الحزمة
@ControllerAdvice(basePackages = "com.example.api")
// بواسطة الفئة
@ControllerAdvice(assignableTypes = {UserController.class, OrderController.class})
السؤال 10: كيف تدير الاستثناءات في الفلاتر؟
الإجابة: الفلاتر تأتي قبل المتحكمات، لذا @ControllerAdvice لا ينطبق:
public class ExceptionHandlerFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain) {
try {
chain.doFilter(request, response);
} catch (JwtException ex) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"Invalid token\"}");
}
}
}
السؤال 11: كيف تدير الأخطاء غير المتزامنة؟
الإجابة:
@ControllerAdvice
public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
log.error("Async error in {}: {}", method.getName(), ex.getMessage());
}
}
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncExceptionHandler();
}
}
السؤال 12: كيف تثير خطأ 404 لمورد مفقود؟
الإجابة:
@Service
public class UserService {
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND, "User not found: " + id));
}
}
// أو مع استثناء مخصص
throw new ResourceNotFoundException("User", id);
السؤال 13: كيف تعطل صفحة الخطأ الافتراضية في Spring Boot؟
الإجابة:
server:
error:
whitelabel:
enabled: false
# أو التخصيص
include-message: always
include-binding-errors: always
include-stacktrace: never
include-exception: false
السؤال 14: كيف تدير الأخطاء 404 بشكل عام؟
الإجابة:
spring:
mvc:
throw-exception-if-no-handler-found: true
web:
resources:
add-mappings: false
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(NoHandlerFoundException ex) {
return ResponseEntity.status(404)
.body(new ErrorResponse("Endpoint not found: " + ex.getRequestURL()));
}
السؤال 15: ما هي أفضل الممارسات للاستثناءات؟
الإجابة:
- إنشاء استثناءات مخصصة للأعمال
- عدم كشف التفاصيل التقنية للعميل
- تسجيل الاستثناءات على الخادم
- استخدام أكواد HTTP المناسبة
- تنسيق رد متسق
- تضمين معرف تتبع للتصحيح
الأسئلة 16-30: التسجيل
السؤال 16: ما هو إطار العمل المستخدم للتسجيل في Spring Boot افتراضيًا؟
الإجابة: SLF4J كواجهة مع Logback كتطبيق.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger log = LoggerFactory.getLogger(MyClass.class);
أو مع Lombok: @Slf4j
السؤال 17: ما هي مستويات السجل؟
الإجابة: من الأقل إلى الأكثر تفصيلاً:
- ERROR: أخطاء حرجة
- WARN: مواقف قد تكون مشكلة
- INFO: معلومات عامة
- DEBUG: تفاصيل للتصحيح
- TRACE: تفاصيل دقيقة جدًا
log.error("Payment failed: {}", ex.getMessage());
log.warn("Slow query: {} ms", duration);
log.info("User {} logged in", username);
log.debug("Processing item: {}", item);
log.trace("Entering method with args: {}", Arrays.toString(args));
السؤال 18: كيف تقوم بتكوين مستويات السجل؟
الإجابة:
logging:
level:
root: INFO
com.example: DEBUG
org.springframework.security: TRACE
org.hibernate.SQL: DEBUG
السؤال 19: لماذا تستخدم النماذج بدلاً من الدمج؟
الإجابة: الأداء - يتم بناء الرسالة فقط إذا كان المستوى نشطًا:
// جيد - تقييم كسول
log.debug("Processing user: {}", user);
// سيء - يتم تقييمه دائمًا
log.debug("Processing user: " + user.toString());
السؤال 20: كيف تقوم بتكوين Logback باستخدام ملف XML؟
الإجابة: إنشاء src/main/resources/logback-spring.xml:
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
السؤال 21: كيف تكتب السجلات بصيغة JSON؟
الإجابة: باستخدام Logstash encoder:
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
السؤال 22: ما هو MDC (Mapped Diagnostic Context)؟
الإجابة: سياق محلي للخيوط لإضافة معلومات إلى السجلات:
// إضافة في بداية الطلب
MDC.put("requestId", UUID.randomUUID().toString());
MDC.put("userId", user.getId().toString());
// في النمط
<pattern>%d [%X{requestId}] [%X{userId}] %msg%n</pattern>
// تنظيف في النهاية
MDC.clear();
السؤال 23: كيف تنفذ فلتر MDC؟
الإجابة:
@Component
public class MdcFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain) {
try {
MDC.put("requestId", UUID.randomUUID().toString());
MDC.put("clientIp", request.getRemoteAddr());
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
السؤال 24: كيف تقوم بتكوين سجلات مختلفة لكل ملف تعريف؟
الإجابة:
<!-- logback-spring.xml -->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="FILE"/>
<appender-ref ref="JSON"/>
</root>
</springProfile>
السؤال 25: كيف تخفي البيانات الحساسة في السجلات؟
الإجابة:
// لا تسجل البيانات الحساسة
log.info("User login: {}", user.getEmail()); // ليس كلمة المرور!
// إخفاء جزئي
String maskedCard = card.substring(0, 4) + "****" + card.substring(12);
log.info("Payment with card: {}", maskedCard);
// نمط الإخفاء في logback
<pattern>%replace(%msg){'password=[^,]*','password=***'}%n</pattern>
السؤال 26: كيف تسجل الطلبات HTTP؟
الإجابة:
@Component
public class RequestLoggingFilter extends CommonsRequestLoggingFilter {
public RequestLoggingFilter() {
setIncludeQueryString(true);
setIncludePayload(true);
setMaxPayloadLength(10000);
setIncludeHeaders(false);
}
}
أو تفعيل في التكوين:
logging:
level:
org.springframework.web.filter.CommonsRequestLoggingFilter: DEBUG
السؤال 27: كيف تسجل الطلبات SQL؟
الإجابة:
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.orm.jdbc.bind: TRACE # المعلمات
# أو مع Spring
spring:
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
السؤال 28: كيف تقوم بأرشفة وضغط السجلات؟
الإجابة:
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/app-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
</appender>
السؤال 29: كيف ترسل السجلات إلى خدمة خارجية؟
الإجابة: باستخدام appenders المتخصصة:
<!-- Logstash -->
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>logstash:5000</destination>
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
<!-- أو غير متزامن للأداء -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="LOGSTASH"/>
</appender>
السؤال 30: ما هي أفضل الممارسات للتسجيل؟
الإجابة:
- استخدام المستوى المناسب للسجل
- تضمين السياق (userId، requestId)
- عدم تسجيل البيانات الحساسة
- استخدام النماذج
- تسجيل الاستثناءات مع stacktrace
- سجلات منظمة في الإنتاج (JSON)
- التدوير والأرشفة
- المركزية للخدمات المصغرة
النتيجة
| النتيجة | المستوى |
|---|---|
| 27-30 | ممتاز - إدارة الاستثناءات والتسجيل متقنة |
| 21-26 | جيد - بعض المراجعات |
| 15-20 | متوسط - أعد قراءة الفصول |
| أقل من 15 | بحاجة إلى مراجعة |