Wie kann absichtlich eine benutzerdefinierte Java-Compiler-Warnmeldung ausgelöst werden?

83

Ich bin dabei, einen hässlichen temporären Hack zu begehen, um ein Blockierungsproblem zu umgehen, während wir darauf warten, dass eine externe Ressource behoben wird. Abgesehen davon, dass ich es mit einem großen, beängstigenden Kommentar und einer Reihe von FIXMEs markiert habe, möchte ich, dass der Compiler eine offensichtliche Warnmeldung als Erinnerung ausgibt, damit wir nicht vergessen, dies herauszunehmen. Zum Beispiel so etwas wie:

[javac] com.foo.Hacky.java:192: warning: FIXME temporary hack to work around library bug, remove me when library is fixed!

Gibt es eine Möglichkeit, eine absichtliche Compiler-Warnung mit einer Nachricht meiner Wahl auszulösen? Wenn dies nicht der Fall ist, was ist am einfachsten zum Code hinzuzufügen, um eine vorhandene Warnung auszulösen, möglicherweise mit einer Nachricht in einer Zeichenfolge in der betreffenden Zeile, damit sie in der Warnmeldung gedruckt wird?

EDIT: Veraltete Tags scheinen nichts für mich zu tun:

/**
 * @deprecated "Temporary hack to work around remote server quirks"
 */
@Deprecated
private void doSomeHackyStuff() { ... }

Keine Compiler- oder Laufzeitfehler in Eclipse oder Sun Javac 1.6 (ausgeführt von Ant Script), und die Funktion wird definitiv ausgeführt.

pimlottc
quelle
1
Zu Ihrer Information: @Deprecated gibt nur eine Compiler- Warnung aus , keinen Compiler- oder Laufzeitfehler. Der Code sollte auf jeden Fall laufen
BalusC
Versuchen Sie, direkt mit Javac zu laufen. Ich vermute, Ant versteckt eine Ausgabe. Oder sehen Sie meine aktualisierte Antwort unten für weitere Details.
Peter Recore

Antworten:

42

Eine Technik, die ich gesehen habe, besteht darin, dies in Unit-Tests einzubinden (Sie machen Unit-Tests, oder?). Grundsätzlich erstellen Sie einen Komponententest, der fehlschlägt, sobald der Fix für externe Ressourcen erreicht ist. Dann kommentieren Sie diesen Komponententest, um anderen zu sagen, wie Sie Ihren knorrigen Hack rückgängig machen können, sobald das Problem behoben ist.

Was an diesem Ansatz wirklich klug ist, ist, dass der Auslöser für das Rückgängigmachen Ihres Hacks eine Lösung des Kernproblems selbst ist.

Kevin Day
quelle
2
Ich habe auf einer der No Fluff Just Stuff-Konferenzen davon gehört (ich kann mich nicht erinnern, wer der Moderator war). Ich fand es ziemlich schlau. Ich kann diese Konferenzen jedoch auf jeden Fall empfehlen.
Kevin Day
3
Ich würde gerne ein Beispiel für diesen Ansatz sehen
birgersp
Die Antwort ist 11 Jahre alt, aber ich würde noch einen Schritt weiter gehen: Das Kommentieren von Unit-Tests ist gefährlich. Ich würde einen Komponententest erstellen, der das unerwünschte Verhalten zusammenfasst, sodass die Kompatibilität unterbrochen wird, wenn es schließlich behoben wird.
avgvstvs
86

Ich denke, dass eine benutzerdefinierte Anmerkung, die vom Compiler verarbeitet wird, die Lösung ist. Ich schreibe häufig benutzerdefinierte Anmerkungen, um Dinge zur Laufzeit zu erledigen, aber ich habe nie versucht, sie zur Kompilierungszeit zu verwenden. Daher kann ich Ihnen nur Hinweise auf die Tools geben, die Sie möglicherweise benötigen:

  • Schreiben Sie einen benutzerdefinierten Anmerkungstyp. Auf dieser Seite wird erläutert, wie Sie eine Anmerkung schreiben.
  • Schreiben Sie einen Anmerkungsprozessor, der Ihre benutzerdefinierte Anmerkung verarbeitet, um eine Warnung auszugeben. Das Tool, mit dem solche Anmerkungsprozessoren ausgeführt werden, heißt APT. Eine Einführung finden Sie auf dieser Seite . Ich denke, was Sie in der APT-API benötigen, ist AnnotationProcessorEnvironment, mit der Sie Warnungen ausgeben können.
  • Ab Java 6 ist APT in Javac integriert. Das heißt, Sie können einen Annotationsprozessor in die javac-Befehlszeile einfügen. In diesem Abschnitt des Javac-Handbuchs erfahren Sie, wie Sie Ihren benutzerdefinierten Anmerkungsprozessor aufrufen.

Ich weiß nicht, ob diese Lösung wirklich praktikabel ist. Ich werde versuchen, es selbst zu implementieren, wenn ich etwas Zeit finde.

Bearbeiten

Ich habe meine Lösung erfolgreich implementiert. Und als Bonus habe ich die Service Provider-Funktion von Java verwendet, um die Verwendung zu vereinfachen. Eigentlich ist meine Lösung ein JAR, das zwei Klassen enthält: die benutzerdefinierte Annotation und den Annotation-Prozessor. Um es zu verwenden, fügen Sie dieses Glas einfach in den Klassenpfad Ihres Projekts ein und kommentieren Sie, was Sie wollen! Dies funktioniert direkt in meiner IDE (NetBeans).

Code der Anmerkung:

package fr.barjak.hack;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.LOCAL_VARIABLE, ElementType.METHOD, ElementType.PACKAGE, ElementType.PARAMETER, ElementType.TYPE})
public @interface Hack {

}

Code des Prozessors:

package fr.barjak.hack_processor;

import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;

@SupportedAnnotationTypes("fr.barjak.hack.Hack")
public class Processor extends AbstractProcessor {

    private ProcessingEnvironment env;

    @Override
    public synchronized void init(ProcessingEnvironment pe) {
        this.env = pe;
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (!roundEnv.processingOver()) {
            for (TypeElement te : annotations) {
                final Set< ? extends Element> elts = roundEnv.getElementsAnnotatedWith(te);
                for (Element elt : elts) {
                    env.getMessager().printMessage(Kind.WARNING,
                            String.format("%s : thou shalt not hack %s", roundEnv.getRootElements(), elt),
                            elt);
                }
            }
        }
        return true;
    }

}

Fügen Sie die Datei META-INF/services/javax.annotation.processing.Processorin das JAR ein, um das resultierende JAR als Dienstanbieter zu aktivieren . Diese Datei ist eine acsii-Datei, die den folgenden Text enthalten muss:

fr.barjak.hack_processor.Processor
Barjak
quelle
3
+1, Großartige Forschung! Dies ist definitiv der "richtige Weg" (wenn ein Komponententest nicht praktikabel ist) und hat den Vorteil, dass er über die regulären Warnungen hinausragt.
Yishai
1
Javac gibt eine Warnung aus, aber nichts passiert in Eclipse (?)
fwonce
8
Kleiner Hinweis: Es ist nicht erforderlich, initdas envFeld zu überschreiben und festzulegen - Sie können das ProcessingEnvironmentvon erhalten, this.processingEnvda es ist protected.
Paul Bellora
Wird diese Warnmeldung in IDE-Warnungen angezeigt?
Uylmz
4
Die Verarbeitung von Anmerkungen ist in Eclipse standardmäßig deaktiviert. Um es zu aktivieren, gehen Sie zu Projekteigenschaften -> Java Compiler -> Anmerkungsverarbeitung -> Anmerkungsverarbeitung aktivieren. Unter dieser Seite befindet sich eine Seite namens "Factory Path", auf der Sie Jars konfigurieren müssen, die über die Prozessoren verfügen, die Sie verwenden möchten.
Konstantin Komissarchik
14

Ein schneller und nicht so schmutziger Ansatz könnte darin bestehen, eine @SuppressWarningsAnmerkung mit einem absichtlich falschen StringArgument zu verwenden:

@SuppressWarnings("FIXME: this is a hack and should be fixed.")

Dies generiert eine Warnung, da sie vom Compiler nicht als spezifische Warnung zum Unterdrücken erkannt wird:

Nicht unterstützte @SuppressWarnings ("FIXME: Dies ist ein Hack und sollte behoben werden.")

Luchostein
quelle
4
Es funktioniert nicht bei der Unterdrückung von Warnungen vor Feldsichtbarkeit oder Flusenfehlern.
Igor Ganapolsky
2
Die Ironie lenkt ab.
Cambunctious
12

Ein guter Hack verdient einen anderen ... Normalerweise generiere ich Compiler-Warnungen für den beschriebenen Zweck, indem ich eine nicht verwendete Variable in die Hacky-Methode einführe.

/**
 * @deprecated "Temporary hack to work around remote server quirks"
 */
@Deprecated
private void doSomeHackyStuff() {
    int FIXMEtemporaryHackToWorkAroundLibraryBugRemoveMeWhenLibraryIsFixed;
    ...
}

Diese nicht verwendete Variable generiert eine Warnung, die (abhängig von Ihrem Compiler) ungefähr so ​​aussieht:

WARNUNG: Die lokale Variable FIXMEtemporaryHackToWorkAroundLibraryBugRemoveMeWhenLibraryIsFixed wird niemals gelesen.

Diese Lösung ist nicht so gut wie eine benutzerdefinierte Annotation, hat jedoch den Vorteil, dass keine vorherige Vorbereitung erforderlich ist (vorausgesetzt, der Compiler ist bereits so konfiguriert, dass Warnungen für nicht verwendete Variablen ausgegeben werden). Ich würde vorschlagen, dass dieser Ansatz nur für kurzlebige Hacks geeignet ist. Für langlebige Hacks würde ich argumentieren, dass der Versuch, eine benutzerdefinierte Anmerkung zu erstellen, gerechtfertigt wäre.

WReach
quelle
Wissen Sie, wie Sie nicht verwendete Variablenwarnungen aktivieren können? Ich baue für Android mit Gradle über die Befehlszeile und erhalte keine Warnungen für nicht verwendete Variablen. Wissen Sie, wie dies aktiviert werden kann build.gradle?
Andreas
@Andreas Sorry, ich weiß nichts über diese Umgebung / Toolchain. Wenn es zu diesem Thema noch keine SO-Frage gibt, können Sie eine stellen.
WReach
10

Ich habe eine Bibliothek geschrieben, die dies mit Anmerkungen tut: Lightweight Javac @Warning Annotation

Die Verwendung ist sehr einfach:

// some code...

@Warning("This method should be refactored")
public void someCodeWhichYouNeedAtTheMomentButYouWantToRefactorItLater() {
    // bad stuff going on here...
}

Und der Compiler gibt eine Warnmeldung mit Ihrem Text aus

Artem Zinnatullin
quelle
Bitte fügen Sie einen Haftungsausschluss hinzu, dass Sie der Autor der empfohlenen Bibliothek sind.
Paul Bellora
@ PaulBellora weiß nicht, wie es Ihnen helfen wird, aber okay
Artem Zinnatullin
2
Vielen Dank! Siehe meta.stackexchange.com/questions/57497/…
Paul Bellora
5

Wie wäre es, die Methode oder Klasse als @Deprecated zu markieren? Dokumente hier . Beachten Sie, dass es sowohl ein @Deprecated als auch ein @deprecated gibt - die Großbuchstaben-D-Version ist die Anmerkung und das Kleinbuchstaben-D ist die Javadoc-Version. In der Javadoc-Version können Sie eine beliebige Zeichenfolge angeben, die erklärt, was gerade passiert. Compiler müssen jedoch keine Warnung ausgeben, wenn sie diese sehen (obwohl dies viele tun). Die Anmerkung sollte immer eine Warnung verursachen, obwohl ich nicht glaube, dass Sie eine Erklärung hinzufügen können.

UPDATE hier ist der Code, mit dem ich gerade getestet habe: Sample.java enthält:

public class Sample {
    @Deprecated
    public static void foo() {
         System.out.println("I am a hack");
    }
}

SampleCaller.java enthält:

public class SampleCaller{
     public static void main(String [] args) {
         Sample.foo();
     }
}

Wenn ich "javac Sample.java SampleCaller.java" ausführe, erhalte ich die folgende Ausgabe:

Note: SampleCaller.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.

Ich benutze Sun's Javac 1.6. Wenn Sie eine Warnung vor Ehrlichkeit und nicht nur eine Notiz wünschen, verwenden Sie die Option -Xlint. Vielleicht sickert das richtig durch Ant.

Peter Recore
quelle
Ich bekomme anscheinend keinen Fehler vom Compiler mit @Deprecate; bearbeite mein q mit beispielcode.
Pimlottc
1
hmm. Ihr Beispiel zeigt nur die veraltete Methode. Wo benutzt du die Methode? Dort wird die Warnung angezeigt.
Peter Recore
3
Funktioniert @Deprecatednur klassenübergreifend (daher ist es für private Methoden nutzlos).
npostavs
4

Wir können dies mit Anmerkungen tun!

Um einen Fehler auszulösen Messager, senden Sie eine Nachricht mit Diagnostic.Kind.ERROR. Kurzes Beispiel:

processingEnv.getMessager().printMessage(
    Diagnostic.Kind.ERROR, "Something happened!", element);

Hier ist eine ziemlich einfache Anmerkung, die ich geschrieben habe, um dies zu testen.

Diese @MarkerAnmerkung gibt an, dass das Ziel eine Markierungsschnittstelle ist:

package marker;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Marker {
}

Und der Anmerkungsprozessor verursacht einen Fehler, wenn dies nicht der Fall ist:

package marker;

import javax.annotation.processing.*;
import javax.lang.model.*;
import javax.lang.model.element.*;
import javax.lang.model.type.*;
import javax.lang.model.util.*;
import javax.tools.Diagnostic;
import java.util.Set;

@SupportedAnnotationTypes("marker.Marker")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public final class MarkerProcessor extends AbstractProcessor {

    private void causeError(String message, Element e) {
        processingEnv.getMessager()
            .printMessage(Diagnostic.Kind.ERROR, message, e);
    }

    private void causeError(
            Element subtype, Element supertype, Element method) {
        String message;
        if (subtype == supertype) {
            message = String.format(
                "@Marker target %s declares a method %s",
                subtype, method);
        } else {
            message = String.format(
                "@Marker target %s has a superinterface " +
                "%s which declares a method %s",
                subtype, supertype, method);
        }

        causeError(message, subtype);
    }

    @Override
    public boolean process(
            Set<? extends TypeElement> annotations,
            RoundEnvironment roundEnv) {

        Elements elementUtils = processingEnv.getElementUtils();
        boolean processMarker = annotations.contains(
            elementUtils.getTypeElement(Marker.class.getName()));
        if (!processMarker)
            return false;

        for (Element e : roundEnv.getElementsAnnotatedWith(Marker.class)) {
            ElementKind kind = e.getKind();

            if (kind != ElementKind.INTERFACE) {
                causeError(String.format(
                    "target of @Marker %s is not an interface", e), e);
                continue;
            }

            if (kind == ElementKind.ANNOTATION_TYPE) {
                causeError(String.format(
                    "target of @Marker %s is an annotation", e), e);
                continue;
            }

            ensureNoMethodsDeclared(e, e);
        }

        return true;
    }

    private void ensureNoMethodsDeclared(
            Element subtype, Element supertype) {
        TypeElement type = (TypeElement) supertype;

        for (Element member : type.getEnclosedElements()) {
            if (member.getKind() != ElementKind.METHOD)
                continue;
            if (member.getModifiers().contains(Modifier.STATIC))
                continue;
            causeError(subtype, supertype, member);
        }

        Types typeUtils = processingEnv.getTypeUtils();
        for (TypeMirror face : type.getInterfaces()) {
            ensureNoMethodsDeclared(subtype, typeUtils.asElement(face));
        }
    }
}

Dies sind beispielsweise korrekte Verwendungen von @Marker:

  • @Marker
    interface Example {}
    
  • @Marker
    interface Example extends Serializable {}
    

Diese Verwendungen von @Markerverursachen jedoch einen Compilerfehler:

  • @Marker
    class Example {}
    
  • @Marker
    interface Example {
        void method();
    }
    

    Markierungsfehler

Hier ist ein Blog-Beitrag, den ich sehr hilfreich fand, um mit dem Thema zu beginnen:


Kleiner Hinweis: Der Kommentator unten weist darauf hin, dass er aufgrund von MarkerProcessorReferenzen Marker.classeine Abhängigkeit von der Kompilierungszeit hat. Ich habe das obige Beispiel mit der Annahme geschrieben, dass beide Klassen in dieselbe JAR-Datei (z. B. marker.jar) gehen würden, aber das ist nicht immer möglich.

Angenommen, es gibt eine Anwendungs-JAR mit den folgenden Klassen:

com.acme.app.Main
com.acme.app.@Ann
com.acme.app.AnnotatedTypeA (uses @Ann)
com.acme.app.AnnotatedTypeB (uses @Ann)

Dann existiert der Prozessor für @Annin einer separaten JAR, die beim Kompilieren der Anwendungs-JAR verwendet wird:

com.acme.proc.AnnProcessor (processes @Ann)

In diesem Fall AnnProcessorwäre es nicht möglich, den Typ von @Anndirekt zu referenzieren , da dies eine zirkuläre JAR-Abhängigkeit erzeugen würde. Es könnte nur @Annmit StringNamen oder TypeElement/ referenzieren TypeMirror.

Radiodef
quelle
Das ist nicht gerade die beste Art, Anmerkungsprozessoren zu schreiben. Normalerweise erhalten Sie den Annotationstyp aus dem Set<? extends TypeElement>Parameter und erhalten dann die annotierten Elemente für die angegebene Runde mit getElementsAnnotatedWith(TypeElement annotation). Ich verstehe auch nicht, warum Sie die printMessageMethode verpackt haben .
ThePyroEagle
@ThePyroEagle Die Wahl zwischen zwei Überladungen ist sicher ein ziemlich kleiner Unterschied im Codierungsstil.
Radiodef
Es ist aber im Idealfall, möchten Sie nicht einfach den Anmerkungsprozessor in der Prozessor-JAR haben? Die Verwendung der zuvor genannten Methoden ermöglicht diese Isolationsstufe, da Sie die verarbeitete Annotation nicht im Klassenpfad haben müssen.
ThePyroEagle
2

Hier finden Sie ein Tutorial zu Anmerkungen und unten ein Beispiel für die Definition Ihrer eigenen Anmerkungen. Leider ergab ein kurzes Überfliegen des Tutorials, dass diese nur im Javadoc verfügbar sind ...

Vom Compiler verwendete Anmerkungen Es gibt drei Anmerkungstypen, die durch die Sprachspezifikation selbst vordefiniert sind: @Deprecated, @Override und @SuppressWarnings.

Es scheint also, dass Sie wirklich nur ein @ Deprecated-Tag einwerfen können, das der Compiler ausdrucken wird, oder ein benutzerdefiniertes Tag in die Javadocs einfügen, das über den Hack informiert.

Matt Phillips
quelle
Außerdem gibt der Compiler eine Warnung aus, die besagt, dass die Methode, die Sie mit @Deprecated markieren, so ist ... Er teilt dem Benutzer mit, um welche es sich handelt.
Matt Phillips
1

Wenn Sie IntelliJ verwenden. Sie können zu: Einstellungen> Editor> TODO gehen und "\ bhack.b *" oder ein anderes Muster hinzufügen.

Wenn du dann einen Kommentar machst wie // HACK: temporary fix to work around server issues

Dann wird es im TODO-Werkzeugfenster zusammen mit all Ihren anderen definierten Mustern während der Bearbeitung gut angezeigt.

BARJ
quelle
0

Sie sollten ein Tool zum Kompilieren verwenden, z. B. ant ou maven. Damit sollten Sie beim Kompilieren einige Aufgaben definieren, die beispielsweise einige Protokolle (wie Nachrichten oder Warnungen) zu Ihren FIXME-Tags erzeugen können.

Und wenn Sie einige Fehler wollen, ist es auch möglich. Stoppen Sie die Kompilierung, wenn Sie TODO in Ihrem Code belassen haben (warum nicht?)

Lioda
quelle
Der Hack ist, es so schnell wie möglich zum Laufen zu bringen. Ich habe
momentan
0

Um überhaupt eine Warnung zu erhalten, stellte ich fest, dass nicht verwendete Variablen und benutzerdefinierte @ SuppressWarnings für mich nicht funktionierten, aber eine unnötige Besetzung:

public class Example {
    public void warn() {
        String fixmePlease = (String)"Hello";
    }
}

Wenn ich jetzt kompiliere:

$ javac -Xlint:all Example.java
ExampleTest.java:12: warning: [cast] redundant cast to String
        String s = (String) "Hello!";
                   ^
1 warning
Andy Bileam
quelle