AspectJ 关于 lambda 表达式的建议:知道 lambda 表达式从哪里来

AspectJ advice on lambda expression: know where the lambda expression came from

给定一个包含两个 lambda 表达式的流:

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

和匹配所有 lambda 的 AspectJ 建议(取自 )并打印出被调用方法的名称和 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[=12=](String)] with parameter [a]
lambda called: [boolean aspectj.Starter.lambda(String)] with parameter [A]
lambda called: [String aspectj.Starter.lambda[=12=](String)] with parameter [b]
lambda called: [boolean aspectj.Starter.lambda(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]


到目前为止我尝试了什么:

at aspectj.Starter$LambdaAspect.beforeLambda(Starter.java:25)
at aspectj.Starter.lambda[=14=](Starter.java:14)
at java.util.stream.ReferencePipeline.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)
@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/1112743104@512c45e7])
Stream method called: [filter] with parameter [aspectj.Starter$$Lambda/888074880@75e9a87])
Stream method called: [count] with parameter [null])
lambda called: [String aspectj.Starter.lambda[=16=](String)] with parameter [a]
lambda called: [boolean aspectj.Starter.lambda(String)] with parameter [A]
lambda called: [String aspectj.Starter.lambda[=16=](String)] with parameter [b]
lambda called: [boolean aspectj.Starter.lambda(String)] with parameter [B]

不拦截 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 语法——出于多种原因,我认为它更具可读性和优雅性,例如因为您可以在切入点中使用导入的 classes 而无需完全限定包名称 - 您可以将 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 在您的情况下 - 启动之前的非终端函数链。
  • 我上面所说的并没有因为也有并行流而变得更复杂。执行顺序不一定是线性的。

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

修改后的驱动程序应用:

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 

@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() 被调用后,所有连接 之前 的函数才会被执行。