
Wir alle lieben es, Fehler bei der Kompilierung zu erkennen, anstatt Laufzeitausnahmen. Der einfachste Weg, sie zu beheben, besteht darin, dass der Compiler selbst alle Stellen anzeigt, die repariert werden müssen. Obwohl die meisten Probleme nur beim Start des Programms erkannt werden können, versuchen wir dies immer noch so schnell wie möglich.
In Initialisierungsblöcken von Klassen, in Konstruktoren von Objekten, beim ersten Aufruf einer Methode usw. Und manchmal haben wir Glück und wissen bereits in der Kompilierungsphase genug, um das Programm auf bestimmte Fehler zu überprüfen.
In diesem Artikel möchte ich die Erfahrung des Schreibens eines solchen Tests teilen. Genauer gesagt, Erstellen einer Annotation, die wie der Compiler Fehler auslösen kann. Gemessen an der Tatsache, dass es in RuNet nicht so viele Informationen zu diesem Thema gibt, sind die oben beschriebenen glücklichen Situationen nicht oft.
Ich werde den allgemeinen Verifizierungsalgorithmus sowie alle Schritte und Nuancen beschreiben, für die ich Zeit und Nervenzellen aufgewendet habe.
Erklärung des Problems
In diesem Abschnitt werde ich ein Beispiel für die Verwendung dieser Anmerkung geben. Wenn Sie bereits wissen, welche Prüfung Sie durchführen möchten, können Sie diese sicher überspringen. Ich bin sicher, dass dies die Vollständigkeit der Präsentation nicht beeinträchtigen wird.
Jetzt werden wir mehr über die Verbesserung der Lesbarkeit des Codes als über die Behebung von Fehlern sprechen. Ein Beispiel, könnte man sagen, aus dem Leben oder eher aus meinem Hobbyprojekt.
Angenommen, es gibt eine UnitManager-Klasse, bei der es sich tatsächlich um eine Sammlung von Einheiten handelt. Es gibt Methoden zum Hinzufügen, Löschen, Abrufen einer Einheit usw. Beim Hinzufügen einer neuen Einheit weist der Manager ihr eine ID zu. Die Generierung der ID wird an die RotateCounter-Klasse delegiert, die eine Zahl im angegebenen Bereich zurückgibt. Und es gibt ein kleines Problem, RotateCounter kann nicht wissen, ob die ausgewählte ID frei ist. Nach dem Prinzip der Abhängigkeitsinversion können Sie eine Schnittstelle erstellen, in meinem Fall RotateCounter.IClient mit einer einzigen Methode, isValueFree (), die id empfängt und true zurückgibt, wenn id frei ist. Und UnitManager implementiert diese Schnittstelle, erstellt eine Instanz von RotateCounter und übergibt sie an sich selbst als Client.
Ich habe genau das getan. Nachdem ich einige Tage nach dem Schreiben die Quelle von UnitManager geöffnet hatte, wurde ich leicht betäubt, nachdem ich die isValueFree () -Methode gesehen hatte, die nicht wirklich zur Logik von UnitManager passte. Es wäre viel einfacher, anzugeben, welche Schnittstelle diese Methode implementiert. In C #, von dem ich zu Java gekommen bin, hilft beispielsweise eine explizite Schnittstellenimplementierung, um dieses Problem zu lösen. In diesem Fall können Sie die Methode zunächst nur mit einer expliziten Umwandlung in die Schnittstelle aufrufen. Zweitens, und was in diesem Fall noch wichtiger ist, wird der Schnittstellenname (und ohne den Zugriffsmodifikator) in der Methodensignatur explizit angegeben, zum Beispiel:
IClient.isValueFree(int value) { }
Eine Lösung besteht darin, eine Anmerkung mit dem Namen der Schnittstelle hinzuzufügen, die diese Methode implementiert. So etwas wie
@Override
, nur mit einer Schnittstelle. Ich stimme zu, Sie können eine anonyme innere Klasse verwenden. In diesem Fall kann die Methode wie in C # nicht nur für das Objekt aufgerufen werden, und Sie können sofort sehen, welche Schnittstelle sie implementiert. Dies erhöht jedoch die Codemenge und beeinträchtigt daher die Lesbarkeit. Ja, und Sie müssen es irgendwie aus der Klasse holen - erstellen Sie einen Getter oder ein öffentliches Feld (schließlich gibt es auch in Java keine Überladung von Cast-Anweisungen). Keine schlechte Option, aber ich mag es nicht.
Zuerst dachte ich, dass Annotationen in Java wie in C # vollständige Klassen sind und von ihnen geerbt werden können. In diesem Fall müssen Sie nur eine Anmerkung erstellen, die von
@Override
erbt. Dies war jedoch nicht der Fall, und ich musste mich bei der Zusammenstellung in die erstaunliche und beängstigende Welt der Schecks stürzen.
UnitManager-Beispielcode 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); } }
Ein bisschen Theorie
Ich werde sofort eine Reservierung vornehmen. Alle oben genannten Methoden sind Instanzen. Der Kürze
<_>.<_>()
werde ich daher die Namen der Methoden mit dem
<_>.<_>()
und ohne Parameter
<_>.<_>()
:
<_>.<_>()
.
Die Verarbeitung von Elementen in der Kompilierungsphase umfasst spezielle Prozessorklassen. Dies sind Klassen, die von
javax.annotation.processing.AbstractProcessor
erben (Sie können einfach die Schnittstelle
javax.annotation.processing.Processor
implementieren). Weitere Informationen zu Prozessoren finden Sie
hier und
hier . Die wichtigste Methode ist der Prozess. In dem wir eine Liste aller kommentierten Elemente erhalten und die notwendigen Prüfungen durchführen können.
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) { return false; }
Zuerst, aufrichtig naiv, dachte ich, dass die Arbeit mit Typen in der Kompilierungsphase in Bezug auf Reflexion erfolgt, aber ... nein. Dort basiert alles auf Elementen.
Element (
javax.lang.model.element.Element ) - die Hauptschnittstelle für die Arbeit mit den meisten Strukturelementen der Sprache. Ein Element hat Nachkommen, die die Eigenschaften eines bestimmten Elements genauer bestimmen (Einzelheiten siehe
hier ):
package ds.magic.example.implement;
TypeMirror (
javax.lang.model.type.TypeMirror ) ist so etwas wie Class <?>, Die von der Methode getClass () zurückgegeben wird. Sie können beispielsweise verglichen werden, um herauszufinden, ob die Elementtypen übereinstimmen. Sie können es mit der
Element.asType()
-Methode
Element.asType()
. Dieser Typ gibt auch einige
TypeElement.getSuperclass()
zurück, z. B.
TypeElement.getSuperclass()
oder
TypeElement.getInterfaces()
.
Typen (
javax.lang.model.util.Types ) - Ich
rate Ihnen, sich diese Klasse genauer anzusehen. Dort finden Sie viele interessante Dinge. Im Wesentlichen handelt es sich hierbei um eine Reihe von Dienstprogrammen für die Arbeit mit Typen. Beispielsweise können Sie ein TypeElement von einem TypeMirror zurückerhalten.
private TypeElement asTypeElement(TypeMirror typeMirror) { return (TypeElement)processingEnv.getTypeUtils().asElement(typeMirror); }
TypeKind (
javax.lang.model.type.TypeKind ) - eine Aufzählung, mit der Sie
Typinformationen klären, prüfen können, ob der Typ ein Array (ARRAY), ein benutzerdefinierter Typ (DECLARED), eine Typvariable (TYPEVAR) usw. ist. Sie können es über
TypeMirror.getKind()
ElementKind (
javax.lang.model.element.ElementKind ) - Aufzählung, mit der Sie Informationen über das Element klären, prüfen können, ob es sich bei dem Element um ein Paket (PACKAGE), eine Klasse (CLASS), eine Methode (METHODE), eine Schnittstelle (INTERFACE) usw. handelt.
Name (
javax.lang.model.element.Name ) - Die Schnittstelle zum Arbeiten mit dem Namen des Elements kann über
Element.getSimpleName()
abgerufen werden.
Grundsätzlich reichten diese Typen für mich aus, um einen Verifizierungsalgorithmus zu schreiben.
Ich möchte ein weiteres interessantes Feature erwähnen. Die Implementierungen der Elementschnittstellen in Eclipse befinden sich in den Paketen org.eclipse .... Die Elemente, die die Methoden darstellen, sind beispielsweise vom Typ
org.eclipse.jdt.internal.compiler.apt.model.ExecutableElementImpl
. Dies brachte mich auf die Idee, dass diese Schnittstellen von jeder IDE unabhängig implementiert werden.
Validierungsalgorithmus
Zuerst müssen Sie die Anmerkung selbst erstellen. Es wurde bereits viel darüber geschrieben (zum Beispiel
hier ), daher werde ich nicht im Detail darauf eingehen. Ich kann nur sagen, dass wir für unser Beispiel zwei Anmerkungen
@Target
und
@Retention
hinzufügen
@Retention
. Die erste gibt an, dass unsere Annotation nur auf die Methode angewendet werden kann, und die zweite, dass die Annotation nur im Quellcode vorhanden ist.
Es muss angegeben werden, welche Schnittstelle die mit Anmerkungen versehene Methode implementiert (die Methode, auf die die Anmerkung angewendet wird). Dies kann auf zwei Arten erfolgen:
@Implement("com.ds.IInterface")
entweder den vollständigen Namen der Schnittstelle mit einer Zeichenfolge an, z. B.
@Implement("com.ds.IInterface")
, oder übergeben Sie die Schnittstellenklasse direkt:
@Implement(IInterface.class)
. Der zweite Weg ist eindeutig besser. In diesem Fall überwacht der Compiler den korrekten Schnittstellennamen. Wenn Sie diesen
Elementwert () aufrufen, müssen Sie beim Hinzufügen von Anmerkungen zur Methode den Namen dieses Parameters nicht explizit angeben.
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.SOURCE) public @interface Implement { Class<?> value(); }
Dann beginnt der Spaß - die Erstellung des Prozessors. Bei der Prozessmethode erhalten wir eine Liste aller mit Anmerkungen versehenen Elemente. Dann erhalten wir die Anmerkung selbst und ihre Bedeutung - die angegebene Schnittstelle. Im Allgemeinen sieht das Prozessorklassen-Framework folgendermaßen aus:
@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);
Ich möchte darauf hinweisen, dass Sie nicht einfach so Wertanmerkungen erhalten und erhalten können. Wenn Sie versuchen,
annotation.value()
aufzurufen, wird eine
MirroredTypeException ausgelöst, von der Sie jedoch einen TypeMirror erhalten können. Diese Betrugsmethode sowie den korrekten Wertempfang habe ich
hier gefunden:
private TypeMirror getValueMirror(Implement annotation) { try { annotation.value(); } catch(MirroredTypeException e) { return e.getTypeMirror(); } return null; }
Die Prüfung selbst besteht aus drei Teilen. Wenn mindestens einer von ihnen fehlschlägt, müssen Sie eine Fehlermeldung anzeigen und mit der nächsten Anmerkung fortfahren. Übrigens können Sie eine Fehlermeldung mit der folgenden Methode anzeigen:
private void printError(String message, Element annotatedElement) { Messager messager = processingEnv.getMessager(); messager.printMessage(Kind.ERROR, message, annotatedElement); }
Der erste Schritt besteht darin, zu überprüfen, ob Wertanmerkungen eine Schnittstelle sind. Hier ist alles einfach:
if (interfaceType.getKind() != ElementKind.INTERFACE) { String name = Implement.class.getSimpleName(); printError("Value of @" + name + " must be an interface", annotated); continue; }
Als Nächstes müssen Sie überprüfen, ob die Klasse, in der sich die mit Anmerkungen versehene Methode befindet, die angegebene Schnittstelle tatsächlich implementiert. Zuerst habe ich diesen Test törichterweise mit meinen Händen durchgeführt. Mit guten Ratschlägen habe ich mir dann
Types angesehen und dort die Methode
Types.isSubtype()
, die den gesamten Vererbungsbaum überprüft und true
Types.isSubtype()
, wenn die angegebene Schnittstelle vorhanden ist. Im Gegensatz zur ersten Option kann es mit generischen Typen arbeiten.
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; }
Schließlich müssen Sie sicherstellen, dass die Schnittstelle eine Methode mit derselben Signatur wie die mit Anmerkungen versehene hat. Ich möchte die Methode
Types.isSubsignature()
verwenden, aber leider funktioniert sie nicht richtig, wenn die Methode Typparameter hat. Also krempeln wir die Ärmel hoch und schreiben alle Schecks mit den Händen aus. Und wir haben wieder drei davon. Genauer gesagt besteht die Methodensignatur aus drei Teilen: dem Namen der Methode, dem Typ des Rückgabewerts und der Liste der Parameter. Sie müssen alle Methoden der Schnittstelle durchgehen und die finden, die alle drei Prüfungen bestanden hat. Es wäre schön, nicht zu vergessen, dass die Methode von einer anderen Schnittstelle geerbt werden kann und rekursiv dieselben Überprüfungen für die zugrunde liegenden Schnittstellen durchführt.
Der Aufruf muss in der Prozessmethode wie folgt am Ende der Schleife platziert werden:
if (!haveMethod(interfaceType, (ExecutableElement)annotatedElement)) { Name name = interfaceType.getSimpleName(); printError(name + " don't have \"" + annotated + "\" method", annotated); continue; }
Und die haveMethod () -Methode selbst sieht folgendermaßen aus:
private boolean haveMethod(TypeElement interfaceType, ExecutableElement method) { Name methodName = method.getSimpleName(); for (Element interfaceElement : interfaceType.getEnclosedElements()) { if (interfaceElement instanceof ExecutableElement) { ExecutableElement interfaceMethod = (ExecutableElement)interfaceElement;
Sehen Sie das Problem? Nein? Und sie ist da. Tatsache ist, dass ich keinen Weg finden konnte, die tatsächlichen Typparameter für generische Schnittstellen zu erhalten. Zum Beispiel habe ich eine Klasse, die die
Prädikatschnittstelle implementiert:
MyPredicate implements Predicate<String> { @Implement(Predicate.class) boolean test(String t) { return false; } }
Bei der Analyse der Methode in der Klasse lautet der Typ des Parameters
String
und in der Schnittstelle
T
, und alle Versuche, stattdessen
String
abzurufen, führten zu nichts. Am Ende habe ich mir nichts Besseres ausgedacht, als nur die Typparameter zu ignorieren. Die Prüfung wird mit allen tatsächlichen Typparametern bestanden, auch wenn diese nicht übereinstimmen. Glücklicherweise gibt der Compiler einen Fehler aus, wenn die Methode keine Standardimplementierung hat und nicht in der Basisklasse implementiert ist. Aber wenn jemand weiß, wie man das umgeht, bin ich für den Hinweis äußerst dankbar.
Stellen Sie eine Verbindung zu Eclipse her
Persönlich liebe ich Eclipce und in meiner Praxis habe ich nur es benutzt. Daher werde ich beschreiben, wie der Prozessor an diese IDE angeschlossen wird. Damit Eclipse den Prozessor sehen kann, müssen Sie ihn in eine separate JAR-Datei packen, in der sich auch die Anmerkung selbst befindet. In diesem Fall müssen Sie den Ordner
META-INF / services im Projekt erstellen und dort die Datei
javax.annotation.processing.Processor erstellen und den vollständigen Namen der Prozessorklasse
ds.magic.annotations.compileTime.ImplementProcessor
: in meinem Fall
ds.magic.annotations.compileTime.ImplementProcessor
. Nur für den Fall, ich werde einen Screenshot geben, aber als nichts für mich funktionierte, fing ich fast an, über die Struktur des Projekts zu sündigen.

Sammeln Sie als Nächstes .JAR und verbinden Sie es zunächst als reguläre Bibliothek mit Ihrem Projekt, damit die Anmerkung im Code sichtbar ist. Dann schließen wir den Prozessor an (
hier ist detaillierter). Öffnen Sie dazu die
Projekteigenschaften und wählen Sie:
- Java Compiler -> Annotation Processing und aktivieren Sie das Kontrollkästchen "Annotation Processing aktivieren".
- Java Compiler -> Annotation Processing -> Factory Path aktivieren Sie das Kontrollkästchen "Projektspezifische Einstellungen aktivieren". Klicken Sie dann auf JARs hinzufügen ... und wählen Sie die zuvor erstellte JAR-Datei aus.
- Stimmt zu, das Projekt neu zu erstellen.
Zusammenfassung
Alles zusammen und im Eclipse-Projekt ist auf
GitHub zu sehen. Zum Zeitpunkt des Schreibens gibt es nur zwei Klassen, wenn die Annotation so genannt werden kann: Implement.java und ImplementProcessor.java. Ich denke, Sie haben ihren Zweck bereits erraten.
Vielleicht scheint diese Anmerkung einigen nutzlos zu sein. Vielleicht ist es das. Aber ich persönlich benutze es anstelle von
@Override
, wenn Methodennamen nicht gut in den Zweck der Klasse passen. Und bis jetzt habe ich keine Lust, sie loszuwerden. Im Allgemeinen habe ich eine Anmerkung für mich selbst gemacht, und der Zweck des Artikels war es zu zeigen, welchen Rechen ich angegriffen habe. Ich hoffe ich habe es geschafft. Vielen Dank für Ihre Aufmerksamkeit.
PS. Vielen Dank an die Benutzer von
ohotNik_alex und
Comdiv für ihre Hilfe bei der Behebung von Fehlern.