اختبار: تقنيات متقدمة
اختبر معرفتك حول WebFlux، Redis، RabbitMQ، الجدولة وWebSockets.
30 سؤالًا - انقر على كل سؤال لكشف الإجابة.
الأسئلة 1-10: البرمجة التفاعلية (WebFlux)
السؤال 1: ما الفرق بين Spring MVC وWebFlux؟
الإجابة:
| Spring MVC | WebFlux |
|---|---|
| حجب | غير حجب |
| 1 خيط/طلب | حلقة الأحداث |
| Servlet API | Reactive Streams |
| Tomcat, Jetty | Netty |
| قابلية التوسع العمودية | قابلية التوسع الأفقية |
السؤال 2: ما الفرق بين Mono وFlux؟
الإجابة:
Mono: 0 أو 1 عنصرFlux: 0 إلى N عناصر
Mono<User> user = userRepository.findById(1L);
Flux<User> users = userRepository.findAll();
السؤال 3: ما هي المشغلين الشائعة لـ Mono/Flux؟
الإجابة:
| المشغل | الوصف |
|---|---|
map | تحويل متزامن |
flatMap | تحويل غير متزامن |
filter | تصفية |
zip | دمج |
onErrorReturn | استرجاع عند الخطأ |
switchIfEmpty | استرجاع عند الفراغ |
timeout | حد زمني |
retry | إعادة المحاولة |
السؤال 4: كيف تكتب وحدة تحكم تفاعلية؟
الإجابة:
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserRepository repository;
@GetMapping
public Flux<User> getAll() {
return repository.findAll();
}
@GetMapping("/{id}")
public Mono<ResponseEntity<User>> getById(@PathVariable Long id) {
return repository.findById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<User> create(@RequestBody User user) {
return repository.save(user);
}
}
السؤال 5: ما هو R2DBC؟
الإجابة: Reactive Relational Database Connectivity - الوصول غير الحاجب إلى قواعد البيانات العلائقية.
public interface UserRepository extends ReactiveCrudRepository<User, Long> {
Flux<User> findByEmail(String email);
}
يستبدل JPA/JDBC الحاجب في التطبيقات التفاعلية.
السؤال 6: كيف تستخدم WebClient؟
الإجابة:
@Service
public class ExternalApiService {
private final WebClient webClient;
public Mono<Data> fetchData(String id) {
return webClient.get()
.uri("/data/{id}", id)
.retrieve()
.bodyToMono(Data.class)
.timeout(Duration.ofSeconds(5))
.retry(3);
}
}
السؤال 7: لماذا لا يجب أبدًا استدعاء block() في الإنتاج؟
الإجابة: block() يحجب الخيط، مما يلغي فوائد التفاعلية:
- يحجب حلقة الأحداث
- يقلل من الإنتاجية
- يمكن أن يسبب في حالات توقف تام
يستخدم فقط في الاختبارات.
السؤال 8: كيف تختبر الكود التفاعلي؟
الإجابة:
@Test
void shouldFindUser() {
Mono<User> userMono = userService.findById(1L);
StepVerifier.create(userMono)
.assertNext(user -> {
assertThat(user.getName()).isEqualTo("John");
})
.verifyComplete();
}
@Test
void shouldHandleError() {
Mono<User> userMono = userService.findById(999L);
StepVerifier.create(userMono)
.expectError(NotFoundException.class)
.verify();
}
السؤال 9: متى تستخدم WebFlux؟
الإجابة:
- التزامن العالي (آلاف الاتصالات)
- تدفق البيانات
- التطبيقات في الوقت الحقيقي
- التكاملات مع الكثير من الاستدعاءات الخارجية
تجنب إذا: الفريق غير مدرب، المكتبات الحاجبة، CRUD بسيط.
السؤال 10: كيف تدير الأخطاء في التفاعلية؟
الإجابة:
return userRepository.findById(id)
.switchIfEmpty(Mono.error(new NotFoundException("User not found")))
.onErrorReturn(new User("Fallback"))
.onErrorResume(e -> {
log.error("Error: {}", e.getMessage());
return Mono.empty();
});
الأسئلة 11-17: التخزين المؤقت مع Redis
السؤال 11: كيف تقوم بتكوين Spring Cache مع Redis؟
الإجابة:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
@Configuration
@EnableCaching
public class CacheConfig { }
السؤال 12: كيف تستخدم @Cacheable؟
الإجابة:
@Cacheable(value = "users", key = "#id")
public User findById(Long id) {
log.info("Fetching from DB");
return userRepository.findById(id).orElseThrow();
}
// أول استدعاء: قاعدة البيانات
// الاستدعاءات التالية: التخزين المؤقت
السؤال 13: ما الفرق بين @Cacheable, @CachePut و@CacheEvict؟
الإجابة:
| التعليق التوضيحي | السلوك |
|---|---|
@Cacheable | يقرأ من التخزين المؤقت أو ينفذ ويخزن |
@CachePut | ينفذ دائمًا ويحدث التخزين المؤقت |
@CacheEvict | يحذف من التخزين المؤقت |
@CachePut(value = "users", key = "#user.id")
public User update(User user) { }
@CacheEvict(value = "users", key = "#id")
public void delete(Long id) { }
@CacheEvict(value = "users", allEntries = true)
public void clearCache() { }
السؤال 14: كيف تقوم بتكوين TTL للتخزين المؤقت؟
الإجابة:
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
السؤال 15: كيف تستخدم RedisTemplate مباشرة؟
الإجابة:
@Service
public class SessionService {
private final RedisTemplate<String, Object> redisTemplate;
public void saveSession(String token, UserSession session) {
redisTemplate.opsForValue().set(
"session:" + token,
session,
Duration.ofHours(1)
);
}
public UserSession getSession(String token) {
return (UserSession) redisTemplate.opsForValue()
.get("session:" + token);
}
}
السؤال 16: كيف تدير عدم توفر Redis؟
الإجابة:
@Configuration
public class CacheConfig extends CachingConfigurerSupport {
@Override
public CacheErrorHandler errorHandler() {
return new SimpleCacheErrorHandler() {
@Override
public void handleCacheGetError(RuntimeException ex, Cache cache, Object key) {
log.warn("Cache error: {}", ex.getMessage());
// الاستمرار بدون التخزين المؤقت
}
};
}
}
السؤال 17: متى تقوم بالتخزين المؤقت؟
الإجابة:
- البيانات التي تقرأ بشكل متكرر وتعدل نادرًا
- الحسابات المكلفة
- استدعاءات API الخارجية
- بيانات مرجعية (الدول، الفئات)
تجنب: البيانات في الوقت الحقيقي، البيانات الشخصية الحساسة.
الأسئلة 18-23: المراسلة مع RabbitMQ
السؤال 18: كيف تقوم بتكوين RabbitMQ مع Spring؟
الإجابة:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
السؤال 19: ما هي أنواع التبادل في RabbitMQ؟
الإجابة:
| النوع | التوجيه |
|---|---|
| Direct | مفتاح التوجيه الدقيق |
| Fanout | بث لجميع الطوابير |
| Topic | مطابقة النمط (*.error, order.#) |
| Headers | بناءً على الرؤوس |
السؤال 20: كيف ترسل رسالة؟
الإجابة:
@Service
public class OrderProducer {
private final RabbitTemplate rabbitTemplate;
public void sendOrderEvent(OrderEvent event) {
rabbitTemplate.convertAndSend(
"order.exchange", // exchange
"order.created", // routing key
event // message
);
}
}
السؤال 21: كيف تستقبل رسالة؟
الإجابة:
@Component
public class OrderListener {
@RabbitListener(queues = "order.queue")
public void handleOrder(OrderEvent event) {
log.info("Received: {}", event);
processOrder(event);
}
}
السؤال 22: ما هي Dead Letter Queue؟
الإجابة: طابور يستقبل الرسائل الفاشلة (المرفوضة، المنتهية، الحد الأقصى للمحاولات):
@Bean
public Queue orderQueue() {
return QueueBuilder.durable("order.queue")
.withArgument("x-dead-letter-exchange", "dlx.exchange")
.withArgument("x-dead-letter-routing-key", "dlq")
.build();
}
يسمح بتحليل وإعادة معالجة الأخطاء.
السؤال 23: كيف تضمن تسليم الرسائل؟
الإجابة:
- Publisher confirms: تأكيد التبادل
- Consumer acks: تأكيد المعالجة
- Persistent messages: البقاء بعد إعادة التشغيل
- Durable queues: الطوابير الدائمة
@RabbitListener(queues = "order.queue", ackMode = "MANUAL")
public void handle(OrderEvent event, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
try {
process(event);
channel.basicAck(tag, false);
} catch (Exception e) {
channel.basicNack(tag, false, false);
}
}
الأسئلة 24-27: المهام المجدولة
السؤال 24: كيف تقوم بتفعيل الجدولة؟
الإجابة:
@SpringBootApplication
@EnableScheduling
public class Application { }
السؤال 25: ما هي خيارات @Scheduled؟
الإجابة:
// كل 5 ثواني
@Scheduled(fixedRate = 5000)
// 5 ثواني بعد الانتهاء
@Scheduled(fixedDelay = 5000)
// تعبير Cron
@Scheduled(cron = "0 0 6 * * *") // كل يوم في الساعة 6
// مع تأخير ابتدائي
@Scheduled(fixedRate = 5000, initialDelay = 10000)
السؤال 26: كيف تكتب تعبير Cron؟
الإجابة:
┌───────────── الثانية (0-59)
│ ┌───────────── الدقيقة (0-59)
│ │ ┌───────────── الساعة (0-23)
│ │ │ ┌───────────── اليوم من الشهر (1-31)
│ │ │ │ ┌───────────── الشهر (1-12)
│ │ │ │ │ ┌───────────── اليوم من الأسبوع (0-7)
* * * * * *
أمثلة:
0 0 * * * *: كل ساعة0 0 9 * * MON-FRI: الساعة 9 صباحًا في أيام الأسبوع0 */15 * * * *: كل 15 دقيقة
السؤال 27: كيف تنفذ طريقة بشكل غير متزامن؟
الإجابة:
@EnableAsync
@Configuration
public class AsyncConfig { }
@Service
public class EmailService {
@Async
public CompletableFuture<Boolean> sendEmail(String to) {
// يتم التنفيذ في خيط آخر
return CompletableFuture.completedFuture(true);
}
}
الأسئلة 28-30: WebSockets
السؤال 28: كيف تقوم بتكوين WebSocket مع STOMP؟
الإجابة:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS();
}
}
السؤال 29: كيف ترسل وتستقبل رسائل WebSocket؟
الإجابة:
@Controller
public class ChatController {
@MessageMapping("/chat.send")
@SendTo("/topic/messages")
public ChatMessage send(ChatMessage message) {
return message;
}
}
// إرسال برمجي
@Service
public class NotificationService {
private final SimpMessagingTemplate template;
public void notifyUser(String userId, Notification notif) {
template.convertAndSendToUser(userId, "/queue/notifications", notif);
}
}
السؤال 30: متى تستخدم WebSocket مقابل REST؟
الإجابة:
| WebSocket | REST |
|---|---|
| اتصال ثنائي الاتجاه | طلب-استجابة |
| في الوقت الحقيقي | بيانات ثابتة |
| الدردشة، الإشعارات | CRUD، API |
| اتصال دائم | اتصالات قصيرة |
| الألعاب، التداول | التطبيقات التقليدية |
النتيجة
| النتيجة | المستوى |
|---|---|
| 27-30 | ممتاز - تقنيات متقدمة متقنة |
| 21-26 | جيد - بعض المراجعات |
| 15-20 | متوسط - أعد قراءة الفصول |
| أقل من 15 | بحاجة إلى مراجعة |