Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to intercept each method call within given method using Spring AOP or AspectJ

 class Test {

@override
public String a(){
b();
d();
}


private String b() {
c();
}

private String c(){
d();
}
private String d(){}

}

I want to intercept each methods of class Test that is been called from overridden method A() and want to know how much time each method like b(), c() took while processing some business logic separately.

How can I achieve it using Spring AOP or Aspectj?

like image 972
Crazy-Coder Avatar asked Mar 07 '18 19:03

Crazy-Coder


2 Answers

In order to

  • weave into private methods,
  • handle self-invocation within one class,
  • dynamically determine control flow and limit interception to only methods called directly or indirectly by your interface method

you need to switch from Spring AOP (proxy-based, many limitations, slow) to AspectJ using LTW (load-time weaving) as described in the Spring manual.

Here is an example in pure AspectJ (no Spring, Just Java SE) which you can easily adapt to your needs:

Sample interface

package de.scrum_master.app;

public interface TextTransformer {
  String transform(String text);
}

Class implementing interface incl. main method:

As you can see, I made up an example like yours and also made the methods spend time in order to have something to measure in the aspect later:

package de.scrum_master.app;

public class Application implements TextTransformer {
  @Override
  public String transform(String text) {
    String geekSpelling;
    try {
      geekSpelling = toGeekSpelling(text);
      return toUpperCase(geekSpelling);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }

  }

  private String toGeekSpelling(String text) throws InterruptedException {
    Thread.sleep(100);
    return replaceVovels(text).replaceAll("[lL]", "1");
  }

  private String replaceVovels(String text) throws InterruptedException {
    Thread.sleep(75);
    return text.replaceAll("[oO]", "0").replaceAll("[eE]", "Ɛ");
  }

  private String toUpperCase(String text) throws InterruptedException {
    Thread.sleep(50);
    return text.toUpperCase();
  }

  public static void main(String[] args) throws InterruptedException {
    System.out.println(new Application().transform("Hello world!"));
  }
}

Aspect:

package de.scrum_master.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import static java.lang.System.currentTimeMillis;

@Aspect
public class TimingAspect {
  @Around("execution(* *(..)) && cflow(execution(* de.scrum_master.app.TextTransformer.*(..)))")
  public Object measureExecutionTime(ProceedingJoinPoint thisJoinPoint) throws Throwable {
    long startTime = currentTimeMillis();
    Object result = thisJoinPoint.proceed();
    System.out.println(thisJoinPoint + " -> " + (currentTimeMillis() - startTime) + " ms");
    return result;
  }
}

Console log:

execution(String de.scrum_master.app.Application.replaceVovels(String)) -> 75 ms
execution(String de.scrum_master.app.Application.toGeekSpelling(String)) -> 189 ms
execution(String de.scrum_master.app.Application.toUpperCase(String)) -> 63 ms
execution(String de.scrum_master.app.Application.transform(String)) -> 252 ms
HƐ110 W0R1D!

You can also exclude the transform(..) method by just changing the pointcut from cflow() to cflowbelow():

@Around("execution(* *(..)) && cflowbelow(execution(* de.scrum_master.app.TextTransformer.*(..)))")

Then the console log is just:

execution(String de.scrum_master.app.Application.replaceVovels(String)) -> 77 ms
execution(String de.scrum_master.app.Application.toGeekSpelling(String)) -> 179 ms
execution(String de.scrum_master.app.Application.toUpperCase(String)) -> 62 ms
HƐ110 W0R1D!

Incidentally, please do read an AspectJ and/or Spring AOP manual.

like image 173
kriegaex Avatar answered Oct 20 '22 09:10

kriegaex


Spring AOP is applied using proxies, when you call a method of the bean from outside, the proxy is used and the method could be intercepted, but when you call the method from inside the class, the proxy is not used and the class is used directly.


You have three options


The first and easy one, if you do not have problems using public methods is to move the functions b(), c(), and d() to another bean. This way each call to this methods would be intercepted.

public class Test {
    public String a() { ... }
}

public class Test2 {
    public String b() { ... }
    public String c() { ... }
    public String d() { ... }
}

You can also use it as inner static class if you want to keep all in the same file.

public class Test {
    public String a() { ... }
    public static class Test2 {
        public String b() { ... }
        public String c() { ... }
        public String d() { ... }
    }
}

You should autowire Test2 in the constructor of Test.

public class Test {
    private final Test2 test2;    
    @Autowired public Test(final Test2 test2) {
        this.test2 = test2;
    }
    public String a() { 
        test2.b();
        test2.c();
        test2.d();
    }
}

And finally create the around method.

@Around(value = "execution(* package.of.the.class.Test.*(..))")
public Object aroundA(ProceedingJoinPoint pjp) throws Throwable { ... }

@Around(value = "execution(* package.of.the.class.Test2.*(..))")
public Object aroundBCD(ProceedingJoinPoint pjp) throws Throwable { 
    long start = System.currentTimeMillis();
    Object output = pjp.proceed();
    long elapsedTime = System.currentTimeMillis() - start;
    // perform side efects with elapsed time e.g. print, store...
    return output;
}

Or something like

@Around(value = "execution(* package.of.the.class.Test.*(..)) || " +
                "execution(* package.of.the.class.Test2.*(..))")
public Object aroundABCD(ProceedingJoinPoint pjp) throws Throwable { ... }

The second option is to use a CGLIB bean, package private methods and self injection.

You declare a CGLIB bean just using the scope annotation

@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
@Bean public Test test() {
    return new Test();
}

Self injection and package private methods as follows

public class Test {
    @Autowired private Test test;
    // ...
    public String a() {
        test.b(); // call through proxy (it is intercepted)
    }
    String b() { ... } // package private method
    // ...
}

The third solution is to use LWT Load-Time weaving that is using aspectj instead of the spring aop. This allows to intercept method calls even inside the same class. You can use the official spring documentation to implement it, but you will have to use a java agent to run.


Calling method

If you need to know if an specific method made the call to the intercepted function, you can use Thread.currentThread().getStackTrace() for the options 1 or 2. If you use aspectj (option 3), you could intercept the methods with cflow().

like image 1
Jose Da Silva Avatar answered Oct 20 '22 08:10

Jose Da Silva