我正在尝试将lambda参数类型转换为类类型,但是我总是遇到类广播异常。这是我试图实现的用例。
- 类 A 有一个方法
public void foo(Supplier<?> msg)
- 我在 foo(( 方法上放了一个Apsect来捕获参数(在这种情况下是 msg 的实际值(
- 类 B 使用类 A 的实例使用 lambda 表达式调用 foo(( 方法
class B{
public void bar(){
A a=new A()
a.foo(()->{ new MyCustomObject()});
}
}
在我的 AOP 类的运行时,我总是得到 foo(( 的 argurment 类型为B$$lambda$0/13qdwqd
问题如何获取方法供应商参数的实际类类型(在本例中为我的自定义对象?
方面代码
Class MyAspect{
@Around("call(public void com.mypkg.A.foo(..))")
public Object captureLambdaType(final ProceedingJoinPoint pjp) throws Throwable {
System.out.println("lambda called: [" + pjp.getSignature() + "] "+
"with parameter [" + pjp.getArgs()[0] + "]");
Supplier<MyCustomObject> obj= (Supplier<MyCustomObject> )(pjp.getArgs()[0]);
}
}
提前感谢!
你为什么要把一件简单的事情变得复杂?用户JB Nizet已经告诉你:
获取供应商返回的对象类型的唯一方法是调用供应商。
你自己的代码以一种非常人为的方式做到这一点(在我修复它以使其编译之后,因为它有问题(,使用反射。只需通过args()
使用 AspectJ 参数参数绑定,并使其类型安全。如果您只想记录Supplier
的返回值而不影响方法执行,也可以使用@Before
而不是@Around
。这样,您可以避免调用ProceedingJoinPoint.proceed()
,这是必要的,但在"解决方案"示例代码中完全缺失。
这个小MCVE怎么样?
package de.scrum_master.app;
public class MyCustomClass {}
package de.scrum_master.app;
import java.util.function.Supplier;
public class A {
public void foo(Supplier<?> msg) {}
}
package de.scrum_master.app;
public class B {
public void bar() {
A a = new A();
a.foo(() -> new MyCustomClass());
}
public static void main(String[] args) {
new B().bar();
}
}
package de.scrum_master.aspect;
import java.util.function.Supplier;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class MyAspect {
@Before("execution(* *(*)) && args(supplier)")
public void methodCallWithSupplierArgument(JoinPoint thisJoinPoint, Supplier<?> supplier) throws Exception {
System.out.println(thisJoinPoint + " -> " + supplier.get());
}
}
运行时的控制台日志B.main(..)
:
execution(void de.scrum_master.app.A.foo(Supplier)) -> de.scrum_master.app.MyCustomClass@66a29884
这与你的方面试图做的是一样的,只是更干净。我认为它肯定也更具可读性。
警告:如果供应商有副作用或计算成本高昂,请在致电供应商get()
之前三思而后行。我知道纯函数(即在Java语言中实现函数接口的代码(不应该有任何副作用,但如果以糟糕的风格编码,它们很容易产生。所以要小心。
更新:谈到带有副作用的警告,让我向您展示一些东西。只需稍微扩展应用程序代码,以便实际评估供应商并(可选(返回其结果:
package de.scrum_master.app;
import java.util.function.Supplier;
public class A {
public Object foo(Supplier<?> msg) {
return msg.get();
}
}
现在,我们还扩展了方面,以便在调用供应商的get()
方法时实际触发日志记录:
package de.scrum_master.aspect;
import java.util.function.Supplier;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class MyAspect {
@Before("execution(* *(*)) && args(supplier)")
public void methodCallWithSupplierArgument(JoinPoint thisJoinPoint, Supplier<?> supplier) throws Exception {
System.out.println(thisJoinPoint + " -> " + supplier.get());
}
@AfterReturning(pointcut = "call(public * java.util.function.Supplier+.get())", returning = "result")
public void supplierEvaluated(JoinPoint thisJoinPoint, Object result) throws Exception {
System.out.println(thisJoinPoint + " -> " + result);
}
}
现在控制台日志将是:
call(Object java.util.function.Supplier.get()) -> de.scrum_master.app.MyCustomClass@66a29884
execution(Object de.scrum_master.app.A.foo(Supplier)) -> de.scrum_master.app.MyCustomClass@66a29884
call(Object java.util.function.Supplier.get()) -> de.scrum_master.app.MyCustomClass@4769b07b
你能看到Supplier.get()
是如何被调用两次,返回两个不同的MyCustomClass
对象,即MyCustomClass@66a29884
和MyCustomClass@4769b07b
吗?这是因为应用程序和第一个方面建议都调用get()
。后者并没有真正记录与应用程序创建的对象相同的对象,因此即使没有进一步的副作用,您也会记录错误的内容并执行供应商方法两次而不是一次。
因此,让我们通过不再从第一个建议方法调用get()
来清理这个问题:
@Before("execution(* *(*)) && args(supplier)")
public void methodCallWithSupplierArgument(JoinPoint thisJoinPoint, Supplier<?> supplier) throws Exception {
System.out.println(thisJoinPoint + " -> " + supplier); // no 'get()' call anymore
}
现在日志变得干净:
execution(Object de.scrum_master.app.A.foo(Supplier)) -> de.scrum_master.app.B$$Lambda$1/1349393271@66a29884
call(Object java.util.function.Supplier.get()) -> de.scrum_master.app.MyCustomClass@4769b07b
另一个优点是,现在每当真正调用该方法时(可以同步、异步、多次或从不执行(,而不是在方面冗余执行时,都会记录get()
结果。
PS:如果你想知道为什么我如此一丝不苟地不执行get()
只是为了记录目的,想象一下lambda打开数据库连接,创建一个4 GB的文件,下载一个持续时间为90分钟的4K视频或其他什么。它会做两次,只是为了让你记录它。
谢谢大家的评论。我能够通过在 lambda 上调用get()
方法来解决它。示例代码如下
Class MyAspect{
@Around("call(public void com.mypkg.A.foo(..))")
public Object captureLambdaType(final ProceedingJoinPoint pjp) throws Throwable {
System.out.println("lambda called: [" + pjp.getSignature() + "] "+
"with parameter [" + pjp.getArgs()[0] + "]");
//Get the lambda argument
Object arg=pjp.getArgs()[0];
//Get the argument class type
Class clazz=arg.getClass();
for (Method method : clazz.getDeclaredMethods()) {
method.setAccessible(true);
Object obj=method.invoke(obj,null);
if(obj instanceof MyCustomClass){
MyCustomClass myObject= (MyCustomClass) obj;
System.out.println("Hurray");
}
}
}
}