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

المفاهيم الأساسية لـ AOP

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

المفاهيم الأساسية لـ AOP

يُقدّم البرمجة الموجّهة نحو الجوانب (AOP) مفردات دقيقة ومحددة. كل نقاش حول AOP — كل مُرشّح (annotation) في Spring، كل أثر في المُنقّح — يستخدم هذه المصطلحات الخمسة: الجانب (Aspect)، ونقطة الالتحاق (Join Point)، والنصيحة (Advice)، ونقطة القطع (Pointcut)، والنسيج (Weaving). أتقنها الآن وسيغدو باقي البرنامج التعليمي سهلًا. أُهملها وستصبح كل شيء ضبابيًا.

لماذا تهمّ المفردات المشتركة: صِيغت مفاهيم AOP في مطلع الألفية الثالثة وهي متسقة عبر AspectJ وSpring AOP وPostSharp في .NET وغيرها. تعلّمها مرة واحدة ينقلك في كل مكان.

الجانب (Aspect)

الجانب هو الوحدة المعيارية التي تُغلّف الاهتمام المشترك بين الأجزاء المختلفة. فكّر فيه على أنه مكافئ AOP للصنف (class): فهو يجمع النصائح وتعريفات نقاط القطع معًا، تمامًا كما يجمع الصنف الحقول والتوابع ذات الصلة.

في Spring تُعلن الجانب بمُرشّحَين على وحدة Spring عادية:

import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Aspect // يُعلّم هذا الصنف كجانب AOP @Component // يُسجّله كوحدة Spring لكي تتمكن Spring من عمل بروكسي له public class AuditAspect { // تعريفات النصيحة ونقطة القطع تُوضع هنا }

بدون @Component (أو ما يعادله)، لن تُنشئ Spring نسخة من الصنف أبدًا ولن يكون للجانب أي أثر. وبدون @Aspect، ستتجاهل Spring مُرشّحات النصيحة داخله. كلا المُرشّحَين ضروريان.

نقطة الالتحاق (Join Point)

نقطة الالتحاق هي أي نقطة محددة في تنفيذ برنامجك حيث يمكن تطبيق النصيحة. تدعم Spring AOP نوعًا واحدًا فقط من نقاط الالتحاق: تنفيذ التابع (method execution). في كل مرة يُستدعى فيها تابع وحدة Spring المُدارة، يُعدّ ذلك الاستدعاء نقطة التحاق.

هذا هو القيد الرئيسي في Spring AOP مقارنةً بـ AspectJ الكامل: لا يمكنك اعتراض قراءة الحقول، أو إنشاء الكائنات، أو استدعاء التوابع الساكنة في Spring AOP الخالص. من الناحية العملية، يغطي تنفيذ التابع 95% من احتياجات القطع المشتركة الحقيقية (التسجيل، فحوصات الأمان، المعاملات، التخزين المؤقت)، لذا نادرًا ما يؤذي هذا القيد.

نموذج ذهني: تخيّل كل تابع في كل وحدة Spring كبابٍ. نقطة الالتحاق هي أي باب من هذه الأبواب. النصيحة هي الحارس الذي يحدد ما يحدث عند فتح الباب. نقطة القطع هي القاعدة التي تحدد أي الأبواب يحصل على حارس.

عند التشغيل، حين تُطلَق النصيحة، تُغلّف Spring نقطة الالتحاق في كائن JoinPoint (من الواجهة org.aspectj.lang.JoinPoint) يمكنك فحصه:

import org.aspectj.lang.JoinPoint; // داخل تابع الجانب public void logBefore(JoinPoint jp) { String method = jp.getSignature().getName(); // "createOrder" Object[] args = jp.getArgs(); // وسائط التابع String target = jp.getTarget().getClass().getName(); // "com.example.OrderService" }

النصيحة (Advice)

النصيحة هي الكود الذي يعمل فعلًا عند نقطة الالتحاق — إنها الماذا والمتى للاعتراض. تدعم Spring AOP خمسة أنواع من النصائح، كل منها يتوافق مع لحظة مختلفة بالنسبة لتنفيذ التابع:

  • @Before — يعمل قبل التابع المستهدف. لا يمكنه منع التابع من التشغيل (استخدم @Around لذلك).
  • @AfterReturning — يعمل بعد أن يُرجع التابع بشكل طبيعي. يستقبل القيمة المُرجعة.
  • @AfterThrowing — يعمل إذا رمى التابع استثناءً. يستقبل كائن الاستثناء.
  • @After — يعمل بعد انتهاء التابع، سواء بشكل طبيعي أو مع استثناء (مثل كتلة finally).
  • @Around — يُغلّف الاستدعاء بأكمله؛ تستدعي proceed() لتشغيل التابع الحقيقي (يُغطى بعمق في الدرس 6).

مثال سريع على نصيحة @Before في السياق:

import org.aspectj.lang.annotation.Before; import org.aspectj.lang.JoinPoint; @Before("execution(* com.example.service.*.*(..))") public void logMethodEntry(JoinPoint jp) { System.out.println("Entering: " + jp.getSignature().toShortString()); }

وسيطة السلسلة النصية لـ @Before هي تعبير نقطة قطع — وهو ما يقودنا إلى المفهوم التالي.

نقطة القطع (Pointcut)

نقطة القطع هي محمول (predicate) — تعبير منطقي — يختار مجموعة فرعية من جميع نقاط الالتحاق. حيث تجيب النصيحة على سؤال ماذا أفعل، تجيب نقطة القطع على سؤال أين أفعله. الاثنان مفصولان عن قصد: يمكنك إعادة استخدام نقطة القطع عبر نصائح متعددة، ويمكنك تغيير إحداهما دون المساس بالأخرى.

تستخدم Spring AOP لغة تعبيرات نقطة القطع الخاصة بـ AspectJ. أكثر المحددات شيوعًا هو execution:

// يطابق أي تابع عام في أي صنف داخل حزمة service execution(public * com.example.service.*.*(..)) // يطابق أي تابع يبدأ اسمه بـ "find*" في OrderRepository، بأي نوع إرجاع وأي وسائط execution(* com.example.repository.OrderRepository.find*(..)) // يطابق أي تابع على الوحدات المُرشَّحة بـ @Transactional @annotation(org.springframework.transaction.annotation.Transactional)

الممارسة الموصى بها هي استخراج نقاط القطع المُسمّاة باستخدام @Pointcut لكي يمكن إعادة استخدامها وتركيبها:

import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.AfterReturning; @Aspect @Component public class AuditAspect { // نقطة قطع مُسمّاة وقابلة لإعادة الاستخدام @Pointcut("execution(* com.example.service.*.*(..))") public void serviceLayer() {} // جسم التابع دائمًا فارغ // نصيحتان تُعيدان استخدام نقطة القطع ذاتها @Before("serviceLayer()") public void logEntry(JoinPoint jp) { System.out.println(">> " + jp.getSignature().getName()); } @AfterReturning(pointcut = "serviceLayer()", returning = "result") public void logExit(JoinPoint jp, Object result) { System.out.println("<< " + jp.getSignature().getName() + " returned: " + result); } }
جسم التابع الفارغ مقصود: التابع المُرشَّح بـ @Pointcut لا يُنفَّذ أبدًا فعليًا. إنه مجرد مرساة مُسمّاة يمكن للغة التعبيرات الرجوع إليها. يجب أن يكون التابع موجودًا وأن يُرجع void — الاسم هو ما يهم.

النسيج (Weaving)

النسيج هو عملية ربط الجوانب بالكائنات التي تؤثر عليها — الخطوة الآلية التي تجعل الاعتراض يعمل. هناك ثلاثة أوقات محتملة يمكن أن يحدث فيها النسيج:

  1. النسيج وقت التحويل (CTW): يُعدّل مُحوّل AspectJ (ajc) الكود الثنائي (bytecode) أثناء التحويل. ملفات .class الناتجة تحتوي بالفعل على كود النصيحة مُضمَّنًا. الأسرع في وقت التشغيل؛ يتطلب مُحوّل AspectJ في سلسلة البناء.
  2. النسيج وقت التحميل (LTW): وكيل Java يُعيد كتابة الكود الثنائي أثناء تحميل الصفوف بواسطة محمّل الصفوف. لا حاجة لتغيير المُحوّل؛ يُضيف تكلفة بدء التشغيل.
  3. النسيج وقت التشغيل (قائم على البروكسي): يُنشئ الإطار كائن بروكسي عند البدء يُغلّف الوحدة الحقيقية. هذا ما تستخدمه Spring AOP حصريًا. لا تعديل للكود الثنائي؛ يعمل فقط لوحدات Spring المُدارة؛ محدود بنقاط الالتحاق الخاصة بتنفيذ التابع.

النسيج وقت التشغيل في Spring يعني أن النسيج يحدث مرة واحدة عند بدء سياق التطبيق، لا في كل استدعاء تابع. عندما تُنشئ Spring وحدة تتطابق مع نقطة قطع واحدة على الأقل، تستبدل بهدوء مرجع الوحدة البسيطة ببروكسي. المُستدعون يتعاملون مع البروكسي الذي يُشغّل النصيحة ثم يُفوّض إلى الكائن الحقيقي. من منظور المُستدعي، لا شيء يتغير — لا يزال يحصل على الواجهة أو الصنف الذي طلبه.

الاستدعاء الذاتي لا يعمل مع بروكسيات Spring AOP. إذا استدعت وحدة تابعها الخاص داخليًا (مثل this.someMethod())، يتجاوز الاستدعاء البروكسي تمامًا ولا تُطلَق أي نصيحة. هذا هو الخطأ الأكثر شيوعًا في Spring AOP. الحل هو حقن الوحدة في نفسها عبر @Autowired على حقل، أو استخدام نسيج AspectJ الكامل. يُغطّي الدرس 9 هذا بالتفصيل.

الجمع في صورة واحدة

إليك سيناريو محدد يستخدم المفاهيم الخمسة دفعةً واحدة. المتطلب: تسجيل كل استدعاء تابع عام في الحزمة com.example.service.

import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Aspect // (1) هذا الصنف هو الجانب (ASPECT) @Component public class MethodLoggingAspect { // (3) نقطة القطع (POINTCUT) — تختار نقاط الالتحاق المُعترَضة @Pointcut("execution(public * com.example.service.*.*(..))") public void publicServiceMethods() {} // (2) كل تابع عام في خدمة هو نقطة التحاق (JOIN POINT) محتملة // (4) النصيحة (ADVICE) — ماذا يحدث عند نقاط الالتحاق المتطابقة @Before("publicServiceMethods()") public void logCall(JoinPoint jp) { System.out.printf("[AUDIT] %s called with %d arg(s)%n", jp.getSignature().toShortString(), jp.getArgs().length); } // (5) النسيج (WEAVING) — تُنشئ Spring وحدات بروكسي عند البدء؛ // تُطلَق logCall() قبل كل تابع متطابق }

الخلاصة

تُشكّل المفاهيم الخمسة لـ AOP سلسلة ذهنية واضحة: الجانب يجمع منطق القطع المشترك ذا الصلة؛ نقطة الالتحاق هي أي نقطة اعتراض مؤهلة (في Spring AOP: أي استدعاء تابع لوحدة Spring)؛ النصيحة هي الكود الذي يعمل عند نقطة الالتحاق وتُحدد متى يعمل (قبل، بعد، حول)؛ نقطة القطع هي التعبير الذي يختار أي نقاط الالتحاق تُطلق أي نصيحة؛ والنسيج هي العملية — في حالة Spring: إنشاء البروكسي عند البدء — التي تربط الجوانب بالتطبيق الجاري. الدرس القادم يحوّل هذه المفاهيم إلى كود عامل بأول جانب كامل لك.