تستخدم معظم البرامج التفكير بطريقة أو بأخرى بأشكاله المختلفة ، لأن إمكانياته يصعب وضعها في مقال واحد.
تنتهي العديد من الإجابات عند هذا الحد ، ولكن الأهم هو فهم المفهوم العام للتفكير. نحن نطارد إجابات قصيرة على الأسئلة من أجل اجتياز المقابلة بنجاح ، لكننا لا نفهم الأساسيات - من أين أتت وما المقصود بالتفكير بالضبط.
في هذه المقالة سوف نتطرق إلى كل هذه القضايا فيما يتعلق بالتعليقات التوضيحية ومع مثال حي سوف نرى كيفية استخدام والعثور على وكتابة الخاص بك.
انعكاس
أعتقد أنه سيكون من الخطأ الاعتقاد بأن انعكاس Java يقتصر على حزمة فقط في المكتبة القياسية. لذلك ، أقترح اعتباره مصطلحًا ، دون أن يكون ملزماً بحزمة معينة.
التأمل مقابل التأمل
إلى جانب التفكير ، هناك أيضًا مفهوم الاستبطان. الاستبطان هو قدرة البرنامج على الحصول على بيانات حول نوع الكائن وخصائصه الأخرى. على سبيل المثال ، هذا
instanceof
:
if (obj instanceof Cat) {
Cat cat = (Cat) obj;
cat.meow();
}
هذه تقنية قوية للغاية ، وبدونها لن تكون Java كما هي. ومع ذلك ، فهو لا يذهب إلى أبعد من تلقي البيانات ، ويلعب التفكير.
بعض احتمالات التفكير
وبشكل أكثر تحديدًا ، التفكير هو قدرة البرنامج على فحص نفسه في وقت التشغيل واستخدامه لتغيير سلوكه.
لذلك ، فإن المثال الموضح أعلاه ليس انعكاسًا ، ولكنه مجرد استبطان لنوع الكائن. ولكن ما هو الانعكاس إذن؟ على سبيل المثال ، إنشاء فصل دراسي أو استدعاء طريقة ، ولكن بطريقة غريبة جدًا. يوجد أدناه مثال.
دعنا نتخيل أننا لا نملك أي معرفة عن الفصل الذي نريد تكوينه ، ولكن فقط معلومات حول مكانه. في هذه الحالة ، لا يمكننا إنشاء فصل بالطريقة الواضحة:
Object obj = new Cat(); // ?
دعنا نستخدم الانعكاس وننشئ مثيلًا للفئة:
Object obj = Class.forName("complete.classpath.MyCat").newInstance();
دعنا نسمي طريقته أيضًا من خلال التفكير:
Method m = obj.getClass().getDeclaredMethod("meow");
m.invoke(obj);
من النظرية إلى التطبيق:
import java.lang.reflect.Method;
import java.lang.Class;
public class Cat {
public void meow() {
System.out.println("Meow");
}
public static void main(String[] args) throws Exception {
Object obj = Class.forName("Cat").newInstance();
Method m = obj.getClass().getDeclaredMethod("meow");
m.invoke(obj);
}
}
يمكنك اللعب بها في Jdoodle .
على الرغم من بساطته ، هناك الكثير من الأشياء المعقدة التي تحدث في هذا الكود ، وغالبًا ما يفتقر المبرمج إلى الاستخدام البسيط
getDeclaredMethod and then invoke
.
السؤال رقم 1
لماذا ، في طريقة الاستدعاء في المثال أعلاه ، يجب أن نجتاز مثيل كائن؟
لن أذهب أبعد من ذلك ، لأننا سنبتعد عن الموضوع. بدلاً من ذلك ، سأترك رابطًا لمقال بقلم الزميل الكبير تاجير فالييف .
شروح
التعليقات التوضيحية جزء مهم من لغة جافا. هذا نوع من الواصف الذي يمكن تعليقه في فئة أو حقل أو طريقة. على سبيل المثال ، ربما شاهدت التعليق التوضيحي
@Override
:
public abstract class Animal {
abstract void doSomething();
}
public class Cat extends Animal {
@Override
public void doSomething() {
System.out.println("Meow");
}
}
هل تساءلت يوما كيف يعمل؟ إذا كنت لا تعرف ، فقبل قراءة المزيد ، حاول التخمين.
أنواع التعليقات التوضيحية
ضع في اعتبارك التعليق التوضيحي أعلاه:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@Target
- يشير إلى ما ينطبق التعليق التوضيحي عليه. في هذه الحالة ، الطريقة.
@Retention
- عمر التعليق التوضيحي في الكود (ليس بالثواني بالطبع).
@interface
- هي صيغة إنشاء التعليقات التوضيحية.
إذا كانت الأولى والأخيرة واضحة إلى حد ما (انظر.
@Target
في الوثائق ) ،
@Retention
فلنلق نظرة الآن ، حيث سيتم تقسيمها إلى عدة أنواع من التعليقات التوضيحية ، وهو أمر مهم جدًا لفهمه.
يمكن أن يأخذ هذا التعليق التوضيحي ثلاث قيم:
في الحالة الأولى ، ستتم كتابة التعليق التوضيحي في الرمز الثانوي للكود الخاص بك ، ولكن لا ينبغي استمراره بواسطة الجهاز الظاهري في وقت التشغيل.
في الحالة الثانية ، سيكون التعليق التوضيحي متاحًا في وقت التشغيل ، وبفضله يمكننا معالجته ، على سبيل المثال ، الحصول على جميع الفئات التي تحتوي على هذا التعليق التوضيحي.
في الحالة الثالثة ، سيتم إزالة التعليق التوضيحي بواسطة المترجم (لن يكون في الرمز الثانوي). عادة ما تكون هذه التعليقات التوضيحية مفيدة فقط للمترجم.
بالعودة إلى التعليق التوضيحي
@Override
، نرى أنه يحتوي ،
RetentionPolicy.SOURCE
وهو أمر منطقي عمومًا ، نظرًا لأنه يستخدم فقط من قبل المترجم. في وقت التشغيل ، لا يقدم هذا التعليق التوضيحي أي شيء مفيد.
القط الخارق
دعنا نحاول إضافة التعليق التوضيحي الخاص بنا (سيكون هذا مفيدًا لنا أثناء التطوير).
abstract class Cat {
abstract void meow();
}
public class Home {
private class Tom extends Cat {
@Override
void meow() {
System.out.println("Tom-style meow!"); // <---
}
}
private class Alex extends Cat {
@Override
void meow() {
System.out.println("Alex-style meow!"); // <---
}
}
}
دعونا نمتلك قطتين في منزلنا: توم وأليكس. لنقم بإنشاء تعليق توضيحي للقطط الخارقة:
@Target(ElementType.TYPE) //
@Retention(RetentionPolicy.RUNTIME) //
@interface SuperCat {
}
// ...
@SuperCat // <---
private class Alex extends Cat {
@Override
void meow() {
System.out.println("Alex-style meow!");
}
}
// ...
في نفس الوقت سنترك توم كقط عادي (العالم غير عادل). الآن دعنا نحاول الحصول على الفئات التي تم شرحها باستخدام هذا العنصر. سيكون من الجيد أن يكون لديك طريقة مثل هذه في فئة التعليقات التوضيحية نفسها:
Set<class<?>> classes = SuperCat.class.getAnnotatedClasses();
لكن ، للأسف ، لا توجد مثل هذه الطريقة حتى الآن. ثم كيف نجد هذه الفصول؟
كلاسباث
هذه معلمة تشير إلى الفئات المخصصة.
أتمنى أن تكون على دراية بهم ، وإذا لم يكن الأمر كذلك ، فأسرع لدراستها ، لأن هذا أحد الأشياء الأساسية.
لذلك ، بعد أن اكتشفنا مكان تخزين الفصول الدراسية لدينا ، يمكننا تحميلها من خلال ClassLoader والتحقق من الفئات الخاصة بهذا التعليق التوضيحي. دعنا ننتقل مباشرة إلى الكود:
public static void main(String[] args) throws ClassNotFoundException {
String packageName = "com.apploidxxx.examples";
ClassLoader classLoader = Home.class.getClassLoader();
String packagePath = packageName.replace('.', '/');
URL urls = classLoader.getResource(packagePath);
File folder = new File(urls.getPath());
File[] classes = folder.listFiles();
for (File aClass : classes) {
int index = aClass.getName().indexOf(".");
String className = aClass.getName().substring(0, index);
String classNamePath = packageName + "." + className;
Class<?> repoClass = Class.forName(classNamePath);
Annotation[] annotations = repoClass.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation.annotationType() == SuperCat.class) {
System.out.println(
"Detected SuperCat!!! It is " + repoClass.getName()
);
}
}
}
}
لا أوصي باستخدام هذا في برنامجك. الكود لأغراض إعلامية فقط!
هذا المثال إرشادي ، ولكنه يستخدم فقط للأغراض التعليمية بسبب هذا:
Class<?> repoClass = Class.forName(classNamePath);
سنكتشف السبب لاحقًا. الآن ، دعنا نلقي نظرة على الأسطر من الأعلى:
// ...
//
String packageName = "com.apploidxxx.examples";
// , -
ClassLoader classLoader = Home.class.getClassLoader();
// com.apploidxxx.examples -> com/apploidxxx/examples
String packagePath = packageName.replace('.', '/');
URL urls = classLoader.getResource(packagePath);
File folder = new File(urls.getPath());
//
File[] classes = folder.listFiles();
// ...
لمعرفة من أين نحصل على هذه الملفات ، دعنا ننظر إلى أرشيف JAR الذي تم إنشاؤه عند تشغيل التطبيق:
├───com │ └───apploidxxx │ └───examples │ Cat.class │ Home$Alex.class │ Home$Tom.class │ Home.class │ Main.class │ SuperCat.class
وبالتالي ،
classes
فهذه ليست سوى ملفاتنا المجمعة كرمز ثانوي. ومع ذلك ،
File
هذا ليس ملفًا تم تنزيله بعد ، فنحن نعرف فقط مكانهم ، لكننا ما زلنا لا نستطيع رؤية ما بداخلهم.
لذلك دعونا نحمل كل ملف:
for (File aClass : classes) {
// , , Home.class, Home$Alex.class
// .class
// Java
int index = aClass.getName().indexOf(".");
String className = aClass.getName().substring(0, index);
String classNamePath = packageName + "." + className;
// classNamePath = com.apploidxxx.examples.Home
Class<?> repoClass = Class.forName(classNamePath);
}
كل ما تم القيام به من قبل كان فقط لاستدعاء هذه الطريقة Class.forName ، والتي ستحمّل الفصل الذي نحتاجه. لذا فإن الجزء الأخير هو الحصول على جميع التعليقات التوضيحية المستخدمة في repoClass ثم التحقق مما إذا كانت تعليقات توضيحية
@SuperCat
:
Annotation[] annotations = repoClass.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation.annotationType() == SuperCat.class) {
System.out.println(
"Detected SuperCat!!! It is " + repoClass.getName()
);
}
}
output: Detected SuperCat!!! It is com.apploidxxx.examples.Home$Alex
إنتهيت! الآن بعد أن أصبح لدينا الفصل نفسه ، يمكننا الوصول إلى جميع طرق التفكير.
تعكس
كما في المثال أعلاه ، يمكننا ببساطة إنشاء مثيل جديد لفصلنا. لكن قبل ذلك ، دعونا نلقي نظرة على بعض الإجراءات الشكلية.
- أولاً ، تحتاج القطط إلى العيش في مكان ما ، لذا فهي بحاجة إلى منزل. في حالتنا ، لا يمكن أن توجد بدون منزل.
- ثانيًا ، سننشئ قائمة بالمعاطف الفائقة.
List<cat> superCats = new ArrayList<>();
final Home home = new Home(); // ,
لذلك ، تأخذ المعالجة شكلها النهائي:
for (Annotation annotation : annotations) {
if (annotation.annotationType() == SuperCat.class) {
Object obj = repoClass
.getDeclaredConstructor(Home.class)
.newInstance(home);
superCats.add((Cat) obj);
}
}
ومرة أخرى عنوان الأسئلة:
السؤال رقم 2
ماذا يحدث إذا وضعنا علامة على@SuperCat
فئة لا ترث منهاCat
؟
السؤال رقم 3
لماذا نحتاج إلى منشئ يأخذ نوع وسيطةHome
؟
فكر لبضع دقائق ، ثم قم بتحليل الإجابات على الفور:
الإجابة رقم 2 : نعم
ClassCastException
، لأن التعليق التوضيحي نفسه
@SuperCat
لا يضمن أن الفصل الذي تم تمييزه بهذا التعليق التوضيحي سيرث شيئًا ما أو ينفذه.
يمكنك التحقق من ذلك عن طريق الإزالة
extends Cat
من Alex. في نفس الوقت ، سترى كيف يمكن أن تكون التعليقات التوضيحية مفيدة
@Override
.
الإجابة رقم 3 : القطط تحتاج إلى منزل لأنها فصول داخلية. كل شيء في إطار الفصل 15.9.3 من مواصفات لغة Java .
ومع ذلك ، يمكنك تجنب ذلك ببساطة عن طريق جعل هذه الفئات ثابتة. ولكن عند العمل مع التفكير ، غالبًا ما تصادف هذا النوع من الأشياء. ولا تحتاج حقًا إلى معرفة مواصفات Java بدقة لذلك. هذه الأشياء منطقية بما فيه الكفاية ، ويمكنك أن تكتشف بنفسك لماذا يجب أن نمرر مثيلًا للفئة الأم إلى المنشئ ، إذا كان كذلك
non-static
.
دعونا نلخص ونحصل على: Home.java
package com.apploidxxx.examples;
import java.io.File;
import java.lang.annotation.*;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface SuperCat {
}
abstract class Cat {
abstract void meow();
}
public class Home {
public class Tom extends Cat {
@Override
void meow() {
System.out.println("Tom-style meow!");
}
}
@SuperCat
public class Alex extends Cat {
@Override
void meow() {
System.out.println("Alex-style meow!");
}
}
public static void main(String[] args) throws Exception {
String packageName = "com.apploidxxx.examples";
ClassLoader classLoader = Home.class.getClassLoader();
String packagePath = packageName.replace('.', '/');
URL urls = classLoader.getResource(packagePath);
File folder = new File(urls.getPath());
File[] classes = folder.listFiles();
List<Cat> superCats = new ArrayList<>();
final Home home = new Home();
for (File aClass : classes) {
int index = aClass.getName().indexOf(".");
String className = aClass.getName().substring(0, index);
String classNamePath = packageName + "." + className;
Class<?> repoClass = Class.forName(classNamePath);
Annotation[] annotations = repoClass.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation.annotationType() == SuperCat.class) {
Object obj = repoClass
.getDeclaredConstructor(Home.class)
.newInstance(home);
superCats.add((Cat) obj);
}
}
}
superCats.forEach(Cat::meow);
}
}
output: Alex-style meow!
إذن ما هو الخطأ
Class.forName
؟
هو نفسه يفعل بالضبط ما هو مطلوب منه. ومع ذلك ، نحن نستخدمها بشكل غير صحيح.
تخيل أنك تعمل في مشاريع مع 1000 فئة أو أكثر (بعد كل شيء ، نكتب بلغة Java). وتخيل تحميل كل فصل تجده في classPath. أنت نفسك تدرك أن الذاكرة وموارد JVM الأخرى ليست مطاطًا.
طرق التعامل مع التعليقات التوضيحية
إذا لم تكن هناك طريقة أخرى للتعامل مع التعليقات التوضيحية ، فإن استخدامها كعناوين للفصل ، كما في فصل الربيع على سبيل المثال ، سيكون أمرًا مثيرًا للجدل للغاية.
ولكن لا يزال يبدو أن الربيع يعمل. هل برنامجي بطيء جدًا بسببهم؟ لسوء الحظ أو لحسن الحظ ، لا. الربيع يعمل بشكل جيد (في هذا الصدد) لأنه يستخدم طريقة مختلفة قليلاً للعمل معهم.
مباشرة إلى الرمز الثانوي
كل شخص (آمل) لديه فكرة بطريقة ما عن ماهية الرمز الثانوي. يخزن جميع المعلومات حول فصولنا وبياناتها الوصفية (بما في ذلك التعليقات التوضيحية).
حان الوقت لتذكر بلدنا
RetentionPolicy
. في المثال السابق ، تمكنا من العثور على هذا التعليق التوضيحي لأننا أشرنا إلى أنه تعليق توضيحي لوقت التشغيل. لذلك ، يجب تخزينه في رمز بايت.
فلماذا لا نقرأها فقط (نعم ، من الرمز الثانوي)؟ لكن هنا لن أقوم بتطبيق برنامج لقراءته من الرمز الثانوي ، لأنه يستحق مقالة منفصلة. ومع ذلك ، يمكنك القيام بذلك بنفسك - ستكون ممارسة رائعة من شأنها دمج مادة المقالة.
للتعرف على الرمز الثنائي ، يمكنك البدء بمقالتي... هناك أصف الأشياء الأساسية في الرمز الثانوي مع Hello World! ستكون المقالة مفيدة حتى إذا كنت لن تعمل مباشرة مع الرمز الثانوي. يصف النقاط الأساسية التي ستساعد في الإجابة على السؤال: لماذا بالضبط؟
بعد ذلك ، مرحبًا بك في مواصفات JVM الرسمية . إذا كنت لا ترغب في تحليل الرمز الثانوي يدويًا (بالبايت) ، فابحث عن مكتبات مثل ASM و Javassist .
خواطر
Reflections هي مكتبة تحمل ترخيص WTFPL الذي يسمح لك بفعل ما تريد به. مكتبة سريعة إلى حد ما لمختلف الأعمال مع Classpath والبيانات الوصفية. الشيء المفيد هو أنه يمكنه حفظ معلومات حول بعض البيانات التي تمت قراءتها بالفعل ، مما يوفر الوقت. يمكنك الحفر بالداخل والعثور على فئة المتجر.
package com.apploidxxx.examples;
import org.reflections.Reflections;
import java.lang.reflect.InvocationTargetException;
import java.util.Optional;
import java.util.Set;
public class ExampleReflections {
private static final Home HOME = new Home();
public static void main(String[] args) {
Reflections reflections = new Reflections("com.apploidxxx.examples");
Set<Class<?>> superCats = reflections
.getTypesAnnotatedWith(SuperCat.class);
for (Class<?> clazz : superCats) {
toCat(clazz).ifPresent(Cat::meow);
}
}
private static Optional<Cat> toCat(Class<?> clazz) {
try {
return Optional.of((Cat) clazz
.getDeclaredConstructor(Home.class)
.newInstance(HOME)
);
} catch (InstantiationException |
IllegalAccessException |
InvocationTargetException |
NoSuchMethodException e)
{
e.printStackTrace();
return Optional.empty();
}
}
}
سياق الربيع
أوصي باستخدام مكتبة Reflections ، حيث إنها تعمل داخليًا من خلال javassist ، مما يشير إلى أنها تقرأ الرمز الثانوي بدلاً من تحميلها.
ومع ذلك ، هناك العديد من المكتبات الأخرى التي تعمل بطريقة مماثلة. هناك الكثير منهم ، لكن الآن أريد تفكيك واحد منهم فقط - هذا
spring-context
. ربما يكون أفضل من الأول عندما تقوم بتطوير روبوت في إطار الربيع. ولكن هناك أيضًا بعض الفروق الدقيقة هنا.
إذا كانت فصولك الدراسية عبارة عن حبوب مُدارة بشكل أساسي ، أي أنها في حاوية Spring ، فلن تحتاج إلى إعادة مسحها ضوئيًا. يمكنك ببساطة الوصول إلى هذه الحبوب من الحاوية نفسها.
شيء آخر هو إذا كنت تريد أن تكون الفصول التي تم وضع علامة عليها عبارة عن حبوب ، فيمكنك القيام بذلك يدويًا من خلال
ClassPathScanningCandidateComponentProvider
ذلك يعمل من خلال ASM.
مرة أخرى ، من النادر جدًا أن تحتاج إلى استخدام هذه الطريقة ، لكن الأمر يستحق النظر فيه كخيار.
لقد كتبت روبوتًا لـ VK عليه. هذا مستودع يمكنك التعرف عليه ، لكنني كتبته منذ وقت طويل ، وعندما ذهبت للبحث عن إدراج رابط في المقالة ، رأيت أنه من خلال VK-Java-SDK أتلقى رسائل بحقول غير مهيأة ، على الرغم من أن كل شيء يعمل من قبل.
الشيء المضحك هو أنني لم أغير إصدار SDK حتى ، لذا إذا وجدت السبب ، سأكون ممتنًا. ومع ذلك ، فإن تحميل الأوامر نفسها يعمل بشكل جيد ، وهو بالضبط ما يمكنك النظر إليه إذا كنت تريد رؤية مثال على العمل معه
spring-context
.
الأوامر فيه كالتالي:
@Command(value = "hello", aliases = {"", ""})
public class Hello implements Executable {
public BotResponse execute(Message message) throws Exception {
return BotResponseFactoryUtil.createResponse("hello-hello",
message.peerId);
}
}
SuperCat
يمكنك العثور على أمثلة التعليمات البرمجية المشروحة في هذا المستودع .
تطبيق عملي للتعليقات التوضيحية في إنشاء روبوت Telegram
كانت هذه مقدمة طويلة إلى حد ما ولكنها ضرورية للعمل مع التعليقات التوضيحية. بعد ذلك ، سنقوم بتنفيذ روبوت ، لكن الغرض من المقالة ليس دليلًا لإنشائه. هذا هو تطبيق عملي للتعليقات التوضيحية. يمكن أن يكون هناك أي شيء هنا: من تطبيقات وحدة التحكم إلى نفس برامج الروبوت لـ VK والعربة وأشياء أخرى.
أيضًا ، لن يتم إجراء بعض الفحوصات المعقدة عمدًا هنا. على سبيل المثال ، قبل ذلك ، لم يكن لدى الأمثلة أي عمليات تحقق من معالجة الأخطاء الفارغة أو الصحيحة ، ناهيك عن تسجيلها.
يتم كل هذا لتبسيط الكود. لذلك ، إذا أخذت الكود من الأمثلة ، فلا تكن كسولًا لتعديله ، لذلك ستفهمه بشكل أفضل وتخصصه ليناسب احتياجاتك.
سنستخدم مكتبة TelegramBots بترخيص من معهد ماساتشوستس للتكنولوجياللعمل مع Telegram API. يمكنك استخدام أي شيء آخر. اخترته لأنه يمكن أن يعمل على حد سواء "c" (له إصدار مع بداية) أو "بدون" حذاء زنبركي.
في الواقع ، لا أريد تعقيد الكود إما عن طريق إضافة نوع من التجريد ، إذا أردت ، يمكنك فعل شيء عالمي ، لكن فكر فيما إذا كان الأمر يستحق ذلك ، لذلك سنستخدم غالبًا في هذه المقالة فئات ملموسة من هذه المكتبات ، رمز لهم.
خواطر
أول روبوت في السطر هو روبوت مكتوب في مكتبة الانعكاسات ، بدون ربيع. لن نقوم بتحليل كل شيء ، ولكن فقط النقاط الرئيسية ، على وجه الخصوص ، نحن مهتمون بمعالجة التعليقات التوضيحية. قبل تحليله في المقالة ، يمكنك أن تكتشف بنفسك كيف يعمل في مستودعي .
في جميع الأمثلة ، سوف نلتزم بحقيقة أن الروبوت يتكون من عدة أوامر ، ولن نقوم بتحميل هذه الأوامر يدويًا ، ولكن نقوم ببساطة بإضافة التعليقات التوضيحية. إليك مثال على الأمر:
@Handler("/hello")
public class HelloHandler implements RequestHandler {
private static final Logger log = LoggerFactory
.getLogger(HelloHandler.class);
@Override
public SendMessage execute(Message message) {
log.info("Executing message from : " + message.getText());
return SendMessage.builder()
.text("Yaks")
.chatId(String.valueOf(message.getChatId()))
.build();
}
}
@Retention(RetentionPolicy.RUNTIME)
public @interface Handler {
String value();
}
في هذه الحالة ،
/hello
ستتم كتابة المعلمة
value
في التعليق التوضيحي. القيمة شيء مثل التعليق التوضيحي الافتراضي. هذا هو
@Handler("/hello")
=
@Handler(value = "/hello")
.
سنقوم أيضا بإضافة قطع الأشجار. سنتصل بهم إما قبل معالجة الطلب أو بعده ، وندمجهم أيضًا:
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value() default ".*"; // regex
ExecutionTime[] executionTime() default ExecutionTime.BEFORE;
}
default` , , `value
@Log
public class LogHandler implements RequestLogger {
private static final Logger log = LoggerFactory
.getLogger(LogHandler.class);
@Override
public void execute(Message message) {
log.info("Just log a received message : " + message.getText());
}
}
ولكن يمكننا أيضًا إضافة معلمة لتشغيل المسجل لرسائل معينة:
@Log(value = "/hello")
public class HelloLogHandler implements RequestLogger {
public static final Logger log = LoggerFactory
.getLogger(HelloLogHandler.class);
@Override
public void execute(Message message) {
log.info("Received special hello command!");
}
}
أو يتم تشغيله بعد معالجة الطلب:
@Log(executionTime = ExecutionTime.AFTER)
public class AfterLogHandler implements RequestLogger {
private static final Logger log = LoggerFactory
.getLogger(AfterLogHandler.class);
@Override
public void executeAfter(Message message, SendMessage sendMessage) {
log.info("Bot response >> " + sendMessage.getText());
}
}
أو كلاهما هناك وهناك:
@Log(executionTime = {ExecutionTime.AFTER, ExecutionTime.BEFORE})
public class AfterAndBeforeLogger implements RequestLogger {
private static final Logger log = LoggerFactory
.getLogger(AfterAndBeforeLogger.class);
@Override
public void execute(Message message) {
log.info("Before execute");
}
@Override
public void executeAfter(Message message, SendMessage sendMessage) {
log.info("After execute");
}
}
يمكننا القيام بذلك لأنه
executionTime
يأخذ مجموعة من القيم. مبدأ التشغيل بسيط ، لذا فلنبدأ في معالجة هذه التعليقات التوضيحية:
Set<Class<?>> annotatedCommands =
reflections.getTypesAnnotatedWith(Handler.class);
final Map<String, RequestHandler> commandsMap = new HashMap<>();
final Class<RequestHandler> requiredInterface = RequestHandler.class;
for (Class<?> clazz : annotatedCommands) {
if (LoaderUtils.isImplementedInterface(clazz, requiredInterface)) {
for (Constructor<?> c : clazz.getDeclaredConstructors()) {
//noinspection unchecked
Constructor<RequestHandler> castedConstructor =
(Constructor<RequestHandler>) c;
commandsMap.put(extractCommandName(clazz),
OBJECT_CREATOR.instantiateClass(castedConstructor));
}
} else {
log.warn("Command didn't implemented: "
+ requiredInterface.getCanonicalName());
}
}
// ...
private static String extractCommandName(Class<?> clazz) {
Handler handler = clazz.getAnnotation(Handler.class);
if (handler == null) {
throw new
IllegalArgumentException(
"Passed class without Handler annotation"
);
} else {
return handler.value();
}
}
في الواقع ، نقوم فقط بإنشاء خريطة باسم الأمر ، والتي نأخذها من القيمة
value
الموجودة في التعليق التوضيحي. شفرة المصدر هنا .
نفعل الشيء نفسه مع Log ، فقط يمكن أن يكون هناك العديد من المسجلين بنفس الأنماط ، لذلك نقوم بتغيير هيكل البيانات لدينا بشكل طفيف:
Set<Class<?>> annotatedLoggers = reflections.getTypesAnnotatedWith(Log.class);
final Map<String, Set<RequestLogger>> commandsMap = new HashMap<>();
final Class<RequestLogger> requiredInterface = RequestLogger.class;
for (Class<?> clazz : annotatedLoggers) {
if (LoaderUtils.isImplementedInterface(clazz, requiredInterface)) {
for (Constructor<?> c : clazz.getDeclaredConstructors()) {
//noinspection unchecked
Constructor<RequestLogger> castedConstructor =
(Constructor<RequestLogger>) c;
String name = extractCommandName(clazz);
commandsMap.computeIfAbsent(name, n -> new HashSet<>());
commandsMap
.get(extractCommandName(clazz))
.add(OBJECT_CREATOR.instantiateClass(castedConstructor));
}
} else {
log.warn("Command didn't implemented: "
+ requiredInterface.getCanonicalName());
}
}
هناك العديد من المسجلات لكل نمط. والباقي هو نفسه.
الآن ، في الروبوت نفسه ، نحتاج إلى تكوين
executionTime
الطلبات وإعادة توجيهها إلى هذه الفئات:
public final class CommandService {
private static final Map<String, RequestHandler> commandsMap
= new HashMap<>();
private static final Map<String, Set<RequestLogger>> loggersMap
= new HashMap<>();
private CommandService() {
}
public static synchronized void init() {
initCommands();
initLoggers();
}
private static void initCommands() {
commandsMap.putAll(CommandLoader.readCommands());
}
private static void initLoggers() {
loggersMap.putAll(LogLoader.loadLoggers());
}
public static RequestHandler serve(String message) {
for (Map.Entry<String, RequestHandler> entry : commandsMap.entrySet()) {
if (entry.getKey().equals(message)) {
return entry.getValue();
}
}
return msg -> SendMessage.builder()
.text(" ")
.chatId(String.valueOf(msg.getChatId()))
.build();
}
public static Set<RequestLogger> findLoggers(
String message,
ExecutionTime executionTime
) {
final Set<RequestLogger> matchedLoggers = new HashSet<>();
for (Map.Entry<String, Set<RequestLogger>> entry:loggersMap.entrySet()) {
for (RequestLogger logger : entry.getValue()) {
if (containsExecutionTime(
extractExecutionTimes(logger), executionTime
))
{
if (message.matches(entry.getKey()))
matchedLoggers.add(logger);
}
}
}
return matchedLoggers;
}
private static ExecutionTime[] extractExecutionTimes(RequestLogger logger) {
return logger.getClass().getAnnotation(Log.class).executionTime();
}
private static boolean containsExecutionTime(
ExecutionTime[] times,
ExecutionTime executionTime
) {
for (ExecutionTime et : times) {
if (et == executionTime) return true;
}
return false;
}
}
public class DefaultBot extends TelegramLongPollingBot {
private static final Logger log = LoggerFactory.getLogger(DefaultBot.class);
public DefaultBot() {
CommandService.init();
log.info("Bot initialized!");
}
@Override
public String getBotUsername() {
return System.getenv("BOT_NAME");
}
@Override
public String getBotToken() {
return System.getenv("BOT_TOKEN");
}
@Override
public void onUpdateReceived(Update update) {
try {
Message message = update.getMessage();
if (message != null && message.hasText()) {
// run "before" loggers
CommandService
.findLoggers(message.getText(), ExecutionTime.BEFORE)
.forEach(logger -> logger.execute(message));
// command execution
SendMessage response;
this.execute(response = CommandService
.serve(message.getText())
.execute(message));
// run "after" loggers
CommandService
.findLoggers(message.getText(), ExecutionTime.AFTER)
.forEach(logger -> logger.executeAfter(message, response));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
من الأفضل اكتشاف الكود بنفسك والبحث في المستودع ، أو حتى فتحه بشكل أفضل من خلال IDE. يعد هذا المستودع جيدًا للبدء والبدء ، ولكنه ليس جيدًا بما يكفي كروبوت.
أولاً ، لا يوجد تجريد كافٍ بين الفرق. أي أنه يمكنك فقط العودة من كل أمر
SendMessage
. يمكن التغلب على هذا باستخدام مستوى أعلى من التجريد ، على سبيل المثال
BotApiMethodMessage
، ولكن حتى هذا لا يحل بالفعل جميع المشكلات.
ثانيًا ، المكتبة نفسها
TelegramBots
، كما يبدو لي ، لا تركز بشكل خاص على مثل هذا العمل (الهندسة المعمارية) للروبوت. إذا كنت تقوم بتطوير روبوت باستخدام هذه المكتبة المعينة ، فيمكنك استخدام
Ability Bot
والتي تم سردها في ويكي المكتبة نفسها. لكنني أريد حقًا أن أرى مكتبة كاملة بمثل هذه الهندسة المعمارية. حتى تتمكن من البدء في كتابة مكتبتك!
الربيع بوت
يكون هذا أكثر منطقية عند العمل مع نظام الربيع البيئي:
- العمل من خلال التعليقات التوضيحية لا ينتهك المفهوم العام للحاوية الزنبركية.
- لا يمكننا إنشاء أوامر بأنفسنا ، ولكننا نحصل عليها من الحاوية ، ونضع علامة على أوامرنا على أنها حبوب.
- نحصل على DI ممتاز من الربيع.
بشكل عام ، استخدام الربيع كإطار عمل للروبوت هو موضوع لمحادثة أخرى. بعد كل شيء ، قد يعتقد الكثيرون أن هذا صعب للغاية بالنسبة للروبوت (على الرغم من أنهم على الأرجح لا يكتبون الروبوتات في Java أيضًا).
لكنني أعتقد أن الربيع هو بيئة جيدة ليس فقط لتطبيقات المؤسسة / الويب. إنه يحتوي فقط على الكثير من المكتبات الرسمية والمخصصة لنظامه البيئي (بحلول الربيع ، أعني Spring Boot).
والأهم من ذلك ، أنه يسمح لك بتنفيذ الكثير من الأنماط بطرق مختلفة توفرها الحاوية.
التنفيذ
حسنًا ، دعنا ننتقل إلى الروبوت نفسه.
نظرًا لأننا نكتب على مجموعة الربيع ، فلا يمكننا إنشاء حاوية الأوامر الخاصة بنا ، ولكننا نستخدم الحاوية الموجودة في الربيع. لا يمكن مسحها ضوئيًا ، ولكن يتم الحصول عليها من حاوية IoC .
يمكن للمطورين الأكثر استقلالية البدء في قراءة التعليمات البرمجية على الفور .
سأقوم هنا بتحليل أوامر القراءة فقط ، على الرغم من وجود بضع نقاط مثيرة للاهتمام في المستودع نفسه يمكنك التفكير فيها بنفسك.
التنفيذ مشابه جدًا للروبوت من خلال Reflections ، لذا فإن التعليقات التوضيحية هي نفسها.
ObjectLoader.java
@Service
public class ObjectLoader {
private final ApplicationContext applicationContext;
public ObjectLoader(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
public Collection<Object> loadObjectsWithAnnotation(
Class<? extends Annotation> annotation
) {
return applicationContext.getBeansWithAnnotation(annotation).values();
}
}
CommandLoader.java
public Map<String, RequestHandler> readCommands() { final Map<String, RequestHandler> commandsMap = new HashMap<>(); for (Object obj : objectLoader.loadObjectsWithAnnotation(Handler.class)) { if (obj instanceof RequestHandler) { RequestHandler handler = (RequestHandler) obj; commandsMap.put(extractCommandName(handler.getClass()), handler); } } return commandsMap; }
على عكس المثال السابق ، يستخدم هذا بالفعل مستوى أعلى من التجريد للواجهات ، وهو بالطبع جيد. لا نحتاج أيضًا إلى إنشاء حالات أوامر بأنفسنا.
دعونا نلخص
الأمر متروك لك لتقرر ما هو الأفضل لمهمتك. لقد قمت بتحليل ثلاث حالات لروبوتات مشابهة تقريبًا:
- خواطر.
- سياق الربيع (لا ربيع).
- ApplicationContext من الربيع.
ومع ذلك ، يمكنني أن أقدم لك النصيحة بناءً على خبرتي:
- ضع في اعتبارك ما إذا كنت بحاجة إلى الربيع. إنه يوفر حاوية IoC قوية وقدرات النظام البيئي ، لكن كل شيء يأتي بسعر. عادة ما أفكر على هذا النحو: إذا كنت بحاجة إلى قاعدة بيانات وبداية سريعة ، فأنت بحاجة إلى Spring Boot. إذا كان الروبوت بسيطًا بدرجة كافية ، فيمكنك الاستغناء عنه.
- إذا لم تكن بحاجة إلى تبعيات معقدة ، فلا تتردد في استخدام انعكاسات.
على سبيل المثال ، يبدو أن تنفيذ JPA بدون Spring Data مهمة تستغرق وقتًا طويلاً إلى حد ما ، على الرغم من أنه يمكنك أيضًا البحث عن بدائل في شكل micronaut أو quarkus ، لكنني سمعت عنها فقط وليس لدي خبرة كافية لتقديم المشورة بشأن هذا الأمر.
إذا كنت ملتزمًا بنهج أنظف من البداية ، حتى بدون JPA ، فابحث عن هذا الروبوت ، الذي يعمل من خلال JDBC عبر VK و Telegram.
هناك سترى العديد من إدخالات النموذج:
PreparedStatement stmt = connection.prepareStatement("UPDATE alias SET aliases=?::jsonb WHERE vkid=?");
stmt.setString(1, aliases.toJSON());
stmt.setInt(2, vkid);
stmt.execute();
لكن الكود يبلغ من العمر عامين ، لذلك لا أنصح بأخذ كل الأنماط من هناك. وبشكل عام ، لا أوصي بالقيام بذلك على الإطلاق (العمل من خلال JDBC).
شخصيًا أيضًا ، لا أحب العمل مباشرة مع Hibernate. لقد مررت بالفعل بتجربة حزينة في الكتابة
DAO
و
HibernateSessionFactoryUtil
(أولئك الذين كتبوا سيفهمون ما أعنيه).
أما بالنسبة للمقال نفسه ، فقد حاولت أن أجعله قصيرًا ، لكن يكفي حتى تتمكن من البدء في التطوير مع وجود هذا المقال فقط. ومع ذلك ، هذا ليس فصلاً في الكتاب ، ولكنه مقال عن حبري. يمكنك معرفة المزيد حول التعليقات التوضيحية والتفكير بشكل عام بنفسك ، على سبيل المثال ، من خلال إنشاء نفس الروبوت.
حظا موفقا للجميع! ولا تنسى كود الترويجي HABR ، والذي يمنحك خصمًا إضافيًا بنسبة 10٪ على الرمز المشار إليه على اللافتة.