问题描述
有没有办法检测代码中java方法引用(双冒号)运算符的用法?
我需要发现给定类中使用的所有实例/静态方法引用,以便能够在构建期间检测到一些错误(必须验证目标方法是否具有特定注释 - 在下面的示例中为 @Good)。按照惯例,当将方法引用传递给某个辅助类的构造函数时,应仅将其用于某些方法(以下示例中的信息)。
class X {
Info init() {
return new Info(X::beta); // good code: target method has @Good annotation
return new Info(X::alpha); // bad code: target method has no @Good annotation
}
void alpha() {
}
@Good
void beta() {
}
}
目的是能够点击方法引用,因为这样可以很容易地遵循,否则如果只传递方法实例或方法名称,它将缺乏这种能力。
(这个例子不是很好,但我现在可以分享更多细节,抱歉!)
我可以看到 IntelliJ IDEA“知道”它们 - 当您按住 ctrl+单击它们时,它会导航到目标方法,因此应该在那里使用某种形式的静态分析。
我已经在使用 ObjectWeb ASM 来检测对某些方法的调用,但它似乎缺乏检测方法引用的能力 (::)
编辑:
请注意,您也可以将 new Info(x -> x.alpha())
作为 @Thomas 传递给下面在评论中提到的,但这不会通过我们的审查过程,但我想检测它的额外能力不会受到伤害。
EDIT2:您到底想通过这些检查达到什么目的?是什么让 beta 值得接受注释?
答案: 当调用 init() 方法时,我们获取 Info 实例,并从中获取必须是方法引用的 lambda。然后我们使用 javassist ProxyFactory 并创建类 X 的子类,然后实例化它并通过设置方法处理程序拦截其所有方法。所以现在可以安全地执行 lambda 而不让它产生任何副作用 - 方法主体被跳过,我们唯一要做的就是捕获 lambda 实际调用的 X 方法 - 在示例中这将导致指向指向 X.beta 或 X.alpha 方法的 java.lang.Method 实例。然后我们可以检查它是否有 @Good 注释并相应地进行 - 即在没有任何代理的情况下调用 lambda,但该调用可能会在稍后发生,例如一毫秒或一小时后。如果没有@Good 注释,我们将无法继续 - 这是一个错误。
所以问题是这将在运行时发生,并且可能没有及早捕获到错误,这就是我想在构建时检查 X 类并捕获所有错误的原因:)
解决方法
这有点摸不着头脑,因为我对 ASM 不太精通,也不确定这种方法是否能解决您的问题。话虽如此,我发现在类似的设置中,asm.MethodVisitor
调用 MethodVisitor.visitInvokeDynamicInsn(...)
以获取(某些?全部?)方法引用。
例如,如果我将您的类 X
的这个变体与 Info
一起编译:
class Info {
public Info(Runnable alpha) {}
}
class X {
Info init() { return new Info(this::alpha); }
void alpha() {}
}
...然后我将生成的 X.class
输入到一个 mini ClassVisitor
+ 打印 MethodVisitor
(Groovy 为简洁起见):
class MyMethodVisitor extends MethodVisitor {
MyMethodVisitor(MethodVisitor parent) { super(Opcodes.ASM8,parent) }
@Override
void visitInvokeDynamicInsn(String name,String descriptor,Handle bootstrapMethodHandle,Object... bootstrapMethodArguments) {
println "visitInvokeDynamicInsn($name,$descriptor,$bootstrapMethodHandle,$bootstrapMethodArguments)"
super.visitInvokeDynamicInsn(name,descriptor,bootstrapMethodHandle,bootstrapMethodArguments)
}
}
class MyClassVisitor extends ClassVisitor {
MyClassVisitor() { super(Opcodes.ASM8) }
@Override
MethodVisitor visitMethod(int access,String name,String signature,String[] exceptions) {
println "Starting method '$name'"
new MyMethodVisitor(super.visitMethod(access,name,signature,exceptions))
}
}
def clr = new ClassReader(new File("./X.class").bytes)
clr.accept(new MyClassVisitor(),ClassReader.SKIP_FRAMES)
然后,方法访问者打印出对 visitInvokeDynamicInsn
的调用,其中包含参数中所需的 X::init
(X::alpha
是我的本地包):
xyz
因此似乎可以从这些参数中剥离该方法。我不确定这是否可靠(例如,这个字节码是否由规范保证,或者它是否可以依赖于编译/优化细节)。