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

前端 未结 2 800
遥遥无期
遥遥无期 2021-01-02 23:23
 class Test {

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


private String b() {
c();
}

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

}

I want to

相关标签:
2条回答
  • 2021-01-02 23:59

    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().

    0 讨论(0)
  • 2021-01-03 00:10

    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!
    

    I hope this is what you wanted. It is hard to tell because your description is somewhat ambiguous. BTW, please do read an AspectJ and/or Spring AOP manual.

    0 讨论(0)
提交回复
热议问题