
تعد النصوص البرمجية واحدة من أكثر الطرق شيوعًا لجعل التطبيق أكثر مرونة ، مع القدرة على إصلاح شيء ما أثناء التنقل. بالطبع ، هذا النهج له أيضًا عيوب ؛ يجب أن تتذكر دائمًا التوازن بين المرونة وقابلية الإدارة. ولكن في هذه المقالة لن نناقش "بشكل عام" حول إيجابيات وسلبيات استخدام البرامج النصية ، وسننظر في طرق عملية لتطبيق هذا النهج ، ونقدم أيضًا مكتبة توفر بنية تحتية ملائمة لإضافة برامج نصية إلى التطبيقات المكتوبة في Spring Framework.
بضع كلمات تمهيدية
عندما تريد إضافة القدرة على تغيير منطق الأعمال في تطبيق دون إعادة الترجمة والنشر اللاحق ، فإن النصوص البرمجية هي إحدى الطرق التي تتبادر إلى الذهن في المقام الأول. غالبًا ما تظهر البرامج النصية ليس لأنها كانت مقصودة ، ولكن لأنها حدثت. على سبيل المثال ، في المواصفات هناك جزء من المنطق غير واضح تمامًا في الوقت الحالي ، ولكن من أجل عدم قضاء يومين إضافيين (وأحيانًا أطول) للتحليل ، يمكنك إنشاء نقطة امتداد واستدعاء نص برمجي - كعب. وبعد ذلك ، بالطبع ، سيتم إعادة كتابة هذا البرنامج النصي عندما تصبح المتطلبات واضحة.
الطريقة ليست جديدة ، ومزاياها وعيوبها معروفة جيدًا: المرونة - يمكنك تغيير المنطق في تطبيق قيد التشغيل وتوفير الوقت عند إعادة التثبيت ، ولكن ، من ناحية أخرى ، من الصعب اختبار البرامج النصية ، وبالتالي المشاكل المحتملة للأمان والأداء وما إلى ذلك.
يمكن أن تكون هذه التقنيات ، التي ستتم مناقشتها لاحقًا ، مفيدة للمطورين الذين يستخدمون بالفعل البرامج النصية في تطبيقاتهم ، ولأولئك الذين يفكرون فيها فقط.
لا شيء شخصي ، فقط البرمجة النصية
مع JSR-233 ، أصبحت البرمجة النصية في Java بسيطة للغاية. هناك ما يكفي من محركات البرمجة النصية استنادًا إلى واجهة برمجة التطبيقات هذه (Nashorn و JRuby و Jython والمزيد) ، لذا فإن إضافة القليل من سحر البرمجة النصية إلى التعليمات البرمجية الخاصة بك ليست مشكلة:
Map<String, Object> parameters = createParametersMap(); ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine scriptEngine = manager.getEngineByName("groovy"); Object result = scriptEngine.eval(script.getScriptAsString("discount.groovy"), new SimpleBindings(parameters));
من الواضح ، إذا كان هذا الرمز مبعثرًا في جميع أنحاء التطبيق ، فسوف يتحول إلى شيء غير مفهوم. وبالطبع ، إذا كان لديك أكثر من مكالمة نصية واحدة في تطبيقك ، فأنت بحاجة إلى إنشاء فصل منفصل للعمل معهم. في بعض الأحيان ، يمكنك الذهاب إلى أبعد من ذلك وإنشاء فصول خاصة من شأنها التفاف مكالمات
evaluateGroovy()
بطرق Java العادية المكتوبة. سيكون لهذه الطرق رمز فائدة موحد إلى حد ما ، كما في المثال:
public BigDecimal applyCustomerDiscount(Customer customer, BigDecimal orderAmount) { Map<String, Object> params = new HashMap<>(); params.put("cust", customer); params.put("amount", orderAmount); return (BigDecimal)scripting.evalGroovy(getScriptSrc("discount.groovy"), params); }
يزيد هذا النهج الشفافية بدرجة كبيرة عند استدعاء البرامج النصية من رمز التطبيق - يمكنك على الفور رؤية المعلمات التي يقبلها البرنامج النصي ، ونوعها وما يتم إرجاعه. الشيء الرئيسي هو ألا تنسى أن تضيف إلى معايير كتابة التعليمات البرمجية حظرًا على استدعاء البرامج النصية وليس من الطرق المكتوبة!
نقوم بضخ النصوص
على الرغم من حقيقة أن البرامج النصية بسيطة ، إذا كان لديك الكثير منها وتستخدمها بشكل مكثف ، فهناك فرصة حقيقية لمواجهة مشكلات في الأداء. على سبيل المثال ، إذا كنت تستخدم مجموعة من القوالب الرائعة لإنشاء التقارير وقمت بتشغيلها في نفس الوقت ، فسيكون هذا عاجلاً أم آجلاً أحد الاختناقات في أداء التطبيق.
لذلك ، تقوم العديد من الأطر بعمل إضافات مختلفة على واجهة برمجة التطبيقات القياسية لتحسين سرعة العمل ، والتخزين المؤقت ، ومراقبة التنفيذ ، واستخدام لغات البرمجة النصية المختلفة في تطبيق واحد ، وما إلى ذلك.
على سبيل المثال ، تم إنشاء محرك
برمجة نصية بارع إلى حد ما في CUBA يدعم ميزات إضافية ، مثل:
- القدرة على كتابة البرامج النصية في Java و Groovy
- ذاكرة التخزين المؤقت للفئة حتى لا يتم إعادة ترجمة البرامج النصية
- بن JMX للتحكم في المحرك
كل هذا ، بالطبع ، يحسن الأداء وقابلية الاستخدام ، ولكن لا يزال المحرك منخفض المستوى لا يزال منخفض المستوى ، وما زلت بحاجة إلى قراءة نص البرنامج النصي ، وتمرير المعلمات واستدعاء API لتنفيذ البرنامج النصي. لذلك ما زلت بحاجة إلى القيام بنوع من الغلاف في كل مشروع لجعل التطوير أكثر كفاءة.
ولن يكون من العدل عدم ذكر GraalVM - محرك تجريبي يمكنه تشغيل البرامج بلغات مختلفة (JVM و non-JVM) ويسمح لك بإدراج
وحدات في هذه اللغات في
تطبيقات Java . آمل أن يدخل ناشورن في التاريخ عاجلاً أم آجلاً ، وستتاح لنا الفرصة لكتابة أجزاء من الشفرة بلغات مختلفة في مصدر واحد. ولكن هذا مجرد حلم.
إطار عمل الربيع: عرض يصعب رفضه؟
يحتوي Spring على دعم تنفيذ نصي مدمج مبني على واجهة JDK API. في
org.springframework.scripting.*
الحزمة ، يمكنك العثور على العديد من الفئات المفيدة - كل ذلك بحيث يمكنك استخدام واجهة برمجة التطبيقات ذات المستوى المنخفض للبرمجة في تطبيقك بسهولة.
بالإضافة إلى ذلك ، هناك مستوى أعلى من الدعم ، تم وصفه بالتفصيل في
الوثائق . باختصار - تحتاج إلى إنشاء فصل بلغة نصية (على سبيل المثال ، Groovy) ونشره كحبة فول عبر وصف XML:
<lang:groovy id="messenger" script-source="classpath:Messenger.groovy"> <lang:property name="message" value="I Can Do The Frug" /> </lang:groovy>
بمجرد نشر الفول ، يمكن إضافته إلى فصوله باستخدام IoC. يوفر الربيع التحديث التلقائي للنص البرمجي عند تغيير النص في الملف ، ويمكنك تعليق الجوانب على الطرق ، وما إلى ذلك.
يبدو الأمر جيدًا ، لكنك تحتاج إلى إنشاء فصول "حقيقية" لنشرها ؛ لا يمكنك كتابة دالة عادية في برنامج نصي. بالإضافة إلى ذلك ، لا يمكن تخزين البرامج النصية إلا في نظام الملفات ، لاستخدام قاعدة البيانات التي عليك أن تتسلقها داخل Spring. نعم ، ويرى الكثير أن تكوين XML قديم ، خاصة إذا كان التطبيق يحتوي بالفعل على كل شيء في التعليقات التوضيحية. هذا ، بالطبع ، ذو نكهة ، ولكن على المرء في كثير من الأحيان أن يحسب له.
مخطوطات: الصعوبات والأفكار
لذلك ، لكل حل سعره الخاص ، وإذا تحدثنا عن البرامج النصية في تطبيقات Java ، فعند تقديم هذه التقنية ، قد يواجه المرء بعض الصعوبات:
- الإدارة. في كثير من الأحيان ، تتناثر مكالمات البرنامج النصي في جميع أنحاء التطبيق ، ومع التغييرات في الرمز ، من الصعب جدًا تتبع مكالمات البرامج النصية الضرورية.
- القدرة على إيجاد أقران الاتصال. إذا حدث خطأ ما في نص برمجي معين ، فسيكون العثور على جميع
evaluateGroovy()
الهاتفي مشكلة ، إلا إذا قمت بتطبيق بحث حسب اسم الملف أو استدعاءات الطريقة مثل evaluateGroovy()
- الشفافية كتابة السيناريو ليس مهمة سهلة في حد ذاته ، والأكثر صعوبة بالنسبة لأولئك الذين يطلقون على هذا النص. عليك أن تتذكر ما يطلق عليه معلمات الإدخال ، ونوع البيانات التي لديهم وما هي نتيجة التنفيذ. أو انظر إلى شفرة مصدر البرنامج النصي في كل مرة.
- الاختبار والتحديث - ليس من الممكن دائمًا اختبار البرنامج النصي في بيئة رمز التطبيق ، وبعد تحميله إلى خادم "المعركة" ، تحتاج بطريقة ما إلى التراجع بسرعة عن كل شيء إذا حدث خطأ ما.
يبدو أن التفاف مكالمات البرنامج النصي في طرق Java سيساعد في حل معظم المشاكل المذكورة أعلاه. من الجيد جدًا أن يتم نشر هذه الفئات في حاوية IoC وطرق الاتصال بأسماء عادية وذات مغزى في خدماتها ، بدلاً من استدعاء
eval(“disc_10_cl.groovy”)
من بعض فئات المنفعة. ميزة أخرى هي أن الكود يصبح توثيقًا ذاتيًا ، ولا يحتاج المطور إلى لغز حول نوع الخوارزمية المخفية خلف اسم الملف.
بالإضافة إلى ذلك ، إذا كان سيتم ربط كل نص برمجي بطريقة واحدة فقط ، فيمكنك العثور بسرعة على جميع النظراء في التطبيق باستخدام قائمة "Find Usages" من IDE وفهم مكان البرنامج النصي في كل خوارزمية منطق الأعمال المحددة.
الاختبار مبسّط - يتحول إلى اختبار "عادي" في الصف ، باستخدام أطر مألوفة ، وصور وهمية ، والمزيد.
كل ما سبق يتوافق تمامًا مع الفكرة المذكورة في بداية المقالة - فئات "خاصة" للأساليب التي يتم تنفيذها بواسطة النصوص البرمجية. ولكن ماذا لو اتخذت خطوة أخرى وقمت بإخفاء جميع رموز الخدمة من نفس النوع لاستدعاء محركات البرامج النصية من المطور حتى لا يفكر في الأمر (جيدًا تقريبًا)؟
مستودعات السيناريو - المفهوم
الفكرة بسيطة للغاية ويجب أن تكون مألوفة لأولئك الذين عملوا مرة واحدة على الأقل مع Spring ، خاصة مع Spring JPA. ما تحتاجه هو إنشاء واجهة Java واستدعاء البرنامج النصي عند استدعاء أساليبها. في JPA ، بالمناسبة ، يتم استخدام نهج متطابق - يتم اعتراض استدعاء CrudRepository ، بناءً على اسم الطريقة والمعلمات ، يتم إنشاء طلب ، والذي يتم تنفيذه بعد ذلك بواسطة محرك قاعدة البيانات.
ما هو المطلوب لتنفيذ المفهوم؟
أولاً ، تعليق توضيحي على مستوى الفصل بحيث يمكنك العثور على الواجهة - المستودع وإنشاء سلة مستندة إليها.
أيضًا ، من المحتمل أن تكون التعليقات التوضيحية على طرق هذه الواجهة مفيدة من أجل تخزين البيانات الوصفية اللازمة لاستدعاء الطريقة. على سبيل المثال - مكان الحصول على نص البرنامج النصي والمحرك الذي تريد استخدامه.
ستكون الإضافة المفيدة هي القدرة على استخدام الأساليب مع التنفيذ في الواجهة (المعروفة أيضًا باسم الافتراضي) - ستعمل هذه الشفرة حتى يعرض محلل الأعمال نسخة أكثر اكتمالاً من الخوارزمية ، ويقوم المطور بعمل نص برمجي بناءً على
هذه المعلومات. أو دع المحلل يكتب البرنامج النصي ، ثم يقوم المطور ببساطة بنسخه إلى الخادم. هناك العديد من الخيارات :-)
لذا ، افترض أنه بالنسبة لمتجر عبر الإنترنت ، تحتاج إلى تقديم خدمة لحساب الخصومات بناءً على ملف تعريف المستخدم. ليس من الواضح الآن كيفية القيام بذلك ، لكن محلل الأعمال يقسم أن جميع المستخدمين المسجلين يحق لهم الحصول على خصم 10٪ ، وسيكتشف الباقي من العميل في غضون أسبوع. الخدمة مطلوبة غدًا - الموسم بعد كل شيء. كيف يمكن أن يبدو الرمز لهذه الحالة؟
@ScriptRepository public interface PricingRepository { @ScriptMethod default BigDecimal applyCustomerDiscount(Customer customer, BigDecimal orderAmount) { return orderAmount.multiply(new BigDecimal("0.9")); } }
وبعد ذلك ، ستصل الخوارزمية نفسها ، المكتوبة ، على سبيل المثال ، بشكل رائع ، في الوقت المناسب ، وستكون هناك خصومات مختلفة قليلاً:
def age = 50 if ((Calendar.YEAR - customer.birthday.year) >= age) { return orderAmount.multiply(0.75) } else { return orderAmount.multiply(0.9) }
الغرض من كل هذا هو منح المطور القدرة على كتابة رمز الواجهة فقط وشفرة البرنامج النصي ، وعدم
getEngine
مع كل هذه المكالمات للحصول على
getEngine
،
eval
، وغيرها. يجب أن تقوم المكتبة للعمل مع البرامج النصية بكل السحر - اعتراض استدعاء طريقة الواجهة ، والحصول على نص البرنامج النصي ، واستبدال قيم المعلمات ، والحصول على محرك البرنامج النصي المطلوب ، وتنفيذ البرنامج النصي (أو استدعاء الطريقة الافتراضية إذا لم يكن هناك نص برنامج نصي) وإرجاع القيمة. من الناحية المثالية ، بالإضافة إلى الشفرة التي تمت كتابتها بالفعل ، يجب أن يحتوي البرنامج على شيء مثل هذا:
@Service public class CustomerServiceBean implements CustomerService { @Inject private PricingRepository pricingRepository;
التحدي قابل للقراءة ، ومفهوم ، ولجعله لا يحتاج المرء إلى أي مهارات خاصة.
كانت هذه الأفكار التي تم على أساسها إنشاء مكتبة صغيرة للعمل مع النصوص. الغرض منه هو لتطبيقات الربيع ، تم استخدام هذا الإطار لإنشاء المكتبة. يوفر واجهة برمجة تطبيقات قابلة للتوسيع لتحميل البرامج النصية من مصادر مختلفة وتنفيذها ، والتي تخفي العمل الروتيني مع محركات البرامج النصية.
كيف يعمل؟
بالنسبة لجميع الواجهات التي تم وضع علامة
@ScriptRepository
، يتم إنشاء كائنات الوكيل أثناء تهيئة سياق الربيع باستخدام طريقة
newProxyInstance
لفئة
Proxy
. يتم نشر هذه الوكلاء في سياق Spring كحبوب فردية ، بحيث يمكنك تعريف حقل فئة بنوع واجهة ووضع التعليق التوضيحي
@Inject
أو
@Inject
عليه. كما هو مخطط له بالضبط.
يتم تنشيط مسح واجهات واجهات البرامج النصية ومعالجتها باستخدام التعليق التوضيحي
@EnableSriptRepositories
، تمامًا كما هو الحال في Spring أو JPA أو مستودعات
@EnableJpaRepositories
تم تنشيطها (
@EnableJpaRepositories
و
@EnableMongoRepositories
على التوالي). كمعلمات تعليق ، تحتاج إلى تحديد مصفوفة بأسماء الحزم التي تريد مسحها.
@Configuration @EnableScriptRepositories(basePackages = {"com.example", "com.sample"}) public class CoreConfig {
يجب وضع تعليقات توضيحية على الطرق باستخدام
@ScriptMethod
(هناك أيضًا
@GroovyScript
و
@JavaScript
، مع التخصص المقابل) لإضافة بيانات وصفية لاستدعاء البرنامج النصي. بالطبع ، يتم دعم الطرق الافتراضية في الواجهات.
يظهر الهيكل العام للمكتبة في الرسم التخطيطي. مكونات مميزة باللون الأزرق تحتاج إلى تطوير ، بيضاء - موجودة بالفعل في المكتبة. يشير رمز Spring إلى المكونات المتوفرة في سياق Spring.
عندما يتم استدعاء طريقة الواجهة (في الواقع ، كائن الوكيل) ، يتم تشغيل معالج المكالمة ، الذي يبحث في سياق التطبيق عن فاصوليين: الموفر ، الذي سيبحث عن نص البرنامج النصي ، والمنفذ ، والذي ، في الواقع ، سيتم تنفيذ النص الموجود. ثم يعيد المعالج النتيجة إلى أسلوب الاستدعاء.
يتم تحديد أسماء
@ScriptMethod
التعليق التوضيحي
@ScriptMethod
، حيث يمكنك أيضًا تعيين حد على وقت تنفيذ الطريقة. فيما يلي نموذج لرمز استخدام المكتبة:
@ScriptRepository public interface PricingRepository { @ScriptMethod (providerBeanName = "resourceProvider", evaluatorBeanName = "groovyEvaluator", timeout = 100) default BigDecimal applyCustomerDiscount( @ScriptParam("cust") Customer customer, @ScriptParam("amount") BigDecimal orderAmount) { return orderAmount.multiply(new BigDecimal("0.9")); } }
يمكنك ملاحظة التعليقات التوضيحية لـ
@ScriptParam
- فهي مطلوبة للإشارة إلى أسماء المعلمات عند تمريرها إلى البرنامج النصي ، حيث يقوم مترجم Java بمسح الأسماء الأصلية من المصادر (هناك طرق لجعلها لا تفعل ذلك ، ولكن من الأفضل عدم الاعتماد عليها). يمكنك حذف أسماء المعلمات ، ولكن في هذه الحالة ، ستحتاج إلى استخدام "arg0" و "arg1" في البرنامج النصي ، مما لا يحسن بشكل كبير من سهولة القراءة.
بشكل افتراضي ، تحتوي المكتبة على موفرين لقراءة ملفات .groovy و. js من القرص والمنفذين المطابقين ، وهم مغلفون على واجهة برمجة التطبيقات القياسية JSR-233. يمكنك إنشاء الفاصوليا الخاصة بك لمصادر نصية مختلفة ومحركات مختلفة ، لذلك تحتاج إلى تنفيذ الواجهات المقابلة:
ScriptProvider
و
SpringEvaluator
. تستخدم الواجهة الأولى
org.springframework.scripting.ScriptSource
والثانية هي
org.springframework.scripting.ScriptEvaluator
. تم استخدام Spring API بحيث يمكن استخدام الفئات الجاهزة إذا كانت موجودة بالفعل في التطبيق.
يتم البحث عن الموفر والفنان بالاسم لمزيد من المرونة - يمكنك استبدال الفاصوليا القياسية من المكتبة في تطبيقك عن طريق تسمية المكونات الخاصة بك بنفس الأسماء.
الاختبار والإصدار
نظرًا لأن النصوص البرمجية تتغير بشكل متكرر وسهل ، فأنت بحاجة إلى طريقة للتأكد من أن التغييرات لا تكسر أي شيء. المكتبة متوافقة مع JUnit ، يمكن ببساطة اختبار المستودع كفئة عادية كجزء من اختبار الوحدة أو التكامل. يتم دعم مكتبات وهمية أيضًا ، في اختبارات المكتبة يمكنك العثور على مثال لكيفية عمل نسخة وهمية على طريقة مستودع البرامج النصية.
إذا كانت هناك حاجة للإصدار ، فيمكنك حينئذٍ إنشاء مزود لقراءة إصدارات مختلفة من البرامج النصية من نظام الملفات أو من قاعدة البيانات أو من Git ، على سبيل المثال. لذلك سيكون من السهل تنظيم العودة إلى الإصدار السابق من البرنامج النصي في حالة حدوث مشاكل على الخادم الرئيسي.
المجموع
ستساعد المكتبة المقدمة في تنظيم النصوص في تطبيق Spring:
- سيكون لدى المطور دائمًا معلومات حول المعلمات التي تحتاجها النصوص البرمجية وما يتم إرجاعها. وإذا تم تسمية طرق الواجهة بشكل ذي معنى ، فماذا يفعل البرنامج النصي.
- سيساعد الموفرون والمنفذون في الحفاظ على الرمز لتلقي النصوص البرمجية والتفاعل مع محرك النص البرمجي في مكان واحد ولن تتناثر هذه المكالمات في جميع أنحاء رمز التطبيق.
- يمكن العثور على جميع مكالمات البرنامج النصي بسهولة باستخدام Find Usages.
ويدعم التكوين التلقائي التمهيد الربيع ، واختبار الوحدة ، وهمية. يمكنك الحصول على بيانات حول طرق "البرنامج النصي" ومعلماتها من خلال واجهة برمجة التطبيقات. ويمكنك أيضًا لف نتيجة التنفيذ بكائن ScriptResult خاص ، حيث ستكون هناك نتيجة أو نسخة استثنائية إذا كنت لا تريد الإزعاج باستخدام try ... catch عند استدعاء البرامج النصية. يتم دعم تكوين XML إذا كان مطلوبًا لسبب أو لآخر. وأخيرًا - يمكنك تحديد مهلة لأسلوب البرنامج النصي ، إذا دعت الحاجة.
مصادر المكتبة هنا.