الفصل 4: Annotations الأمان، الاختبار والتقدم
1. Annotations Spring Security
@EnableWebSecurity
يُفعل إعدادات الأمان المخصصة.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.build();
}
}
@EnableMethodSecurity
يُفعل الأمان على مستوى الطرق.
@Configuration
@EnableMethodSecurity // يُفعل @PreAuthorize, @PostAuthorize, @Secured
public class MethodSecurityConfig {
}
@PreAuthorize
يتحقق من الأذونات قبل تنفيذ الطريقة.
@RestController
@RequestMapping("/api/admin")
public class AdminController {
@GetMapping("/users")
@PreAuthorize("hasRole('ADMIN')")
public List<User> getAllUsers() { ... }
@GetMapping("/stats")
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
public Stats getStats() { ... }
@DeleteMapping("/users/{id}")
@PreAuthorize("hasRole('ADMIN') and #id != principal.id")
public void deleteUser(@PathVariable Long id) { ... }
@GetMapping("/users/{id}")
@PreAuthorize("#id == principal.id or hasRole('ADMIN')")
public User getUser(@PathVariable Long id) { ... }
@PreAuthorize("isAuthenticated()")
@GetMapping("/profile")
public User getProfile() { ... }
}
@PostAuthorize
يتحقق من الأذونات بعد التنفيذ.
@GetMapping("/{id}")
@PostAuthorize("returnObject.owner == authentication.name or hasRole('ADMIN')")
public Document getDocument(@PathVariable Long id) {
return documentService.findById(id);
}
@Secured
يحدد الأدوار المطلوبة (أبسط من @PreAuthorize).
@Secured("ROLE_ADMIN")
public void adminOnly() { ... }
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
public void adminOrManager() { ... }
@RolesAllowed
معيار JSR-250، مشابه لـ @Secured.
@RolesAllowed("ADMIN")
public void adminOnly() { ... }
ملخص الأمان
| Annotation | الاستخدام |
|---|---|
@EnableWebSecurity | يُفعل إعدادات الأمان |
@EnableMethodSecurity | يُفعل الأمان على مستوى الطرق |
@PreAuthorize | التحقق قبل التنفيذ (SpEL) |
@PostAuthorize | التحقق بعد التنفيذ |
@Secured | الأدوار المطلوبة (بسيط) |
@RolesAllowed | الأدوار المطلوبة (JSR-250) |
2. Annotations الاختبار
@SpringBootTest
يُحمل السياق الكامل لـ Spring للاختبارات التكاملية.
@SpringBootTest
class UserServiceIntegrationTest {
@Autowired
private UserService userService;
@Test
void shouldCreateUser() {
User user = userService.create(new UserDto("Jean", "jean@test.com"));
assertThat(user.getId()).isNotNull();
}
}
// مع خادم ويب عشوائي
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ApiIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldReturnProducts() {
ResponseEntity<List> response = restTemplate.getForEntity("/api/products", List.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}
@WebMvcTest
يختبر فقط طبقة المتحكم (بدون قاعدة بيانات، بدون خدمات حقيقية).
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ProductService productService;
@Test
void shouldReturnProducts() throws Exception {
when(productService.findAll()).thenReturn(List.of(new Product("Laptop", 999.99)));
mockMvc.perform(get("/api/products"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].name").value("Laptop"));
}
}
@DataJpaTest
يختبر فقط طبقة JPA.
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void shouldFindByEmail() {
entityManager.persistAndFlush(new User("Jean", "jean@test.com"));
Optional<User> found = userRepository.findByEmail("jean@test.com");
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("Jean");
}
}
@MockBean
يستبدل bean من Spring بـ mock من Mockito.
@WebMvcTest(UserController.class)
class UserControllerTest {
@MockBean
private UserService userService; // Mock بدلاً من الخدمة الحقيقية
@Test
void shouldReturnUser() {
when(userService.findById(1L)).thenReturn(new User("Jean"));
// ...
}
}
@SpyBean
يغلف bean حقيقي بـ spy من Mockito (استدعاءات حقيقية باستثناء تلك المستبدلة).
@SpringBootTest
class NotificationTest {
@SpyBean
private EmailService emailService;
@Test
void shouldSendEmail() {
doNothing().when(emailService).sendEmail(anyString(), anyString());
// بقية الطرق تستدعي الكود الحقيقي
}
}
@TestConfiguration
يوفر إعدادات خاصة بالاختبارات.
@TestConfiguration
public class TestSecurityConfig {
@Bean
public SecurityFilterChain testFilterChain(HttpSecurity http) throws Exception {
return http.csrf(c -> c.disable())
.authorizeHttpRequests(a -> a.anyRequest().permitAll())
.build();
}
}
@ActiveProfiles
يُفعل ملف تعريف Spring محدد للاختبار.
@SpringBootTest
@ActiveProfiles("test")
class ServiceTest {
// يستخدم application-test.yml
}
@Sql / @SqlGroup
ينفذ سكربتات SQL قبل/بعد اختبار.
@DataJpaTest
@Sql("/test-data.sql")
class ProductRepositoryTest {
// test-data.sql يُنفذ قبل كل اختبار
}
@Sql(scripts = "/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
ملخص الاختبار
| Annotation | النطاق | الاستخدام |
|---|---|---|
@SpringBootTest | السياق الكامل | اختبارات تكاملية |
@WebMvcTest | طبقة الويب فقط | اختبارات المتحكمات |
@DataJpaTest | طبقة JPA فقط | اختبارات المستودعات |
@MockBean | bean مقلد | استبدال bean |
@SpyBean | bean مراقب | تغليف bean حقيقي |
@ActiveProfiles | ملف تعريف | اختيار إعدادات الاختبار |
@Sql | قاعدة البيانات | تحميل بيانات SQL |
3. Annotations غير متزامنة والجدولة
@EnableAsync / @Async
ينفذ طريقة بشكل غير متزامن (في خيط منفصل).
@Configuration
@EnableAsync
public class AsyncConfig {
}
@Service
public class EmailService {
@Async
public CompletableFuture<String> sendEmailAsync(String to, String body) {
// يُنفذ في خيط منفصل
// ... إرسال البريد الإلكتروني
return CompletableFuture.completedFuture("Email envoyé à " + to);
}
}
@EnableScheduling / @Scheduled
يُجدول التنفيذ الدوري لطريقة.
@Configuration
@EnableScheduling
public class SchedulingConfig {
}
@Service
public class CleanupService {
@Scheduled(fixedRate = 60000) // كل 60 ثانية
public void cleanupExpiredTokens() {
tokenRepository.deleteExpired();
}
@Scheduled(fixedDelay = 30000) // 30 ثانية بعد انتهاء التنفيذ السابق
public void processQueue() {
queueService.processNext();
}
@Scheduled(cron = "0 0 2 * * ?") // كل يوم في الساعة 2 صباحًا
public void dailyReport() {
reportService.generateDailyReport();
}
@Scheduled(cron = "0 0 * * * MON-FRI") // كل ساعة، من الاثنين إلى الجمعة
public void hourlySync() {
syncService.sync();
}
}
صيغة Cron
┌───────── الثانية (0-59)
│ ┌─────── الدقيقة (0-59)
│ │ ┌───── الساعة (0-23)
│ │ │ ┌─── اليوم من الشهر (1-31)
│ │ │ │ ┌─ الشهر (1-12)
│ │ │ │ │ ┌ اليوم من الأسبوع (0-7, MON-SUN)
│ │ │ │ │ │
* * * * * *
| التعبير | الوصف |
|---|---|
0 0 * * * * | كل ساعة |
0 0 2 * * * | كل يوم في الساعة 2 |
0 0 9-17 * * MON-FRI | كل ساعة من 9 صباحًا إلى 5 مساءً في أيام الأسبوع |
0 */15 * * * * | كل 15 دقيقة |
0 0 0 1 * * | الأول من كل شهر عند منتصف الليل |
4. Annotations التخزين المؤقت
@EnableCaching / @Cacheable / @CacheEvict / @CachePut
@Configuration
@EnableCaching
public class CacheConfig {
}
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id")
public Product findById(Long id) {
// النتيجة مخزنة مؤقتًا؛ الاستدعاءات التالية لا تصل إلى قاعدة البيانات
return productRepository.findById(id).orElseThrow();
}
@Cacheable(value = "productList", unless = "#result.size() == 0")
public List<Product> findAll() {
return productRepository.findAll();
}
@CachePut(value = "products", key = "#product.id")
public Product update(Product product) {
// يُحدث التخزين المؤقت بالنتيجة الجديدة
return productRepository.save(product);
}
@CacheEvict(value = "products", key = "#id")
public void delete(Long id) {
// يُحذف الإدخال من التخزين المؤقت
productRepository.deleteById(id);
}
@CacheEvict(value = "products", allEntries = true)
public void clearCache() {
// يُفرغ كل التخزين المؤقت "products"
}
}
5. Annotations البرمجة الموجهة للجوانب (AOP)
@Aspect / @Before / @After / @Around
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
log.info("Appel: {}", joinPoint.getSignature().getName());
}
@After("execution(* com.example.service.*.*(..))")
public void logAfter(JoinPoint joinPoint) {
log.info("Fin: {}", joinPoint.getSignature().getName());
}
@Around("@annotation(com.example.annotation.Timed)")
public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - start;
log.info("{} exécuté en {} ms", joinPoint.getSignature().getName(), duration);
return result;
}
}
6. Annotations التكوين التلقائي لـ Spring Boot
@EnableAutoConfiguration
مضمنة في @SpringBootApplication. تُفعل التكوين التلقائي بناءً على التبعيات.
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
// يُعطل التكوين التلقائي لـ DataSource
@ConditionalOn...
عائلة من Annotations للتكوين الشرطي.
| Annotation | الشرط |
|---|---|
@ConditionalOnProperty | الخاصية لها قيمة معينة |
@ConditionalOnClass | الفئة موجودة في classpath |
@ConditionalOnMissingClass | الفئة غير موجودة في classpath |
@ConditionalOnBean | bean موجود في السياق |
@ConditionalOnMissingBean | bean غير موجود في السياق |
@ConditionalOnExpression | تعبير SpEL صحيح |
@ConditionalOnWebApplication | التطبيق هو تطبيق ويب |
@ConditionalOnNotWebApplication | التطبيق ليس تطبيق ويب |
7. Annotations المراسلة والأحداث
@EventListener
يستمع للأحداث في Spring.
@Component
public class UserEventListener {
@EventListener
public void handleUserCreated(UserCreatedEvent event) {
log.info("Nouvel utilisateur: {}", event.getUser().getEmail());
emailService.sendWelcomeEmail(event.getUser());
}
@Async
@EventListener
public void handleUserCreatedAsync(UserCreatedEvent event) {
// يُعالج بشكل غير متزامن
}
}
@TransactionalEventListener
يستمع للأحداث المتعلقة بالمعاملة.
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleAfterCommit(OrderCreatedEvent event) {
// يُنفذ فقط إذا نجحت المعاملة
notificationService.notifyNewOrder(event.getOrder());
}
8. Annotations عميل REST
@FeignClient (Spring Cloud)
يُعلن عن عميل REST وصفي.
@FeignClient(name = "user-service", url = "http://localhost:8081")
public interface UserClient {
@GetMapping("/api/users/{id}")
UserDto getUser(@PathVariable Long id);
@PostMapping("/api/users")
UserDto createUser(@RequestBody CreateUserDto dto);
}
جدول ملخص شامل
أكثر من 50 Annotation مستخدمة
| # | Annotation | الفئة | الدور |
|---|---|---|---|
| 1 | @SpringBootApplication | Boot | نقطة الدخول |
| 2 | @Component | Core | bean عام |
| 3 | @Service | Core | منطق الأعمال |
| 4 | @Repository | Core | الوصول إلى قاعدة البيانات |
| 5 | @Controller | Web | متحكم MVC |
| 6 | @RestController | Web | متحكم REST |
| 7 | @Configuration | Core | فئة التكوين |
| 8 | @Bean | Core | إعلان bean |
| 9 | @Autowired | Core | حقن |
| 10 | @Qualifier | Core | اختيار bean |
| 11 | @Value | Core | حقن قيمة |
| 12 | @Profile | Core | التفعيل حسب ملف التعريف |
| 13 | @Scope | Core | نطاق bean |
| 14 | @Lazy | Core | تهيئة مؤجلة |
| 15 | @PostConstruct | Core | تهيئة بعد الحقن |
| 16 | @PreDestroy | Core | تنظيف قبل التدمير |
| 17 | @RequestMapping | Web | تعيين URL |
| 18 | @GetMapping | Web | HTTP GET |
| 19 | @PostMapping | Web | HTTP POST |
| 20 | @PutMapping | Web | HTTP PUT |
| 21 | @DeleteMapping | Web | HTTP DELETE |
| 22 | @PatchMapping | Web | HTTP PATCH |
| 23 | @PathVariable | Web | متغير URL |
| 24 | @RequestParam | Web | متغير استعلام |
| 25 | @RequestBody | Web | جسم JSON |
| 26 | @RequestHeader | Web | رأس HTTP |
| 27 | @ResponseStatus | Web | كود استجابة HTTP |
| 28 | @ExceptionHandler | Web | معالجة الاستثناءات |
| 29 | @RestControllerAdvice | Web | معالجة الأخطاء العامة |
| 30 | @CrossOrigin | Web | CORS |
| 31 | @Valid | Validation | يُفعل التحقق |
| 32 | @NotNull | Validation | ليس null |
| 33 | @NotBlank | Validation | ليس فارغًا |
| 34 | @Size | Validation | حجم الحد الأدنى/الأقصى |
| 35 | @Email | Validation | تنسيق البريد الإلكتروني |
| 36 | @Entity | JPA | كيان = جدول |
| 37 | @Table | JPA | إعدادات الجدول |
| 38 | @Id | JPA | المفتاح الأساسي |
| 39 | @GeneratedValue | JPA | توليد تلقائي للمعرف |
| 40 | @Column | JPA | إعدادات العمود |
| 41 | @OneToMany | JPA | علاقة 1:N |
| 42 | @ManyToOne | JPA | علاقة N:1 |
| 43 | @ManyToMany | JPA | علاقة N:N |
| 44 | @Query | JPA | استعلام مخصص |
| 45 | @Transactional | JPA | معاملة |
| 46 | @EnableWebSecurity | Security | إعدادات الأمان |
| 47 | @PreAuthorize | Security | إذن قبل التنفيذ |
| 48 | @SpringBootTest | Test | اختبار تكاملي |
| 49 | @WebMvcTest | Test | اختبار المتحكم |
| 50 | @DataJpaTest | Test | اختبار المستودع |
| 51 | @MockBean | Test | Mock لـ bean |
| 52 | @Async | متقدم | تنفيذ غير متزامن |
| 53 | @Scheduled | متقدم | مهمة مجدولة |
| 54 | @Cacheable | متقدم | تخزين مؤقت |
| 55 | @EventListener | متقدم | استماع للأحداث |