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

تعبيرات نقاط القطع

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

تعبيرات نقاط القطع

معرفة كيفية كتابة أسلوب الاستشارة (advice) ليست سوى نصف القصة. النصف الآخر — والجانب الذي يُميّز الجوانب القابلة للصيانة عن تلك الهشّة — هو إخبار Spring بدقة بنقاط الدمج التي يجب أن تعترضها تلك الاستشارة. هذا هو دور تعبير نقطة القطع. أتقن لغة التعابير وستحصل على سيطرة جراحية على اهتماماتك المشتركة دون لمس سطر واحد من الكود التجاري.

ما هو تعبير نقطة القطع في حقيقته

تعبير نقطة القطع هو سلسلة نصية مكتوبة بلغة تعابير Spring AOP (مستعارة من AspectJ). يُقيَّم عند بدء تشغيل السياق: يفحص Spring كل حبّة (bean)، ويحلّل التعبير مقابل كل أسلوب، ويبني وكيلًا (proxy) لأي حبّة تتطابق أساليبها مع التعبير. وقت التشغيل، يعترض الوكيل الاستدعاءات المتطابقة فقط — أما كل شيء آخر فيمرّ مباشرة دون أي تكلفة.

يمكنك إرفاق التعبير إما مباشرةً على تعليق الاستشارة أو على أسلوب @Pointcut مستقل، ثم الإشارة إلى ذلك الأسلوب بالاسم:

@Aspect @Component public class AuditAspect { // نقطة قطع مسمّاة وقابلة لإعادة الاستخدام @Pointcut("execution(* com.example.service.*.*(..))") public void serviceLayer() {} // الجسم فارغ دائمًا @Before("serviceLayer()") public void auditBefore(JoinPoint jp) { System.out.println("Calling: " + jp.getSignature().getName()); } }
أعلن دائمًا عن نقاط القطع المشتركة كأساليب @Pointcut مسمّاة. إذا لصقت نفس سلسلة التعبير في خمسة تعليقات استشارة، فإن تغيير حرف واحد في اسم حزمة يكسر الكل بصمت عند بدء التشغيل — لكنك تُصلح نقطة قطع واحدة فقط بدلًا من تعديل كل تكرار.

مُحدِّد execution()

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

execution( [نمط-المعدِّل] نمط-نوع-الإرجاع [نمط-نوع-الإعلان.] نمط-اسم-الأسلوب(نمط-المعاملات) [نمط-الاستثناءات] )

الأقواس المربعة تُشير إلى أجزاء اختيارية. عمليًا ستستخدم ثلاثة أو أربعة أجزاء. إليك مجموعة تدريجية من الأمثلة الحقيقية، كل منها أكثر تحديدًا من السابق:

// 1. كل أسلوب عام في كل فئة — أوسع تطابق ممكن execution(public * *(..)) // 2. كل أسلوب يبدأ اسمه بـ "find"، في أي مكان execution(* find*(..)) // 3. كل أسلوب في حزمة الخدمة (ليس الحزم الفرعية) execution(* com.example.service.*.*(..)) // 4. كل أسلوب في حزمة الخدمة وجميع الحزم الفرعية execution(* com.example.service..*.*(..)) // 5. فقط الأساليب المسمّاة "save" التي تُرجع void، في أي مكان execution(void save(..)) // 6. أسلوب محدد بأنواع معاملات دقيقة (بدون أحرف بدل) execution(* com.example.service.OrderService.placeOrder( com.example.model.Order, com.example.model.User)) // 7. الأساليب التي تقبل وسيطًا واحدًا بالضبط من أي نوع execution(* *(*)) // 8. الأساليب التي تُرجع List — لاحظ النوع الكامل التسمية execution(java.util.List<?> *(..))

رموز الأنماط التي تحتاج معرفتها:

  • * — يُطابق أي جزء مفرد (مستوى حزمة واحد، اسم نوع واحد، اسم أسلوب واحد، نوع إرجاع واحد). لا يتجاوز النقاط.
  • .. — في موضع الحزمة يُطابق أي عدد من الحزم الفرعية؛ في موضع المعاملات يُطابق أي عدد وأي نوع من المعاملات.
  • () — بدون وسيطات تمامًا.
  • (*) — وسيط واحد بالضبط من أي نوع.
  • (..) — صفر وسيطات أو أكثر من أي نوع (الأكثر شيوعًا).
فضّل التحديد على الاتساع. تعبير مثل execution(* *(..)) يُطابق كل أسلوب في كل حبّة مُخوَّل، بما فيها حبّات البنية التحتية التي لم تقصد اعتراضها. ابدأ بالحزمة الكاملة المسمّاة ووسّع فقط عند الحاجة.

مُحدِّد within()

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

// كل الأساليب في هذه الفئة تحديدًا within(com.example.service.PaymentService) // كل الأساليب في كل فئة في حزمة المستودع within(com.example.repository.*) // كل الأساليب في حزمة المستودع وجميع الحزم الفرعية within(com.example.repository..*) // كل الفئات المُعلَّقة بـ @Service (تطابق التعليق على مستوى النوع) within(@org.springframework.stereotype.Service *)

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

execution() مقابل within() — اختيار الأداة المناسبة

غالبًا ما يتداخل هذان المُحدِّدان، لكنهما ليسا قابلَين للتبادل:

  • استخدم execution() عندما يكون التوقيع هو المهم — نوع إرجاع محدد، اصطلاح تسمية كـ find* أو save*، أو قائمة معاملات معيّنة.
  • استخدم within() عندما يكون الموقع هو المهم — "اعترض كل شيء في طبقة الخدمة" أو "اعترض كل شيء في هذه الفئة المحددة".
  • استخدم كليهما معًا عندما تحتاج القيدَين: نمط اسم أسلوب داخل حزمة محددة.
// مدمج: فقط الأساليب التي تبدأ بـ "find" داخل حزمة المستودع @Pointcut("execution(* find*(..)) && within(com.example.repository.*)") public void repositoryFinders() {}
المعاملات المنطقية في تعبيرات نقاط القطع: استخدم && (AND) و|| (OR) و! (NOT) لتركيب التعبيرات. في ضبط XML يجب عليك كتابة and وor وnot لأن XML يُفلت محرف العطف — لكن في التعليقات تعمل السلسلة && مباشرةً.

نقاط القطع المستندة إلى التعليقات مع @annotation()

نمط شائع في الواقع العملي هو تعريف تعليق مخصص ثم كتابة نقطة قطع تُطابق أي أسلوب يحمل ذلك التعليق. يمنح هذا مؤلفي المكتبات ومصمّمي الأطر ربطًا نظيفًا قائمًا على الاختيار الصريح:

// 1. تعريف التعليق الإشاري package com.example.annotation; import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Auditable {} // 2. الإشارة إليه من الجانب @Pointcut("@annotation(com.example.annotation.Auditable)") public void auditableMethods() {} @Before("auditableMethods()") public void recordAuditEvent(JoinPoint jp) { // يُنفَّذ فقط للأساليب المُعلَّقة بـ @Auditable } // 3. الاشتراك بالأساليب الفردية @Service public class InvoiceService { @Auditable public Invoice generateInvoice(Order order) { ... } public Invoice fetchInvoice(Long id) { ... } // لا يُعترَض }

هذا النمط أكثر قابلية للصيانة بكثير من التعبيرات المستندة إلى الحزم عندما تحتاج تحكمًا دقيقًا، لأن التعليق يرافق الأسلوب حتى لو انتقل إلى حزم مختلفة.

الأخطاء الشائعة وكيفية تجنّبها

Spring AOP يعترض فقط الحبّات المُدارة من Spring. تعبير نقطة قطع يُطابق أسلوبًا على كائن مُنشأ بـ new مباشرةً لن يُطلَق أبدًا، لأنه لا يوجد وكيل. أدر دائمًا حبّاتك عبر حاوية IoC الخاصة بـ Spring.
  • الاستدعاء الذاتي لا يُعترَض. إذا استدعى OrderService.methodA() داخليًا this.methodB()، يتجاوز وكيل AOP الأسلوبَ methodB. الحل هو حقن الحبّة في نفسها (يتعامل Spring 6 مع التبعية الدائرية) أو إعادة الهيكلة إلى حبّة مستقلة.
  • الأساليب الخاصة (private) ومحدودة الرؤية للحزمة لا تُطابَق قط بواسطة Spring AOP (يعتمد على وكلاء JDK الديناميكية أو CGLIB، وكلاهما يُجاوز الأساليب العامة والمحمية فقط). إذا بدا أن نقطة قطعك لا تُطلَق، تحقق من رؤية الأسلوب أولًا.
  • الرمز .. في نمط الحزمة يُطابق صفر مقاطع أو أكثر، لذا يُطابق com.example.. الحزمةَ com.example نفسها وأي حزم متداخلة. هذا مصدر شائع لتطابقات أوسع مما قُصِد.

التحقق من صحة تعبيراتك

بدلًا من التخمين في مطابقة النمط، اكتب اختبار تكامل سريعًا وسجّل نقاط الدمج:

@Aspect @Component @Profile("dev") // نشط فقط في بيئة التطوير public class PointcutDebugAspect { @Before("execution(* com.example..*.*(..)) && !within(PointcutDebugAspect)") public void logMatch(JoinPoint jp) { System.out.println("[AOP DEBUG] Matched: " + jp.getSignature()); } }

اربطه بـ Spring Profile حتى لا يصل إلى الإنتاج أبدًا. شغّل التطبيق، مارس ميزاتك، وافحص مخرجات الطرفية. إذا لم يظهر أسلوب توقعت تطابقه — تحقق من الرؤية وإدارة الحبّة والاستدعاء الذاتي.

الخلاصة

يمنحك execution() دقة على مستوى التوقيع: التطابق على نوع الإرجاع واسم الأسلوب وأنواع المعاملات. يمنحك within() دقة على مستوى الموقع: تطابق كل شيء داخل نوع أو حزمة. اجمعهما مع && و|| و!، واستخدم @annotation() للاعتراض القائم على الاشتراك الصريح. سمّ التعبيرات المشتركة بأساليب @Pointcut لإعادة الاستخدام، واجعل التعبيرات محددة بقدر ما تتطلبه حالة الاستخدام. في الدرس التالي ستطبّق هذه الأدوات على أكثر أنواع الاستشارات مرونة: @Around.