简体   繁体   中英

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

Given a stream with two lambda expressions working on it:

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

and an AspectJ advice that matches all lambdas (taken from here ) and prints out the name of the called method and the value of the lamdba's first parameter:

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

The output is:

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]

Is there a way to include in the output not only the lambda's parameter, but also the Stream's method that got the lambda as parameter? In other words: is it possible to know in the beforeLambda method, if currently the map or the filter call is being processed?

The output I am looking for would be:

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


What I have tried so far:

  • check the information in the JoinPoint. It contains the signature of the method that was created by the lambda expression. The name of the actual method is different ( lambda$0 for map and lambda$1 for filter), but as they are generated by the compiler, there is no way to use this information in the code. I could try to distinguish the two cases based on the return types, but in my real life problem, the different lambda expressions also have the same return types.
  • try to find a more specific pointcut expression that only matches one of the calls. Again, the problem is that there is no way to know the name of the generated method for the map or filter lambda.
  • looking a the stack trace while beforeLambda is running. In both cases, the lowest entry in the stack trace is the stream's count method and the last entry before beforeLambda is the generated method:
@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") + "])");
}
  • add a second aspect to the Stream's methods, printing out which Stream method is called with which parameter (that would be in case of map and filter one of the lambdas) so that I could later replace the generated method names in the output. However the lambda's names in the Stream methods do not match the method names seen in the beforeLambda output:
 @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] 

How about not intercepting lambda execution() but call() to Java stream methods instead? (Cannot use execution here because AspectJ cannot intercept JDK method executions as they are outside your code base.)

Driver application:

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();
  }
}

Aspect:

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());
  }
}

As you can see, I also added source location info for demonstration purposes. I would not use it if you ask me, I just wanted to show you it exists.

Console log:

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

Update: If you switch to native AspectJ syntax - which I think is much more readable and elegant anyway for several reasons, eg because you can use imported classes in your pointcuts without fully qualifying package names - you can use thisEnclosingJoinPointStaticPart for call() pointcuts like this:

Modified aspect:

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());
  }
}

New console log:

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

Update after OP significantly changed his question:

What you want is not possible. The reason can be seen in your own log output at the bottom of the question:

  • The stream method calls are long finished before the mapped functions are executed. Do not let the way the source code looks fool you.
  • This is because Java streams are lazy . Only when a terminal function is called - count in your case - the chain of non-terminal functions before that one is set into motion.
  • What I said above does not get any less complicated by the fact that there are parallel streams, too. There execution order is not necessarily linear anyway.

So even if you explicitly implement functional interfaces in classes instead of using lambdas, this is true. But then at least you could infer from the class name in your log what is happening:

Modified driver application:

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");
    }
  }
}

Modified aspect:

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);
  }
}

Modified console log:

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

It does not get any better than this. If you want to have log output you actually understand, you need to refactor in the way I did. As I said: Only after count() has been called, all the functions wired before that will be executed.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM