可以使用“ElementVisitor”来遍历方法主体中的语句吗?
Can an `ElementVisitor` be used to traverse the statements in the body of a method?
我正在尝试创建一个自定义注释,以检查是否在用它注释的方法主体中调用了某个方法。类似于:
@TypeQualifierDefault(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
@interface MutatingMethod {
}
interface Mutable {
void preMutate();
void postMutate();
// other methods
}
然后在某个 Mutable
class 内我们将有:
class Structure<T> implements Mutable {
@MutatingMethod
void add(T data) {
preMutate();
// actual mutation code
postMutate();
}
}
如果像 add
这样用 @MutatingMethod
注释的方法的主体 不包含 ,我希望能够得到某种警告调用 preMutate
和 postMutate
。是否可以使用 ElementVisitor
(javax.lang.model.element.ElementVisitor
) 遍历方法主体中的(可能被混淆的)语句和方法调用?如果是这样,那会是什么样子?如果不行我还能用什么?
澄清一下,我知道这不可能(或更难)通过字节码反编译在运行时完成,因此此注释仅在编译期间起作用通过反射( java.lang.reflect.*
和 javax.lang.model.*
),并且不会保留在 class 个文件中。
您可以随意修改代码,但是您希望它能正常工作,例如通过引入一个名为 @MutableType
的新注释,Structure
和任何其他 Mutable
类型必须对其进行注释以使其起作用。
最重要的是断言 preMutate
在 postMutate
之前而不是之后被调用。
没关系,但我正在使用 Gradle 和 IntelliJ IDEA IDE.
非常感谢任何帮助; material 这方面的内容出奇地少 and/or 网络上的不足。我一直在使用公开可用的资源来了解这一点!
注解处理器只处理声明,不处理字节码。它们不能用于对方法的内容进行断言。
如果始终调用这些方法对您很重要,您可能希望使用代理来强制执行此操作,而不是在每个方法中都使用样板代码。例如,您可以使用字节码工程库(如 Javassist)在运行时添加调用:
var f = new ProxyFactory();
f.setSuperclass(Foo.class);
f.setFilter(m -> m.isAnnotationPresent(MutatingMethod.class));
Class c = f.createClass();
Foo foo = c.newInstance();
((Proxy)foo).setHandler((self, m, proceed, args) -> {
self.preMutate();
proceed.invoke(self, args);
self.postMutate();
});
foo.setName("Peter"); // automatically calls preMutate and postMutate()
(代码未经测试,因为我手头没有 IDE)
然后,只要您控制相关对象的创建(您可以通过使 super class abstract
).
有两个模块,
java.compiler
which contains the API for annotation processors 以及您已经发现的简单抽象。
ElementVisitor
抽象不支持挖掘方法的代码。
jdk.compiler
模块,包含一个扩展的API,最初不被认为是标准的一部分API,因此不包含在官方API 引入模块系统之前的文档。
这个API允许分析当前编译的源代码的语法树。
当您的起点是注释处理器时,您应该有一个 ProcessingEnvironment
which was given to your init
method. Then, you can invoke Trees.instance(ProcessingEnvironment)
to get a helper object which has the method getTree(Element)
可用于获取语法树元素。然后,您可以从那里遍历语法树。
大多数 类 记录在 JDK 17 API 中确实已经存在于早期版本中(您可能会注意到“从 1.6 开始”),即使旧版本中不存在文档。但是在 JDK 9 之前,您必须在编译注释处理器时将特定 JDK 的 lib/tools.jar
包含到您的类路径中。
(编写模块化注释处理器时)
import javax.annotation.processing.Processor;
module anno.proc.example {
requires jdk.compiler;
provides Processor with anno.proc.example.MyProcessor;
}
package anno.proc.example;
import java.util.*;
import javax.annotation.processing.*;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import com.sun.source.tree.*;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.util.Trees;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
@SupportedSourceVersion(SourceVersion.RELEASE_17) // adapt when using older version
@SupportedAnnotationTypes(MyProcessor.ANNOTATION_NAME)
public class MyProcessor extends AbstractProcessor {
static final String ANNOTATION_NAME = "my.example.MutatingMethod";
static final String REQUIRED_FIRST = "preMutate", REQUIRED_LAST = "postMutate";
// the inherited method does already store the processingEnv
// public void init(ProcessingEnvironment processingEnv) {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Optional<? extends TypeElement> o = annotations.stream()
.filter(e -> ANNOTATION_NAME.contentEquals(e.getQualifiedName())).findAny();
if(!o.isPresent()) return false;
TypeElement myAnnotation = o.get();
roundEnv.getElementsAnnotatedWith(myAnnotation).forEach(this::check);
return true;
}
private void check(Element e) {
Trees trees = Trees.instance(processingEnv);
Tree tree = trees.getTree(e);
if(tree.getKind() != Kind.METHOD) { // should not happen as compiler handles @Target
processingEnv.getMessager()
.printMessage(Diagnostic.Kind.ERROR, ANNOTATION_NAME + " only allowed at methods", e);
return;
}
MethodTree m = (MethodTree) tree;
List<? extends StatementTree> statements = m.getBody().getStatements();
if(statements.isEmpty() || !isRequiredFirst(statements.get(0))) {
processingEnv.getMessager()
.printMessage(Diagnostic.Kind.MANDATORY_WARNING,
"Mutating method does not start with " + REQUIRED_FIRST + "();", e);
}
// open challenges:
// - accept a return statement after postMutate();
// - allow a try { body } finally { postMutate(); }
if(statements.isEmpty() || !isRequiredLast(statements.get(statements.size() - 1))) {
processingEnv.getMessager()
.printMessage(Diagnostic.Kind.MANDATORY_WARNING,
"Mutating method does not end with " + REQUIRED_LAST + "();", e);
}
}
private boolean isRequiredFirst(StatementTree st) {
return invokes(st, REQUIRED_FIRST);
}
private boolean isRequiredLast(StatementTree st) {
return invokes(st, REQUIRED_LAST);
}
// check whether tree is an invocation of a no-arg method of the given name
private boolean invokes(Tree tree, String method) {
if(tree.getKind() != Kind.EXPRESSION_STATEMENT) return false;
tree = ((ExpressionStatementTree)tree).getExpression();
if(tree.getKind() != Kind.METHOD_INVOCATION) return false;
MethodInvocationTree i = (MethodInvocationTree)tree;
if(!i.getArguments().isEmpty()) return false; // not a no-arg method
ExpressionTree ms = i.getMethodSelect();
// TODO add support for explicit this.method()
return ms.getKind() == Kind.IDENTIFIER
&& method.contentEquals(((IdentifierTree)ms).getName());
}
}
我正在尝试创建一个自定义注释,以检查是否在用它注释的方法主体中调用了某个方法。类似于:
@TypeQualifierDefault(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
@interface MutatingMethod {
}
interface Mutable {
void preMutate();
void postMutate();
// other methods
}
然后在某个 Mutable
class 内我们将有:
class Structure<T> implements Mutable {
@MutatingMethod
void add(T data) {
preMutate();
// actual mutation code
postMutate();
}
}
如果像 add
这样用 @MutatingMethod
注释的方法的主体 不包含 ,我希望能够得到某种警告调用 preMutate
和 postMutate
。是否可以使用 ElementVisitor
(javax.lang.model.element.ElementVisitor
) 遍历方法主体中的(可能被混淆的)语句和方法调用?如果是这样,那会是什么样子?如果不行我还能用什么?
澄清一下,我知道这不可能(或更难)通过字节码反编译在运行时完成,因此此注释仅在编译期间起作用通过反射( java.lang.reflect.*
和 javax.lang.model.*
),并且不会保留在 class 个文件中。
您可以随意修改代码,但是您希望它能正常工作,例如通过引入一个名为 @MutableType
的新注释,Structure
和任何其他 Mutable
类型必须对其进行注释以使其起作用。
最重要的是断言 preMutate
在 postMutate
之前而不是之后被调用。
没关系,但我正在使用 Gradle 和 IntelliJ IDEA IDE.
非常感谢任何帮助; material 这方面的内容出奇地少 and/or 网络上的不足。我一直在使用公开可用的资源来了解这一点!
注解处理器只处理声明,不处理字节码。它们不能用于对方法的内容进行断言。
如果始终调用这些方法对您很重要,您可能希望使用代理来强制执行此操作,而不是在每个方法中都使用样板代码。例如,您可以使用字节码工程库(如 Javassist)在运行时添加调用:
var f = new ProxyFactory();
f.setSuperclass(Foo.class);
f.setFilter(m -> m.isAnnotationPresent(MutatingMethod.class));
Class c = f.createClass();
Foo foo = c.newInstance();
((Proxy)foo).setHandler((self, m, proceed, args) -> {
self.preMutate();
proceed.invoke(self, args);
self.postMutate();
});
foo.setName("Peter"); // automatically calls preMutate and postMutate()
(代码未经测试,因为我手头没有 IDE)
然后,只要您控制相关对象的创建(您可以通过使 super class abstract
).
有两个模块,
java.compiler
which contains the API for annotation processors 以及您已经发现的简单抽象。ElementVisitor
抽象不支持挖掘方法的代码。jdk.compiler
模块,包含一个扩展的API,最初不被认为是标准的一部分API,因此不包含在官方API 引入模块系统之前的文档。这个API允许分析当前编译的源代码的语法树。
当您的起点是注释处理器时,您应该有一个 ProcessingEnvironment
which was given to your init
method. Then, you can invoke Trees.instance(ProcessingEnvironment)
to get a helper object which has the method getTree(Element)
可用于获取语法树元素。然后,您可以从那里遍历语法树。
大多数 类 记录在 JDK 17 API 中确实已经存在于早期版本中(您可能会注意到“从 1.6 开始”),即使旧版本中不存在文档。但是在 JDK 9 之前,您必须在编译注释处理器时将特定 JDK 的 lib/tools.jar
包含到您的类路径中。
(编写模块化注释处理器时)
import javax.annotation.processing.Processor;
module anno.proc.example {
requires jdk.compiler;
provides Processor with anno.proc.example.MyProcessor;
}
package anno.proc.example;
import java.util.*;
import javax.annotation.processing.*;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import com.sun.source.tree.*;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.util.Trees;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
@SupportedSourceVersion(SourceVersion.RELEASE_17) // adapt when using older version
@SupportedAnnotationTypes(MyProcessor.ANNOTATION_NAME)
public class MyProcessor extends AbstractProcessor {
static final String ANNOTATION_NAME = "my.example.MutatingMethod";
static final String REQUIRED_FIRST = "preMutate", REQUIRED_LAST = "postMutate";
// the inherited method does already store the processingEnv
// public void init(ProcessingEnvironment processingEnv) {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Optional<? extends TypeElement> o = annotations.stream()
.filter(e -> ANNOTATION_NAME.contentEquals(e.getQualifiedName())).findAny();
if(!o.isPresent()) return false;
TypeElement myAnnotation = o.get();
roundEnv.getElementsAnnotatedWith(myAnnotation).forEach(this::check);
return true;
}
private void check(Element e) {
Trees trees = Trees.instance(processingEnv);
Tree tree = trees.getTree(e);
if(tree.getKind() != Kind.METHOD) { // should not happen as compiler handles @Target
processingEnv.getMessager()
.printMessage(Diagnostic.Kind.ERROR, ANNOTATION_NAME + " only allowed at methods", e);
return;
}
MethodTree m = (MethodTree) tree;
List<? extends StatementTree> statements = m.getBody().getStatements();
if(statements.isEmpty() || !isRequiredFirst(statements.get(0))) {
processingEnv.getMessager()
.printMessage(Diagnostic.Kind.MANDATORY_WARNING,
"Mutating method does not start with " + REQUIRED_FIRST + "();", e);
}
// open challenges:
// - accept a return statement after postMutate();
// - allow a try { body } finally { postMutate(); }
if(statements.isEmpty() || !isRequiredLast(statements.get(statements.size() - 1))) {
processingEnv.getMessager()
.printMessage(Diagnostic.Kind.MANDATORY_WARNING,
"Mutating method does not end with " + REQUIRED_LAST + "();", e);
}
}
private boolean isRequiredFirst(StatementTree st) {
return invokes(st, REQUIRED_FIRST);
}
private boolean isRequiredLast(StatementTree st) {
return invokes(st, REQUIRED_LAST);
}
// check whether tree is an invocation of a no-arg method of the given name
private boolean invokes(Tree tree, String method) {
if(tree.getKind() != Kind.EXPRESSION_STATEMENT) return false;
tree = ((ExpressionStatementTree)tree).getExpression();
if(tree.getKind() != Kind.METHOD_INVOCATION) return false;
MethodInvocationTree i = (MethodInvocationTree)tree;
if(!i.getArguments().isEmpty()) return false; // not a no-arg method
ExpressionTree ms = i.getMethodSelect();
// TODO add support for explicit this.method()
return ms.getKind() == Kind.IDENTIFIER
&& method.contentEquals(((IdentifierTree)ms).getName());
}
}