التعليقات التوضيحية لوقت الترجمة باستخدامImplement كمثال



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

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

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

بيان المشكلة


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

الآن سنتحدث عن تحسين إمكانية قراءة التعليمات البرمجية أكثر من إصلاح الأخطاء. كمثال ، يمكن للمرء أن يقول ، من الحياة ، أو بالأحرى من مشروع هوايتي.

افترض أن هناك فئة UnitManager ، وهي في الواقع مجموعة من الوحدات. لديها طرق لإضافة وحدة أو حذفها أو الحصول عليها ، إلخ. عند إضافة وحدة جديدة ، يعينها المدير معرفًا. يتم تفويض توليد المعرف لفئة RotateCounter ، والتي تقوم بإرجاع رقم في النطاق المحدد. وهناك مشكلة صغيرة ، لا يمكن لـ RotateCounter معرفة ما إذا كان المعرّف المحدد مجانيًا. وفقًا لمبدأ انعكاس التبعية ، يمكنك إنشاء واجهة ، في حالتي هي RotateCounter.IClient ، التي تحتوي على طريقة واحدة isValueFree () ، والتي تتلقى معرفًا وترجع true إذا كان id مجانيًا. وتقوم UnitManager بتنفيذ هذه الواجهة ، وإنشاء مثيل لـ RotateCounter وتمريره إلى نفسه كعميل.

فعلت ذلك فقط. ولكن ، بعد أن فتحت مصدر UnitManager بعد أيام قليلة من الكتابة ، دخلت في ذهول سهل بعد رؤية طريقة isValueFree () ، والتي لم تتناسب تمامًا مع منطق UnitManager. سيكون من الأسهل بكثير إذا كان من الممكن تحديد الواجهة التي تنفذ هذه الطريقة. على سبيل المثال ، في C # ، التي جئت منها إلى Java ، يساعد تنفيذ واجهة صريحة على التعامل مع هذه المشكلة. في هذه الحالة ، أولاً ، يمكنك استدعاء الطريقة فقط مع إرسال صريح إلى الواجهة. ثانيًا ، والأهم من ذلك في هذه الحالة ، يشار إلى اسم الواجهة (وبدون معدّل الوصول) بشكل صريح في توقيع الطريقة ، على سبيل المثال:

IClient.isValueFree(int value) { } 

أحد الحلول هو إضافة تعليق توضيحي باسم الواجهة التي تنفذ هذه الطريقة. شيء مثل @Override ، فقط مع واجهة. أوافق ، يمكنك استخدام فئة داخلية مجهولة. في هذه الحالة ، تمامًا كما هو الحال في C # ، لا يمكن استدعاء الطريقة فقط على الكائن ، ويمكنك على الفور معرفة الواجهة التي تنفذها. ولكن ، سيؤدي ذلك إلى زيادة مقدار التعليمات البرمجية ، وبالتالي ، يقلل من سهولة القراءة. نعم ، وتحتاج إلى الحصول عليه بطريقة أو بأخرى من الفصل - قم بإنشاء معلم أو حقل عام (بعد كل شيء ، لا يوجد عبء زائد على عبارات Cast في Java أيضًا). ليس خيارًا سيئًا ، لكني لا أحبه.

في البداية ، اعتقدت أنه في Java ، كما هو الحال في C # ، فإن التعليقات التوضيحية هي فئات كاملة ويمكن توريثها منها. في هذه الحالة ، ما عليك سوى إنشاء تعليق توضيحي يرث من @Override . لكن الأمر لم يكن كذلك ، وكان عليّ أن أغوص في عالم الشيكات المذهل والمرعب في مرحلة التجميع.

رمز عينة UnitManager
 public class Unit { private int id; } public class UnitManager implements RotateCounter.IClient { private final Unit[] units; private final RotateCounter idGenerator; public UnitManager(int size) { units = new Unit[size]; idGenerator = new RotateCounter(0, size, this); } public void addUnit(Unit unit) { int id = idGenerator.findFree(); units[id] = unit; } @Implement(RotateCounter.IClient.class) public boolean isValueFree(int value) { return units[value] == null; } public void removeUnit(int id) { units[id] = null; } } public class RotateCounter { private final IClient client; private int next; private int minValue; private int maxValue; public RotateCounter(int minValue, int maxValue, IClient client) { this.client = client; this.minValue = minValue; this.maxValue = maxValue; next = minValue; } public int incrementAndGet() { int current = next; if (next >= maxValue) { next = minValue; return current; } next++; return current; } public int range() { return maxValue - minValue + 1; } public int findFree() { int range = range(); int trysCounter = 0; int id; do { if (++trysCounter > range) { throw new IllegalStateException("No free values."); } id = incrementAndGet(); } while (!client.isValueFree(id)); return id; } public static interface IClient { boolean isValueFree(int value); } } 

جزء من النظرية


سأحجز على الفور ، جميع الطرق المذكورة أعلاه هي أمثلة ، لذلك ، للإيجاز ، <_>.<_>() إلى أسماء الطرق باسم النوع وبدون معلمات: <_>.<_>() .

تتضمن معالجة العناصر في مرحلة التجميع فئات خاصة للمعالج. هذه هي الفئات التي ترث من javax.annotation.processing.AbstractProcessor (يمكنك ببساطة تنفيذ واجهة javax.annotation.processing.Processor ). يمكنك قراءة المزيد عن المعالجات هنا وهنا . أهم طريقة في العملية. حيث يمكننا الحصول على قائمة بجميع العناصر المشروحة وإجراء الفحوصات اللازمة.

 @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) { return false; } 

في البداية ، ساذجة بصدق ، اعتقدت أن العمل مع الأنواع في مرحلة التجميع يتم من حيث التفكير ، ولكن ... لا. كل شيء مبني على عناصر هناك.

Element ( javax.lang.model.element.Element ) - الواجهة الرئيسية للعمل مع معظم العناصر الهيكلية للغة. يحتوي العنصر على أحفاد تحدد بدقة خصائص عنصر معين (لمزيد من التفاصيل ، انظر هنا ):

 package ds.magic.example.implement; // PackageElement public class Unit // TypeElement { private int id; // VariableElement public void setId(int id) { // ExecutableElement this.id = id; } } 

TypeMirror ( javax.lang.model.type.TypeMirror ) هو شيء مثل Class <؟> يتم إرجاعه بواسطة طريقة getClass (). على سبيل المثال ، يمكن مقارنتها لمعرفة ما إذا كانت أنواع العناصر متطابقة. يمكنك الحصول عليه باستخدام طريقة Element.asType() . يُرجع هذا النوع أيضًا بعض عمليات النوع ، مثل TypeElement.getSuperclass() أو TypeElement.getInterfaces() .

الأنواع ( javax.lang.model.util.Types ) - أنصحك بإلقاء نظرة فاحصة على هذا الفصل. يمكنك العثور على الكثير من الأشياء المثيرة للاهتمام هناك. في جوهرها ، هذه مجموعة من الأدوات للعمل مع الأنواع. على سبيل المثال ، يسمح لك باستعادة TypeElement من TypeMirror.

 private TypeElement asTypeElement(TypeMirror typeMirror) { return (TypeElement)processingEnv.getTypeUtils().asElement(typeMirror); } 

TypeKind ( javax.lang.model.type.TypeKind ) - تعداد يتيح لك توضيح معلومات النوع ، والتحقق مما إذا كان النوع عبارة عن مصفوفة (ARRAY) ، ونوع مخصص (DECLARED) ، ومتغير نوع (TYPEVAR) ، إلخ. يمكنك الحصول عليه من خلال TypeMirror.getKind()

ElementKind ( javax.lang.model.element.ElementKind ) - التعداد ، يتيح لك توضيح المعلومات حول العنصر ، والتحقق مما إذا كان العنصر عبارة عن حزمة (PACKAGE) ، والفئة (CLASS) ، والطريقة (METHOD) ، والواجهة (INTERFACE) ، إلخ.

الاسم ( javax.lang.model.element.Name ) - يمكن الحصول على واجهة للعمل مع اسم العنصر من خلال Element.getSimpleName() .

بشكل أساسي ، كانت هذه الأنواع كافية لي لكتابة خوارزمية تحقق.

أريد أن أشير إلى ميزة أخرى مثيرة للاهتمام. توجد تطبيقات واجهات Element في Eclipse في حزم org.eclipse ... ، على سبيل المثال ، العناصر التي تمثل الأساليب من النوع org.eclipse.jdt.internal.compiler.apt.model.ExecutableElementImpl . أعطاني هذا فكرة أن هذه الواجهات يتم تنفيذها بواسطة كل IDE بشكل مستقل.

خوارزمية التحقق


تحتاج أولاً إلى إنشاء التعليق التوضيحي نفسه. لقد تم بالفعل كتابة الكثير حول هذا الموضوع (على سبيل المثال ، هنا ) ، لذلك لن أتناول هذا بالتفصيل. لا يسعني إلا أن أقول أنه @Target ، نحتاج إلى إضافة @Target و @Retention . يشير الأول إلى أن التعليق التوضيحي الخاص بنا لا يمكن تطبيقه إلا على الطريقة ، والثاني أن التعليق التوضيحي سيكون موجودًا فقط في شفرة المصدر.

يجب تحديد التعليقات التوضيحية للواجهة التي تنفذ الطريقة المُعلَّقة (الطريقة التي يتم تطبيق التعليقات التوضيحية عليها). يمكن القيام بذلك بطريقتين: إما تحديد الاسم الكامل للواجهة بسلسلة ، على سبيل المثال @Implement("com.ds.IInterface") ، أو تمرير فئة الواجهة مباشرة: @Implement(IInterface.class) . الطريقة الثانية أفضل بشكل واضح. في هذه الحالة ، سيقوم المترجم بمراقبة اسم الواجهة الصحيح. بالمناسبة ، إذا قمت باستدعاء قيمة العضو () ، فعند إضافة التعليقات التوضيحية إلى الطريقة ، لن تحتاج إلى تحديد اسم هذه المعلمة بشكل صريح.

 @Target({ElementType.METHOD}) @Retention(RetentionPolicy.SOURCE) public @interface Implement { Class<?> value(); } 

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

 @SupportedAnnotationTypes({"ds.magic.annotations.compileTime.Implement"}) @SupportedSourceVersion(SourceVersion.RELEASE_8) public class ImplementProcessor extends AbstractProcessor { private Types typeUtils; @Override public void init(ProcessingEnvironment procEnv) { super.init(procEnv); typeUtils = this.processingEnv.getTypeUtils(); } @Override public boolean process(Set<? extends TypeElement> annos, RoundEnvironment env) { Set<? extends Element> annotatedElements = env.getElementsAnnotatedWith(Implement.class); for(Element annotated : annotatedElements) { Implement annotation = annotatedElement.getAnnotation(Implement.class); TypeMirror interfaceMirror = getValueMirror(annotation); TypeElement interfaceType = asTypeElement(interfaceMirror); //... } return false; } private TypeElement asTypeElement(TypeMirror typeMirror) { return (TypeElement)typeUtils.asElement(typeMirror); } } 

أريد أن أشير إلى أنه لا يمكنك فقط الحصول على التعليقات التوضيحية ذات القيمة والحصول عليها تمامًا. عند محاولة استدعاء annotation.value() ، سيتم طرح MirroredTypeException ، ولكن يمكنك الحصول على TypeMirror. وجدت طريقة الغش هذه ، وكذلك الاستلام الصحيح للقيمة ، هنا :

 private TypeMirror getValueMirror(Implement annotation) { try { annotation.value(); } catch(MirroredTypeException e) { return e.getTypeMirror(); } return null; } 

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

 private void printError(String message, Element annotatedElement) { Messager messager = processingEnv.getMessager(); messager.printMessage(Kind.ERROR, message, annotatedElement); } 

الخطوة الأولى هي التحقق مما إذا كانت التعليقات التوضيحية للقيمة واجهة. كل شيء بسيط هنا:

 if (interfaceType.getKind() != ElementKind.INTERFACE) { String name = Implement.class.getSimpleName(); printError("Value of @" + name + " must be an interface", annotated); continue; } 

بعد ذلك ، تحتاج إلى التحقق مما إذا كانت الفئة التي توجد بها الطريقة المشروحة تنفذ بالفعل الواجهة المحددة. في البداية قمت بتنفيذ هذا الاختبار بحماقة بيدي. ولكن بعد ذلك ، وباستخدام نصيحة جيدة ، نظرت إلى الأنواع ووجدت طريقة Types.isSubtype() هناك ، والتي ستتحقق من شجرة الميراث بالكامل وتعود صحيحة إذا كانت الواجهة المحددة موجودة. الأهم من ذلك ، يمكن أن تعمل مع الأنواع العامة ، على عكس الخيار الأول.

 TypeElement enclosingType = (TypeElement)annotatedElement.getEnclosingElement(); if (!typeUtils.isSubtype(enclosingType.asType(), interfaceMirror)) { Name className = enclosingType.getSimpleName(); Name interfaceName = interfaceType.getSimpleName(); printError(className + " must implemet " + interfaceName, annotated); continue; } 

أخيرًا ، تحتاج إلى التأكد من أن الواجهة تحتوي على طريقة لها نفس التوقيع الذي يحتوي على تعليق توضيحي. أود استخدام طريقة Types.isSubsignature() ، ولكن للأسف ، لا تعمل بشكل صحيح إذا كانت الطريقة تحتوي على معلمات كتابة. لذلك نشمر عن سواعدنا ونكتب جميع الشيكات بأيدينا. ولدينا ثلاثة منهم مرة أخرى. حسنًا ، بشكل أكثر دقة ، يتكون توقيع الطريقة من ثلاثة أجزاء: اسم الطريقة ونوع القيمة المرتجعة وقائمة المعلمات. تحتاج إلى مراجعة جميع طرق الواجهة والعثور على الطريقة التي اجتازت جميع عمليات الفحص الثلاثة. سيكون من الجيد عدم نسيان أن الطريقة يمكن أن تكون موروثة من واجهة أخرى وتقوم بشكل متكرر بإجراء نفس الاختبارات للواجهات الأساسية.

يجب وضع المكالمة في نهاية الحلقة في طريقة العملية ، مثل هذا:

 if (!haveMethod(interfaceType, (ExecutableElement)annotatedElement)) { Name name = interfaceType.getSimpleName(); printError(name + " don't have \"" + annotated + "\" method", annotated); continue; } 

تبدو طريقة haveMethod () نفسها كما يلي:

 private boolean haveMethod(TypeElement interfaceType, ExecutableElement method) { Name methodName = method.getSimpleName(); for (Element interfaceElement : interfaceType.getEnclosedElements()) { if (interfaceElement instanceof ExecutableElement) { ExecutableElement interfaceMethod = (ExecutableElement)interfaceElement; // Is names match? if (!interfaceMethod.getSimpleName().equals(methodName)) { continue; } // Is return types match (ignore type variable)? TypeMirror returnType = method.getReturnType(); TypeMirror interfaceReturnType = method.getReturnType(); if (!isTypeVariable(interfaceReturnType) && !returnType.equals(interfaceReturnType)) { continue; } // Is parameters match? if (!isParametersEquals(method.getParameters(), interfaceMethod.getParameters())) { continue; } return true; } } // Recursive search for (TypeMirror baseMirror : interfaceType.getInterfaces()) { TypeElement base = asTypeElement(baseMirror); if (haveMethod(base, method)) { return true; } } return false; } private boolean isParametersEquals(List<? extends VariableElement> methodParameters, List<? extends VariableElement> interfaceParameters) { if (methodParameters.size() != interfaceParameters.size()) { return false; } for (int i = 0; i < methodParameters.size(); i++) { TypeMirror interfaceParameterMirror = interfaceParameters.get(i).asType(); if (isTypeVariable(interfaceParameterMirror)) { continue; } if (!methodParameters.get(i).asType().equals(interfaceParameterMirror)) { return false; } } return true; } private boolean isTypeVariable(TypeMirror type) { return type.getKind() == TypeKind.TYPEVAR; } 

ترى المشكلة؟ لا؟ وهي هناك. الحقيقة هي أنني لم أجد طريقة للحصول على معلمات الكتابة الفعلية للواجهات العامة. على سبيل المثال ، لدي فئة تنفذ واجهة المسند :
 MyPredicate implements Predicate&ltString&gt { @Implement(Predicate.class) boolean test(String t) { return false; } } 

عند تحليل الطريقة في الفصل ، يكون نوع المعلمة هو String ، وفي الواجهة هو T ، وكل محاولات الحصول على String بدلاً من ذلك لم تؤد إلى أي شيء. في النهاية ، توصلت إلى شيء أفضل من مجرد تجاهل معلمات الكتابة. سيتم تمرير الشيك مع أي معلمات نوع فعلية ، حتى إذا لم تتطابق. لحسن الحظ ، سيقوم المترجم بإلقاء خطأ إذا لم يكن للطريقة تطبيق افتراضي ولم يتم تطبيقه في الفئة الأساسية. ولكن مع ذلك ، إذا كان أي شخص يعرف كيفية الالتفاف حول هذا ، فسأكون ممتنًا جدًا للتلميح.

الاتصال بالكسوف


أنا شخصياً أحب Eclipce وفي ممارستي استخدمته فقط. لذلك ، سأصف كيفية توصيل المعالج بـ IDE هذا. لكي يرى Eclipse المعالج ، تحتاج إلى حزمه في .JAR منفصل ، حيث سيكون التعليق التوضيحي نفسه أيضًا. في هذه الحالة ، تحتاج إلى إنشاء مجلد META-INF / services في المشروع وإنشاء ملف javax.annotation.processing.Processor هناك والإشارة إلى الاسم الكامل لفئة المعالج: ds.magic.annotations.compileTime.ImplementProcessor في حالتي. فقط في حالة ، سأعطي لقطة شاشة ، ولكن عندما لا يعمل شيء بالنسبة لي ، كدت أخطئ في بنية المشروع.

الصورة

بعد ذلك ، قم بجمع .JAR وربطه بمشروعك ، أولاً كمكتبة عادية ، بحيث تظهر التعليقات التوضيحية في الكود. ثم نقوم بتوصيل المعالج ( هنا هو أكثر تفصيلاً). للقيام بذلك ، افتح خصائص المشروع وحدد:

  1. Java Compiler -> Annotation Processing وتحقق من مربع "Enablenotation معالجة".
  2. Java Compiler -> Annotation Processing -> Factory Path حدد مربع الاختيار "تمكين الإعدادات الخاصة بالمشروع". ثم اضغط اضافة JARs ... وحدد ملف JAR الذي تم تكوينه مسبقا.
  3. يوافق على إعادة بناء المشروع.

الملخص


يمكن رؤية الجميع معًا وفي مشروع Eclipse على GitHub . في وقت كتابة هذا التقرير ، لم يكن هناك سوى فئتين ، إذا كان يمكن تسمية التعليق التوضيحي ذلك: Implement.java و ImplementProcessor.java. أعتقد أنك خمنت بالفعل غرضهم.

ربما يبدو هذا التعليق التوضيحي عديم الفائدة بالنسبة للبعض. ربما يكون كذلك. ولكن أنا شخصياً أستخدمه بدلاً من @Override ، عندما لا تتناسب أسماء الطرق مع غرض الفصل. وحتى الآن ، ليست لدي رغبة في التخلص منها. بشكل عام ، قمت بعمل تعليق توضيحي لنفسي ، وكان الغرض من المقالة هو إظهار أشعل النار الذي كنت أهاجمه. آمل أنني فعلت ذلك. شكرا لكم على اهتمامكم.

ملاحظة. بفضل مستخدمي ohotNik_alex و Comdiv لمساعدتهم في إصلاح الأخطاء.

Source: https://habr.com/ru/post/ar414715/


All Articles