البرمجة الجانبية في Spring

إنشاء أول Aspect لك

18 دقيقة الدرس 3 من 13

إنشاء أول Aspect لك

معرفة مفهوم AOP نظريًا شيء، وكتابة كلاس تتعامل معه Spring فعليًا كـ aspect شيء آخر. في هذا الدرس ستبني aspect حقيقية وقابلة للتشغيل من الصفر داخل تطبيق Spring Boot 3. بحلول نهاية الدرس ستفهم المكوّنات الثلاثة الأساسية — الأنوتيشن @Aspect، والأنوتيشن @Component، وبنية الـ auto-proxy — وستعرف سبب ضرورة كلٍّ منها.

المكوّنات الثلاثة الأساسية

يحتاج كل Spring aspect إلى ثلاثة أشياء بالضبط لكي يعمل:

  1. @Aspect — يُخبر محرّك AspectJ (الذي تستخدمه Spring في وقت التشغيل) بأن هذا الكلاس يحمل تعريفات advice وpointcut.
  2. @Component (أو أي أنوتيشن نمطي آخر) — يسجّل الكلاس كـ Spring bean حتى يتمكن الحاوي من إدارته. بدون هذا لن تكتشف Spring الـ aspect أصلًا.
  3. تفعيل Auto-proxy — الآلية التي تعترض استدعاءات الدوال وتُمرّرها عبر الـ advice المطابق. في Spring Boot هذا مفعّل افتراضيًا؛ أما في Spring العادية فعليك إضافة أنوتيشن واحدة.
لماذا نحتاج كلاً من @Aspect و@Component؟ لأن كلًّا منهما يخدم غرضًا مختلفًا. @Aspect علامة AspectJ — تُخبر إطار AOP بكيفية تفسير الكلاس. @Component علامة Spring IoC — تُخبر الحاوي بإنشاء bean منه. لا يدلّ أيٌّ منهما على الآخر. نسيان @Component هو الخطأ الأكثر شيوعًا لدى المبتدئين؛ الكود يُترجَم بنجاح لكن الـ advice يُتجاهَل بصمت.

تفعيل AspectJ Auto-Proxying

تعمل Spring AOP بتغليف الـ beans المستهدفة في proxy يعترض الاستدعاءات ويُشغّل الـ advice المطابق قبل التفويض إلى الكائن الحقيقي أو بعده. البنية التحتية التي تُنشئ هذه الـ proxies تُسمى auto-proxy creator، ويُفعَّل بواسطة @EnableAspectJAutoProxy.

في تطبيق Spring Boot لا تحتاج إلى إضافة هذه الأنوتيشن بنفسك. تتضمن وحدة spring-boot-autoconfigure الكلاس AopAutoConfiguration التي تُفعّل الـ auto-proxy تلقائيًا طالما spring-aop موجود في classpath. فقط أضف الـ starter إلى ملف البناء:

<!-- pom.xml --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>

هذه التبعية الواحدة تجلب معها كلًّا من spring-aop وaspectjweaver، وهو مكتبة AspectJ للتشغيل التي تفوّض إليها Spring مطابقة الـ pointcuts. إن كنت تستخدم Spring العادية (بدون Boot) تُفعّل البنية التحتية بشكل صريح:

import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @EnableAspectJAutoProxy public class AppConfig { // تعريفات الـ beans الأخرى }
Spring Boot = لا إعداد زائد. في مشروع Spring Boot الإعداد الوحيد المطلوب هو إضافة الـ starter. إن كان advice يبدو صحيحًا لكنه لا يُنفَّذ، تحقق أولًا من وجود الـ starter في pom.xml أو build.gradle قبل تشخيص أي شيء آخر.

كتابة كلاس الـ Aspect

لنبنِ logging aspect تطبع رسالة في كل مرة تُستدعى فيها أي دالة في طبقة الخدمات. إليك الهيكل الأدنى:

package com.example.demo.aspect; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class); @Before("execution(* com.example.demo.service.*.*(..))") public void logBeforeServiceCall() { log.info("A service method is about to be called"); } }

دعنا نفصّل كل جزء:

  • @Aspect — يُعلّم الكلاس كـ aspect. ستفحصه Spring بحثًا عن أنوتيشنات @Before و@After و@Around وغيرها من أنوتيشنات الـ advice.
  • @Component — يسجّله كـ singleton bean في الـ application context. يمكنك استخدام @Service أو @Repository أيضًا، لكن @Component هو الاختيار الأصدق لأن الـ aspect ليس خدمةً ولا مستودعًا — إنه بنية تحتية cross-cutting.
  • @Before("execution(...)") — يُعلن عن before advice مع تعبير pointcut مضمّن. يستهدف التعبير كل دالة (أي نوع إرجاع *، أي اسم *، أي مُعطيات (..)) داخل أي كلاس في حزمة com.example.demo.service.

مثال عملي كامل

إليك الصورة الكاملة مع خدمة حقيقية حتى تتمكن من التشغيل ومراقبة السلوك:

// OrderService.java package com.example.demo.service; import org.springframework.stereotype.Service; @Service public class OrderService { public String placeOrder(String item) { System.out.println("Placing order for: " + item); return "ORDER-001"; } public void cancelOrder(String orderId) { System.out.println("Cancelling order: " + orderId); } }
// LoggingAspect.java package com.example.demo.aspect; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class); @Before("execution(* com.example.demo.service.*.*(..))") public void logBeforeServiceCall(JoinPoint joinPoint) { log.info("[ASPECT] Entering: {}.{}()", joinPoint.getTarget().getClass().getSimpleName(), joinPoint.getSignature().getName()); } }

لاحظ المُعامل JoinPoint. تحقنه Spring تلقائيًا عند الإعلان عنه كأول مُعامل في دالة الـ advice. يمنحك معلومات وقت التشغيل عن الاستدعاء المعترَض — الكائن المستهدف واسم الدالة والمُعطيات. لا يحتاج توقيع الـ advice إلى مطابقة توقيع الدالة المستهدفة إطلاقًا؛ Spring تتولى الربط.

ما يحدث في وقت التشغيل

عند بدء تشغيل الـ application context يفحص auto-proxy creator جميع تعريفات الـ beans. عندما يجد bean تطابق pointcut واحد على الأقل معلَن في أي @Aspect bean، يُغلّف تلك الـ bean في dynamic proxy. من تلك اللحظة كل استدعاء للـ bean المُغلَّفة يمر عبر الـ proxy أولًا، الذي يتحقق مما إذا كان الـ advice ينطبق على الدالة المُستدعاة ويُشغّله إن انطبق.

بالنسبة للـ OrderService أعلاه، يعترض الـ proxy الذي أنشأته Spring كلًّا من placeOrder وcancelOrder. عندما يحقن controller أو خدمة أخرى OrderService، يستقبل في الواقع الـ proxy لا الكائن الخام. الـ proxy شفاف — ينفّذ نفس الواجهة أو يرث نفس الكلاس — فلا تحتاج عناصر الاستدعاء إلى أي تغييرات.

الاستدعاء الذاتي يتجاوز الـ proxy. إذا استدعت placeOrder الدالةَ cancelOrder مباشرةً (أي this.cancelOrder(...))، يذهب الاستدعاء مباشرةً إلى الكائن الحقيقي ويتخطى الـ proxy كليًا. لن يُشغَّل الـ aspect. هذا قيد جوهري في AOP القائمة على الـ proxy في Spring. الحل هو حقن الـ bean في نفسها (عبر @Lazy) أو، في الحالات المعقدة، استخدام compile-time weaving الكامل لـ AspectJ.

تنظيم الـ Aspects في مشروع حقيقي

احتفظ بجميع كلاسات الـ aspect في حزمة مخصصة كـ com.example.demo.aspect. يجعل هذا واضحًا فورًا أن الكلاس بنية تحتية cross-cutting وليس منطق عمل. كلاس aspect واحد لكل concern — LoggingAspect وSecurityAspect وPerformanceAspect — يُقابل التوسع أفضل من وضع كل الـ advice في كلاس ضخم واحد. يجب أن يكون كل aspect مُركّزًا وقابلًا للاختبار بمعزل.

الخلاصة

يتطلب إنشاء aspect في Spring كلًّا من @Aspect لتحديد الكلاس كبنية AOP، و@Component لتسجيله كـ Spring bean، ودعم auto-proxy (تُوفّره تلقائيًا حزمة AOP الخاصة بـ Spring Boot). تستقبل دوال الـ advice مُعامل JoinPoint اختياريًا يكشف تفاصيل وقت التشغيل عن الاستدعاء المعترَض. الاستدعاء الذاتي هو القيد الصعب الوحيد الذي ينبغي مراعاته. مع هذه الأسس في مكانها أنت مستعد لاستكشاف النطاق الكامل لأنواع الـ advice في الدرس التالي.