Предоставьте java-компилятор, когда используется аннотированный метод (например, @deprecated)

Скажем, я определяю пользовательскую аннотацию под названием @Unsafe.

Я хотел бы предоставить процессор аннотации, который будет определять ссылки на методы, аннотированные с помощью @Unsafe, и печатать предупреждение.

Например, данный код...

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

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

... Я хочу, чтобы компилятор печатал что-то вроде:

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

Он очень похож по духу на @Deprecated, но моя аннотация передает что-то другое, поэтому я не могу использовать @Deprecated напрямую. Есть ли способ достичь этого с помощью обработчика аннотаций? API-интерфейс обработчика аннотаций, по-видимому, более сфокусирован на объектах, применяющих аннотации (Foo.java в моем примере), чем объекты, которые ссылаются на аннотированные элементы.

Этот вопрос предоставляет технику для его достижения в виде отдельного этапа сборки с использованием ASM. Но мне интересно, могу ли я сделать это более естественным образом с обработкой javac и аннотацией?

Ответ 1

Думаю, я мог бы технически достичь своей цели, используя ответ от @mernst, поэтому я ценю это предложение. Тем не менее, я нашел другой маршрут, который работал лучше для меня, поскольку я работаю над коммерческим продуктом и не могу отменить Checker Framework (его лицензия GPL несовместима с нашей).

В моем решении я использую свой собственный "стандартный" обработчик аннотации java для создания списка всех методов, аннотированных с помощью @Unsafe.

Затем я разработал плагин javac. API-интерфейс плагина позволяет легко найти все вызовы любого метода в AST. Используя несколько советов от этого вопроса, я смог определить имя класса и метода из MethodInvocationTree AST node. Затем я сравниваю эти вызовы метода с более ранним "листингом", который я создал, содержал методы, аннотированные с помощью @Unsafe, и выдавал предупреждения там, где это необходимо.

Вот сокращенная версия моего javac-плагина.

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) {
  }

}

Примечание. Для запуска javac-плагина необходимо указать аргументы в командной строке:

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

Кроме того, у вас должен быть файл META-INF/services/com.sun.source.util.Plugin в unsafe-plugin.jar, содержащий полное имя плагина:

com.unsafetest.javac.UnsafePlugin

Ответ 2

Да, это возможно с помощью обработки аннотаций.

Одно из осложнений состоит в том, что стандартный обработчик аннотации не опускается в тела метода (он рассматривает только объявление метода). Вы хотите обработчик аннотации, который проверяет каждую строку кода.

Checker Framework предназначен для создания таких обработчиков аннотаций. Вам просто нужно определить обратный вызов, который при вызове метода выдает предупреждение javac, если вызов неприемлем. (В вашем случае это просто означает, что объявление метода имеет аннотацию @Unsafe.) Checker Framework запускает обратный вызов при каждом вызове метода в программе.

Ответ 3

AbstractProcessor ниже обрабатывает аннотацию greghmerrill @Unsafe и выдает предупреждения о вызовах методов для @Unsafe аннотированных методов.

Это небольшая модификация собственного ответа greghmerrills, что было здорово, но у меня были некоторые проблемы с получением инкрементного компилятора IDEs (я использую Netbeans) для обнаружения предупреждений/ошибок и т.д., испускаемых из плагина, - только те, которые я напечатал из процессор был показан, хотя поведение было таким, как ожидалось, когда я запускал "mvn clean compile" (я использую Maven). Независимо от того, связано ли это с какой-либо проблемой из моей руки или точки разницы между плагинами и абстрактными процессорами/фазами процесса компиляции, я не знаю.

В любом случае:

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) { } }

При использовании аннотации и вызове аннотированного метода она будет выглядеть так: введите описание изображения здесь

Следует помнить о том, чтобы добавить полное имя класса обработчика аннотаций в файл META-INF/service с именем javax.annotation.processing.Processor. Это делает его доступным для платформы ServiceLoader.

Пользователи Maven, имеющие проблемы с импортом com.sun **, могут найти этот ответ от AnimeshSharma.

Я сохраняю свой комментарий + обработчик аннотации в отдельном проекте. Мне пришлось отключить обработку аннотаций, добавив следующее в pom:

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

Использование аннотации и наличие процессора делает его работу простой: в моем другом проекте (том, из которого сделан снимок экрана метода foo()) я просто добавил зависимость от проекта, содержащего аннотацию и процессор.

Наконец, следует отметить, что я новичок в AbstractProcessors и TaskListeners. Я, fx, не имею обзора производительности или надежности кода. Цель состояла в том, чтобы просто "заставить его работать" и предоставить заглушку для подобных проектов.