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

أنواع النصائح (Advice Types)

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

أنواع النصائح (Advice Types)

في Spring AOP، النصيحة (Advice) هي الإجراء الذي ينفّذه الجانب (Aspect) عند نقطة انضمام (Join Point) معينة. يمنحك Spring خمسة تعليقات توضيحية مميزة للنصائح، لكلٍّ منها علاقة مختلفة بتنفيذ الأسلوب المستهدف. اختيار النوع الصحيح ليس مسألة أسلوب — فلكل نوع ضمانات محددة وقدرات محددة ومقايضات تؤثر على الصحة والأداء والقابلية للصيانة.

جميع الأنواع الخمسة موجودة في الحزمة org.aspectj.lang.annotation وتعمل مع Spring 6 / Spring Boot 3 باستخدام مساحة الأسماء jakarta.*.

@Before — التنفيذ قبل الأسلوب

تُنفَّذ نصيحة @Before قبل استدعاء الأسلوب المستهدف. لا يمكنها إيقاف التنفيذ (إلا برمي استثناء) ولا يمكنها رؤية القيمة المُعادة لأن الأسلوب لم يُنفَّذ بعد.

الاستخدامات الشائعة: فحوصات التحكم في الوصول، التحقق من المدخلات، التحقق من الشروط المسبقة، تسجيل الأحداث قبل وقوع الاستدعاء.

import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class SecurityAspect { @Before("execution(* com.example.service.OrderService.placeOrder(..))") public void checkAuthentication(JoinPoint jp) { // jp.getArgs() تُعيد وسائط الأسلوب Object[] args = jp.getArgs(); System.out.println("على وشك استدعاء: " + jp.getSignature().getName() + " بعدد " + args.length + " وسيطة"); // ارمِ استثناءً لإلغاء الاستدعاء if (!SecurityContext.isAuthenticated()) { throw new SecurityException("تم رفض الاستدعاء: المستخدم غير مصادق"); } } }
الضمان الأساسي: تعمل @Before دائمًا قبل جسم الأسلوب. إذا رمت استثناءً، لن يُستدعى الأسلوب المستهدف أبدًا وسيُمرّر Spring الاستثناء إلى المُستدعي. هذا ما يجعلها الخطّاف (hook) المناسب لمنطق التحكم في البوابات.

@After — التنفيذ بعد الأسلوب (Finally)

تعمل نصيحة @After بعد اكتمال الأسلوب — سواء أعاد قيمة بشكل طبيعي أم رمى استثناءً. فكّر فيها كمكافئ الجانب لكتلة finally. لا يمكنها الوصول إلى القيمة المُعادة ولا يمكنها إخفاء الاستثناء أو استبداله.

الاستخدامات الشائعة: تحرير الموارد، مسح حالة الـ Thread المحلية، سجلات "نهاية الاستدعاء" في المراجعات.

import org.aspectj.lang.annotation.After; @Aspect @Component public class CleanupAspect { @After("execution(* com.example.service.*.*(..))") public void releaseContext(JoinPoint jp) { // تُنفَّذ بصرف النظر عن النجاح أو الفشل RequestContext.clear(); System.out.println("تم مسح السياق بعد: " + jp.getSignature()); } }
@After لا تستطيع تغيير النتيجة. إذا رمى الأسلوب المستهدف استثناءً، سيستمر ذلك الاستثناء في الانتشار بعد تنفيذ نصيحتك. إذا كنت بحاجة إلى التفاعل بشكل مختلف مع النجاح مقابل الفشل، استخدم @AfterReturning و@AfterThrowing بدلًا من ذلك — فهما يمنحانك هذا التمييز.

@AfterReturning — التنفيذ بعد الإعادة الطبيعية

تُطلق @AfterReturning فقط عندما يكتمل الأسلوب المستهدف دون رمي استثناء. قوتها الكبرى: يمكنك ربط القيمة المُعادة الفعلية وفحصها أو تسجيلها. لا يمكنك استبدال القيمة المُعادة من هذا النوع من النصائح.

import org.aspectj.lang.annotation.AfterReturning; @Aspect @Component public class AuditAspect { // 'returning' يُسمّي المعامل الذي سيستقبل القيمة المُعادة @AfterReturning( pointcut = "execution(* com.example.repository.UserRepository.save(..))", returning = "savedUser" ) public void auditSave(JoinPoint jp, Object savedUser) { // savedUser هو الكائن الذي أعادته UserRepository.save() فعليًا System.out.println("تم الحفظ: " + savedUser); AuditLog.record("USER_SAVED", savedUser); } }

يمكنك تضييق نوع المعامل المربوط. إذا كتبت Object savedUser فإنه يطابق أي نوع إعادة. إذا كتبت User savedUser، سيطبّق Spring النصيحة فقط عندما تكون القيمة المُعادة متوافقة مع User.

نمط ملء الذاكرة المؤقتة: @AfterReturning هي أنظف مكان لملء ذاكرة التخزين المؤقت بعد قراءة ناجحة من قاعدة البيانات. الأسلوب نُفِّذ وتم بنجاح والنتيجة في يدك الآن — دون أي بنية try/catch في طبقة الخدمة.

@AfterThrowing — التنفيذ عند رمي استثناء

تُطلق @AfterThrowing فقط عندما يرمي الأسلوب المستهدف استثناءً يتجاوزه. يمكنك ربط الاستثناء وتسجيله وتنبيه أنظمة المراقبة أو إثرائه — لكن الاستثناء لا يزال ينتشر ما لم ترمِ استثناءً مختلفًا.

import org.aspectj.lang.annotation.AfterThrowing; @Aspect @Component public class ExceptionMonitorAspect { // 'throwing' يُسمّي المعامل الذي سيستقبل الاستثناء @AfterThrowing( pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex" ) public void onServiceException(JoinPoint jp, RuntimeException ex) { // تُطلق فقط للـ RuntimeException وأنواعها الفرعية System.err.println("استثناء في " + jp.getSignature() + ": " + ex.getMessage()); MetricsClient.increment("service.error", "method", jp.getSignature().getName()); } }

كما في @AfterReturning، يمكنك تقييد نوع الاستثناء. الإعلان عن RuntimeException ex يعني أن الاستثناءات المتحقق منها (checked exceptions) تمر دون تشغيل هذه النصيحة. الإعلان عن Throwable ex يصطاد كل شيء.

@Around — التحكم الكامل في التنفيذ

تُعدّ @Around أقوى نوع من النصائح. فهي تُحيط باستدعاء الأسلوب بالكامل: أنت تقرر ما إذا كنت ستستدعي الأسلوب المستهدف ومتى، ويمكنك تعديل الوسائط قبل استدعائه، وتعديل القيمة المُعادة أو استبدالها بعد عودته. هذه القوة تأتي بمسؤولية — إذا نسيت استدعاء proceed()، لن يُنفَّذ الأسلوب المستهدف أبدًا.

import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; @Aspect @Component public class TimingAspect { @Around("execution(* com.example.service.*.*(..))") public Object measureTime(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); try { // استدعاء الأسلوب الفعلي؛ pjp.proceed() تُعيد قيمته المُعادة Object result = pjp.proceed(); long elapsed = System.currentTimeMillis() - start; System.out.println(pjp.getSignature() + " اكتمل في " + elapsed + " مللي ثانية"); return result; // يجب الإعادة — المُستدعي ينتظرها } catch (Throwable t) { long elapsed = System.currentTimeMillis() - start; System.out.println(pjp.getSignature() + " فشل بعد " + elapsed + " مللي ثانية"); throw t; // أعد الرمي حتى تُطبَّق معالجة الاستثناءات الطبيعية } } }
أعِد دائمًا نتيجة pjp.proceed(). قد يكون نوع الإعادة للأسلوب void (في هذه الحالة تكون القيمة null وإهمالها مقبول)، لكن لجميع الأنواع الأخرى ينتظر المُستدعي تلك القيمة. نسيان تعليمة الإعادة يُترجَم بلا أخطاء لكنه يُنتج أخطاء null صامتة في وقت التشغيل.

ترتيب التنفيذ عندما تنطبق نصائح متعددة

عندما تتطابق عدة نصائح مع نفس نقطة الانضمام، يستدعيها Spring بترتيب محدد حول مكدس الاستدعاء:

  1. @Around (قبل proceed())
  2. @Before
  3. تنفيذ الأسلوب المستهدف
  4. @AfterReturning أو @AfterThrowing (أيهما ينطبق)
  5. @After
  6. @Around (بعد عودة proceed())

عندما تنطبق نصيحتان من نفس النوع في جانبين مختلفين على نفس نقطة الانضمام، يمكنك التحكم في ترتيبهما النسبي باستخدام @Order(n) على فئة الجانب — الرقم الأصغر يعمل في الخارج (أول في الدخول، آخر في الخروج).

import org.springframework.core.annotation.Order; @Aspect @Component @Order(1) // الأكثر خارجية — يعمل أولًا عند الدخول، آخرًا عند الخروج public class TransactionAspect { ... } @Aspect @Component @Order(2) // داخلي — يعمل ثانيًا عند الدخول، أولًا عند الخروج public class LoggingAspect { ... }

اختيار النوع الصحيح من النصائح

  • تحتاج إلى بوابة أو تحقق قبل التنفيذ؟ استخدم @Before.
  • تحتاج إلى تنظيف مضمون بصرف النظر عن النتيجة؟ استخدم @After.
  • تحتاج إلى القيمة المُعادة للتسجيل أو التخزين المؤقت أو التفاعل مع النجاح؟ استخدم @AfterReturning.
  • تحتاج إلى اكتشاف استثناء معين وتسجيله أو إثرائه؟ استخدم @AfterThrowing.
  • تحتاج إلى تعديل الوسائط أو إخفاء الاستثناءات أو استبدال القيمة المُعادة؟ استخدم @Around.

يُفضَّل استخدام النوع الأقل قوةً الذي يلبّي متطلباتك. @Around قادرة على كل شيء، لكن استخدامها لمجرد التسجيل البسيط يُعيّم النية ويُدخل خطر ابتلاع الاستثناءات أو القيم المُعادة عن طريق الخطأ. احتفظ بها للمخاوف المشتركة التي تحتاج حقًا إلى تحكم كامل: التخزين المؤقت، والمحاولة مجددًا، والمعاملات، وقواطع الدوائر.

الخلاصة

تُشكّل الأنواع الخمسة من النصائح في Spring AOP طيفًا من الحد الأدنى من المسؤولية (@Before، @After) إلى التحكم الكامل (@Around). لكل نوع عقد واضح: ما الذي يراه، ومتى يعمل، وما الذي يمكنه تغييره. مطابقة النوع الصحيح لمتطلباتك تجعل كود الجانب مقروءًا، وتمنع الأخطاء الدقيقة، وتوصل النية للمطوّر التالي. في الدروس التالية ستضع هذا موضع التطبيق مع @Around وواجهة ProceedingJoinPoint، ثم ستبني جوانب تسجيل وأمن حقيقية.