【问题标题】:AspectJ advice on lambda expression: know where the lambda expression came fromAspectJ 关于 lambda 表达式的建议:知道 lambda 表达式的来源
【发布时间】:2018-02-13 21:10:30
【问题描述】:

给定一个带有两个 lambda 表达式的流:

Stream.of(new String[]{"a", "b"})
   .map(s -> s.toUpperCase())
   .filter(s -> s.equals("A"))
   .count();

和一个匹配所有 lambdas 的 AspectJ 建议(取自 here)并打印出被调用方法的名称和 lamdba 的第一个参数的值:

@Before("execution(* *..*lambda*(..))")
public void beforeLambda(JoinPoint jp) {
    System.out.println("lambda called: [" + jp.getSignature() + "] "+
        "with parameter [" + jp.getArgs()[0] + "]");
}

输出是:

lambda called: [String aspectj.Starter.lambda$0(String)] with parameter [a]
lambda called: [boolean aspectj.Starter.lambda$1(String)] with parameter [A]
lambda called: [String aspectj.Starter.lambda$0(String)] with parameter [b]
lambda called: [boolean aspectj.Starter.lambda$1(String)] with parameter [B]

有没有办法在输出中不仅包含 lambda 的参数,还包含将 lambda 作为参数的 Stream 方法?换句话说:是否有可能在beforeLambda 方法中知道当前是否正在处理mapfilter 调用?

我正在寻找的输出是:

lambda called: [map] with parameter [a]
lambda called: [filter] with parameter [A]
lambda called: [map] with parameter [b]
lambda called: [filter] with parameter [B]


到目前为止我已经尝试过:
  • 检查JoinPoint 中的信息。它包含由 lambda 表达式创建的方法的签名。实际方法的名称不同(lambda$0 用于映射,lambda$1 用于过滤器),但由于它们是由编译器生成的,因此无法在代码中使用此信息。我可以尝试根据返回类型区分这两种情况,但在我的实际问题中,不同的 lambda 表达式也具有相同的返回类型。
  • 尝试查找仅匹配其中一个调用的更具体的切入点表达式。同样,问题在于无法知道为映射或过滤器 lambda 生成的方法的名称。
  • beforeLambda 运行时查看堆栈跟踪。在这两种情况下,堆栈跟踪中的最低条目是流的 count 方法,beforeLambda 之前的最后一个条目是生成的方法:
at aspectj.Starter$LambdaAspect.beforeLambda(Starter.java:25)
at aspectj.Starter.lambda$0(Starter.java:14)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
[...more from java.util, but no hint to map or filter...]
at java.util.stream.ReferencePipeline.count(ReferencePipeline.java:526)
at aspectj.Starter.main(Starter.java:16)
  • 在 Stream 的方法中添加第二个方面,打印出使用哪个参数调用哪个 Stream 方法(如果是 mapfilter 其中一个 lambda),以便我以后可以替换生成的方法输出中的名称。但是,Stream 方法中的 lambda 名称与 beforeLambda 输出中看到的方法名称不匹配:
@Before("call(* java.util.stream.Stream.*(..))")
public void beforeStream(JoinPoint jp) {
    System.out.println("Stream method called: [" + jp.getSignature().getName() + "] with parameter [" + (jp.getArgs().length > 0 ? jp.getArgs()[0] : "null") + "])");
}
Stream method called: [of] with parameter [[Ljava.lang.String;@754c89eb])
Stream method called: [map] with parameter [aspectj.Starter$$Lambda$1/1112743104@512c45e7])
Stream method called: [filter] with parameter [aspectj.Starter$$Lambda$2/888074880@75e9a87])
Stream method called: [count] with parameter [null])
lambda called: [String aspectj.Starter.lambda$0(String)] with parameter [a]
lambda called: [boolean aspectj.Starter.lambda$1(String)] with parameter [A]
lambda called: [String aspectj.Starter.lambda$0(String)] with parameter [b]
lambda called: [boolean aspectj.Starter.lambda$1(String)] with parameter [B]

【问题讨论】:

    标签: java lambda aspectj


    【解决方案1】:

    不拦截 lambda execution() 而是拦截 call() 到 Java 流方法怎么样? (此处不能使用执行,因为 AspectJ 无法拦截 JDK 方法执行,因为它们在您的代码库之外。)

    驱动程序应用:

    package de.scrum_master.app;
    
    import java.util.stream.Stream;
    
    public class Application {
      public static void main(String[] args) {
        new Application().doSomething();
      }
    
      public long doSomething() {
        return Stream.of(new String[]{"a", "b"})
          .map(s -> s.toUpperCase())
          .filter(s -> s.equals("A"))
          .count();
      }
    }
    

    方面:

    package de.scrum_master.aspect;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.reflect.SourceLocation;
    
    @Aspect
    public class MyAspect {
      @Before("!within(*Aspect) && call(* java.util.stream.Stream.*(..))")
      public void interceptStreamMethods(JoinPoint thisJoinPoint) throws Throwable {
        System.out.println(thisJoinPoint);
        SourceLocation sourceLocation = thisJoinPoint.getSourceLocation();
        System.out.println("  " + sourceLocation.getWithinType());
        System.out.println("  " + sourceLocation.getFileName());
        System.out.println("  " + sourceLocation.getLine());
      }
    }
    

    如您所见,我还添加了源位置信息用于演示目的。如果你问我,我不会使用它,我只是想告诉你它存在。

    控制台日志:

    call(Stream java.util.stream.Stream.of(Object[]))
      class de.scrum_master.app.Application
      Application.java
      11
    call(Stream java.util.stream.Stream.map(Function))
      class de.scrum_master.app.Application
      Application.java
      12
    call(Stream java.util.stream.Stream.filter(Predicate))
      class de.scrum_master.app.Application
      Application.java
      13
    call(long java.util.stream.Stream.count())
      class de.scrum_master.app.Application
      Application.java
      14
    

    更新:如果您切换到本机 AspectJ 语法 - 我认为无论如何,出于多种原因,它更具可读性和优雅性,例如因为您可以在切入点中使用导入的类,而无需完全限定包名称 - 您可以将 thisEnclosingJoinPointStaticPart 用于 call() 切入点,如下所示:

    修改方面:

    package de.scrum_master.aspect;
    
    import java.util.stream.Stream;
    
    public aspect MyAspect {
      before(): !within(*Aspect) && call(* Stream.*(..)) {
        System.out.println(thisJoinPoint);
        System.out.println("  called by: " + thisEnclosingJoinPointStaticPart);
        System.out.println("  line: " + thisJoinPoint.getSourceLocation().getLine());
      }
    }
    

    新的控制台日志:

    call(Stream java.util.stream.Stream.of(Object[]))
      called by: execution(long de.scrum_master.app.Application.doSomething())
      line: 11
    call(Stream java.util.stream.Stream.map(Function))
      called by: execution(long de.scrum_master.app.Application.doSomething())
      line: 12
    call(Stream java.util.stream.Stream.filter(Predicate))
      called by: execution(long de.scrum_master.app.Application.doSomething())
      line: 13
    call(long java.util.stream.Stream.count())
      called by: execution(long de.scrum_master.app.Application.doSomething())
      line: 14
    

    OP 后更新明显改变了他的问题:

    你想要的都是不可能的。原因可以在问题底部的你自己的日志输出中看到:

    • 流方法调用在映射函数执行之前很久就完成了。不要让源代码看起来欺骗了您。
    • 这是因为 Java 流是惰性。仅当调用终端函数时 - 在您的情况下为 count - 在该函数启动之前的非终端函数链。
    • 我上面所说的并没有因为也有并行流这一事实而变得不那么复杂。无论如何,执行顺序不一定是线性的。

    因此,即使您在类中显式实现函数式接口而不是使用 lambda,也是如此。但至少你可以从日志中的类名推断出发生了什么:

    修改后的驱动程序应用:

    package de.scrum_master.app;
    
    import java.util.function.Function;
    import java.util.function.Predicate;
    import java.util.stream.Stream;
    
    public class Application {
      public static void main(String[] args) {
        new Application().doSomething();
      }
    
      public long doSomething() {
        return Stream.of(new String[]{"a", "b"})
          .map(new UpperCaseMapper())
          .filter(new EqualsAFilter())
          .count();
      }
    
      static class UpperCaseMapper implements Function<String, String> {
        @Override
        public String apply(String t) {
          return t.toUpperCase();
        }
      }
    
      static class EqualsAFilter implements Predicate<String> {
        @Override
        public boolean test(String t) {
          return t.equals("A");
        }
      }
    }
    

    修改方面:

    package de.scrum_master.aspect;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    
    // See https://stackoverflow.com/a/48778440/1082681
    
    @Aspect
    public class MyAspect {
      @Before("call(* java.util.stream..*(..))")
      public void streamCall(JoinPoint thisJoinPoint) {
        System.out.println(thisJoinPoint);
      }
    
      @Before("execution(* java.util.function..*(*)) && args(functionArg)")
      public void functionExecution(JoinPoint thisJoinPoint, Object functionArg) {
        System.out.println(thisJoinPoint);
        System.out.println("  " + thisJoinPoint.getTarget().getClass().getSimpleName() + " -> " + functionArg);
      }
    }
    

    修改控制台日志:

    call(Stream java.util.stream.Stream.of(Object[]))
    call(Stream java.util.stream.Stream.map(Function))
    call(Stream java.util.stream.Stream.filter(Predicate))
    call(long java.util.stream.Stream.count())
    execution(String de.scrum_master.app.Application.UpperCaseMapper.apply(String))
      UpperCaseMapper -> a
    execution(boolean de.scrum_master.app.Application.EqualsAFilter.test(String))
      EqualsAFilter -> A
    execution(String de.scrum_master.app.Application.UpperCaseMapper.apply(String))
      UpperCaseMapper -> b
    execution(boolean de.scrum_master.app.Application.EqualsAFilter.test(String))
      EqualsAFilter -> B
    

    没有比这更好的了。如果你想得到你真正理解的日志输出,你需要按照我的方式进行重构。正如我所说:只有在调用了count() 之后,所有连接到before 的函数才会被执行。

    【讨论】:

    • 不幸的是,这个解决方案不能解决我的问题。我想查看 lambda 的每个参数以及触发此 lambda 执行的 Stream 函数。因此,拦截 Stream 的方法并没有帮助。我已经更新了我的问题以澄清我在寻找什么。
    • 那么你应该更好地陈述你的问题。我花了一些时间为您准备答案,现在您对原始问题进行了重大修改,因此您不必接受答案,而是接受它并创建一个与此相关的新问题。回答这个问题感觉不值得。顺便说一句,您想在这里解决什么样的跨领域问题?更换调试器?你想要的可能会以某种方式完成,但它会很昂贵并且会减慢你的应用程序。
    • 无论如何,我更新了答案,以帮助您了解流的实际工作方式。我还是觉得这种方面太低级了,不过可能是口味问题吧。
    • 感谢您的反馈。我的问题是“如果当前正在处理地图或过滤器调用,是否有一种方法可以了解 in beforeLambda 方法”。在我的辩护中,我觉得这并没有改变,虽然我知道我的原文中“方法内”的部分没有被强调得足够强。也感谢您指出不可能实现我计划做的事情。显式实现接口的提示非常有帮助。
    猜你喜欢
    • 2015-07-19
    • 2011-05-05
    • 2013-07-02
    • 1970-01-01
    • 2013-08-18
    • 2019-12-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多