2015-12-29 5 views
11

Angenommen, ich definiere eine benutzerdefinierte Annotation namens @Unsafe.Lassen Sie den Java Compiler warnen, wenn eine mit Anmerkungen versehene Methode verwendet wird (wie @deprecated)

Ich möchte eine Anmerkung Prozessor schaffen, Referenzen Verfahren kommentierten mit @Unsafe erkennt und eine Warnung aus.

Zum Beispiel gegeben dieser Code ...

public class Foo { 
    @Unsafe 
    public void doSomething() { ... } 
} 

public class Bar { 
    public static void main(String[] args) { 
    new Foo().doSomething(); 
    } 
} 

... Ich möchte der Compiler wie etwas drucken:

WARN > Bar.java, line 3 : Call to Unsafe API - Foo.doSomething() 

Es ist sehr ähnlich im Geiste @Deprecated, aber mein Annotation kommuniziert etwas anderes, daher kann ich @Deprecated nicht direkt verwenden. Gibt es eine Möglichkeit, dies mit einem Annotationsprozessor zu erreichen? Die Annotationsprozessor-API scheint sich mehr auf die Entitäten zu konzentrieren, die die Annotationen (Foo.java in meinem Beispiel) als Entitäten anwenden, die Referenz annotierte Mitglieder.

This question bietet eine Technik, um es als separaten Build-Schritt mit ASM zu erreichen. Aber ich frage mich, ob ich es auf natürliche Weise mit Javac & Annotationsverarbeitung tun kann?

+0

Sind Sie sicher, dass das zur Kompilierzeit vollständig möglich ist? Es ist nicht immer möglich zu wissen, auf welche Methode Bezug genommen wird. Angenommen, Ihr 'Foo' würde eine Schnittstelle implementieren, mit der '@ Unsafe'-Anmerkung nur für die' Foo'-Implementierung. Dann würde ein Client, der die Schnittstelle verwendet, nicht angezeigt. – sisyphus

+0

Sie möchten eine Warnung bezüglich der Codezeile ausgeben, die die Methode aufruft, so dass Sie die Bytecodeanalyse auf keinen Fall vermeiden können. – user3707125

+0

@sisyphus, wenn ein Aufruf der Interface-Methode unsicher sein kann, sollte die Interface-Methode mit '@ Unsafe' gekennzeichnet werden. Somit ist die Kompilierzeitverarbeitung vollständig und solide. – mernst

Antwort

3

Ich denke, ich hätte mein Ziel mit der Antwort von @mernst technisch erreichen können, also schätze ich den Vorschlag. Ich fand jedoch einen anderen Weg, der für mich besser funktionierte, da ich an einem kommerziellen Produkt arbeite und das Checker Framework nicht verwenden kann (seine GPL-Lizenz ist mit unserer nicht kompatibel).

In meiner Lösung verwende ich meinen eigenen "Standard" Java-Annotationsprozessor, um eine Liste aller mit @Unsafe annotierten Methoden zu erstellen.

Dann habe ich ein Java-Plugin entwickelt. Die Plugin-API macht es einfach, jeden Aufruf einer Methode im AST zu finden. Mit einigen Tipps von this question konnte ich den Klassen- und Methodennamen vom AST-Knoten MethodInvocationTree ermitteln. Dann vergleiche ich diese Methodenaufrufe mit dem früheren "Listing", das ich erstellt habe und die Methoden enthält, die mit @Unsafe kommentiert sind, und gebe bei Bedarf Warnungen aus.

Hier ist eine gekürzte Version meines Javac Plugins.

import javax.lang.model.element.Element; 
import javax.lang.model.element.TypeElement; 

import com.sun.source.tree.MethodInvocationTree; 
import com.sun.source.util.JavacTask; 
import com.sun.source.util.Plugin; 
import com.sun.source.util.TaskEvent; 
import com.sun.source.util.TaskEvent.Kind; 
import com.sun.tools.javac.tree.JCTree; 
import com.sun.tools.javac.tree.TreeInfo; 
import com.sun.source.util.TaskListener; 
import com.sun.source.util.TreeScanner; 

public class UnsafePlugin implements Plugin, TaskListener { 

    @Override 
    public String getName() { 
    return "UnsafePlugin"; 
    } 

    @Override 
    public void init(JavacTask task, String... args) { 
    task.addTaskListener(this); 
    } 

    @Override 
    public void finished(TaskEvent taskEvt) { 
    if (taskEvt.getKind() == Kind.ANALYZE) { 
     taskEvt.getCompilationUnit().accept(new TreeScanner<Void, Void>() { 
     @Override 
     public Void visitMethodInvocation(MethodInvocationTree methodInv, Void v) { 
      Element method = TreeInfo.symbol((JCTree) methodInv.getMethodSelect()); 
      TypeElement invokedClass = (TypeElement) method.getEnclosingElement(); 
      String className = invokedClass.toString(); 
      String methodName = methodInv.getMethodSelect().toString().replaceAll(".*\\.", ""); 
      System.out.println("Method Invocation: " + className + " : " + methodName); 
      return super.visitMethodInvocation(methodInv, v); 
     } 
     }, null); 
    } 
    } 

    @Override 
    public void started(TaskEvent taskEvt) { 
    } 

} 

Hinweis - um für die javac Plugin aufgerufen werden, können Sie Argumente auf der Kommandozeile zur Verfügung stellen muss:

javac -processorpath build/unsafe-plugin.jar -Xplugin:UnsafePlugin 

Außerdem müssen Sie eine Datei META-INF/services/com.sun.source.util.Plugin in unsicheren-plugin.jar haben enthaltend der vollständige Name des Plugins:

com.unsafetest.javac.UnsafePlugin 
+1

In Bezug auf die Lizenzierung: Genauso wie das Kompilieren Ihres Codes mit javac Ihren Code nicht mit der GPL infiziert, führt die Typ-Überprüfung Ihres Codes mit dem Checker-Framework nicht zur Infektion Ihres Codes mit der GPL. Das Ausführen des Checker-Frameworks während der Entwicklung hat keinen Einfluss auf Ihr geistiges Eigentum oder Ihre Lizenzierung. * Wenn Sie jedoch das Checker Framework als Teil Ihres Produkts versenden möchten, muss Ihr Produkt unter der GPL lizenziert werden. Daher sollten Sie in diesem Fall das Checker-Framework vermeiden. Wie auch immer, Ihr Plugin ist einfach genug, um nicht die Leistung des gesamten Checker Frameworks zu benötigen. – mernst

+2

Ausgezeichnet, ich sehe, dass Sie den Javac-Plugin-Mechanismus gefunden haben, bevor ich es vorgeschlagen habe. :-) Leider ist es nicht sehr gut dokumentiert, aber es sieht so aus, als ob du es nutzen könntest, um das zu erreichen, was du wolltest. Beachten Sie, dass die Plugin-Schnittstelle "Nicht-Standard" ist. Das heißt, es ist nicht Teil von Java SE, sondern spezifisch für Oracle JDK und OpenJDK. Andere Java-Implementierungen haben es möglicherweise nicht. Aus diesem Grund hat die Plugin-Option das Präfix "-X". –

3

Ja, dies ist mit Annotationsverarbeitung möglich.

Eine Komplikation besteht darin, dass ein Standard-Annotationsprozessor nicht in Methodenkörper absteigt (er untersucht nur die Methodendeklaration). Sie möchten einen Annotationsprozessor, der jede Codezeile überprüft.

Die Checker Framework wurde entwickelt, um solche Annotationsprozessoren zu erstellen. Sie müssen nur einen Callback definieren, der bei einem Methodenaufruf eine Javac-Warnung ausgibt, wenn der Aufruf nicht akzeptabel ist. (In Ihrem Fall ist es einfach, ob die Deklaration der Methode eine @Unsafe Annotation hat.) Das Checker Framework führt diesen Callback bei jedem Methodenaufruf im Programm aus.

+0

Vielen Dank für den Zeiger auf das Checker-Framework. Wenn ich das richtig verstehe, dann ist es mir * nicht * möglich, dies mit dem Standard javax.annotation.processing.Processor und dem JDK javac zu tun, da ich sehe, dass Checker seine Verarbeitung durch die Bereitstellung eines eigenen Ersatzes für javac (Ich nehme Delegierte auf den "echten" Javac intern an? – greghmerrill

+0

Das Checker Framework kann den Standard-Javac verwenden. Sein Java-Script hat zwei Auswirkungen: (1) Hinzufügen der Checker-Framework-JAR-Datei zum Klassenpfad und (2) Erkennen von Anmerkungen in Kommentaren. Wenn Sie Anmerkungen in Kommentaren nicht interessieren und bereit sind, ein zusätzliches Befehlszeilenargument zu übergeben, benötigen Sie den Checker Framework-Compiler nicht. – mernst

1

die AbstractProcessor unten verarbeitet greghmerrill des @ Unsafe Annotation und gibt Warnungen zu Methodenaufrufen an annotierte Annotated-Methoden aus.

Es ist eine geringfügige Änderung der greghmerrill eigenen Antwort, die war großartig, aber ich hatte einige Probleme bekommen meine IDEs inkrementellen Compiler (ich benutze Netbeans), um die Warnungen/Fehler etc aus dem Plugin - nur diejenigen, die ich gedruckt Vom Prozessor wurde gezeigt, obwohl das Verhalten wie erwartet war, als ich 'mvn clean kompilieren' lief (ich benutze Maven). Ob dies auf ein Problem von meiner Hand zurückzuführen ist, oder auf Unterschiede zwischen Plugins und AbstractProcessors/den Phasen des Kompilierungsprozesses, weiß ich nicht.

Wie dem auch sei:

package com.hervian.annotationutils.target; 

import com.sun.source.tree.MethodInvocationTree; 
import com.sun.source.util.*; 
import com.sun.tools.javac.tree.JCTree; 
import com.sun.tools.javac.tree.TreeInfo; 
import java.util.Set; 
import javax.annotation.processing.*; 
import javax.lang.model.SourceVersion; 
import javax.lang.model.element.*; 
import javax.tools.Diagnostic; 

@SupportedAnnotationTypes({"com.hervian.annotationutils.target.Unsafe"}) 
@SupportedSourceVersion(SourceVersion.RELEASE_8) 
public class UnsafeAnnotationProcessor extends AbstractProcessor implements TaskListener { 
Trees trees; 

@Override 
public synchronized void init(ProcessingEnvironment processingEnv) { 
    super.init(processingEnv); 
    trees = Trees.instance(processingEnv); 
    JavacTask.instance(processingEnv).setTaskListener(this); 
} 

@Override 
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { 
    //Process @Unsafe annotated methods if needed 
    return true; 
} 

@Override public void finished(TaskEvent taskEvt) { 
    if (taskEvt.getKind() == TaskEvent.Kind.ANALYZE) { 
     taskEvt.getCompilationUnit().accept(new TreeScanner<Void, Void>() { 
      @Override 
      public Void visitMethodInvocation(MethodInvocationTree methodInv, Void v) { 
       Element method = TreeInfo.symbol((JCTree) methodInv.getMethodSelect()); 
       Unsafe unsafe = method.getAnnotation(Unsafe.class); 
       if (unsafe != null) { 
        JCTree jcTree = (JCTree) methodInv.getMethodSelect(); 
        trees.printMessage(Diagnostic.Kind.WARNING, "Call to unsafe method.", jcTree, taskEvt.getCompilationUnit()); 
       } 
       return super.visitMethodInvocation(methodInv, v); 
      } 
     }, null); 
    } 
} 

@Override public void started(TaskEvent taskEvt) { } } 

Wenn die Annotation verwenden und Anrufe an die kommentierten Methode macht es wie folgt aussehen: enter image description here

One den vollständig qualifizierten Klassennamen der Anmerkung hinzuzufügen, zu erinnern braucht Prozessor zu einer META-INF/Service-Datei namens javax.annotation.processing.Processor. Dadurch ist es für das ServiceLoader-Framework verfügbar.

Maven Benutzer, die Probleme mit den com.sun ** Importen haben, finden this answer von AnimeshSharma hilfreich.

Ich halte meine Annotation + Annotation Prozessor in einem separaten Projekt. Ich hatte Annotation Verarbeitung zu deaktivieren, indem Sie die folgenden auf die pom Zugabe:

<build> 
    <pluginManagement> 
     <plugins> 
      <plugin> 
       <artifactId>maven-compiler-plugin</artifactId> 
       <configuration> 
        <compilerArgument>-proc:none</compilerArgument> 
       </configuration> 
      </plugin> 
     </plugins> 
    </pluginManagement> 
</build> 

die Anmerkung verwenden und mit dem Prozessor seine Arbeit tun war einfach: In meinem anderen Projekt (die, wo der Screenshot der Methode foo() ist von) Ich habe dem Projekt, das die Annotation und den Prozessor enthält, einfach eine Abhängigkeit hinzugefügt.

Schließlich sollte erwähnt werden, dass ich neu bei AbstractProcessors und TaskListeners bin. Ich habe, fx, keinen Überblick über die Leistung oder Robustheit des Codes. Das Ziel war einfach, "es zur Arbeit zu bringen" und einen Stummel für ähnliche Projekte zur Verfügung zu stellen.