6

Is it possible to register AOP advices programmatically, after the application has booted and the context has been initialized?

When I tried, the advices didn't work, supposedly because they need to wrap the bean BEFORE it gets available in the context.

Something like this (it doesn't work):

@Bean
private AspectJExpressionPointcutAdvisor createPointcutAdvisor(AWSXRayRecorder awsxRayRecorder, String name, String pointcut) {

    AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
    advisor.setExpression("execution ...()");
    advisor.setAdvice(new CustomAdvice("custom bean"));

    return advisor;
  }

Clarification: I need to read a list of advice from a config file, and register the pointcuts accordingly. I need the label for bookeeping purposes. The file contents are unknown at compile time.

label: execution(* com.my.ns.OtherClass(..))
label2: execution(* com.my.ns.Class(..)) 
Miguel Ping
  • 18,082
  • 23
  • 88
  • 136

3 Answers3

8

The previous solution is too invasive as it not only creates advice on the fly but also handles advising beans. This replicates functionality of Spring's AbstractAdvisorAutoProxyCreator, specifically the getAdvicesAndAdvisorsForBean method, where Spring will locate and apply eligible Advisors to each bean. A better approach is to simply programmatically create Advisors and let Spring handle the rest of the plumbing of advising beans, creating proxies, and so forth.

A simple way of creating a Advisor is to create a Advisor bean using the @Bean annotation:

@Bean
    public Advisor advisorBean() {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(* com.testit.MyAspectedService.*(..))");
        return new DefaultPointcutAdvisor(pointcut, new MyMethodInterceptor());
    }

Where the class MyMethodInterceptor implements the MethodInterceptor interface:

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

import java.lang.reflect.Method;

public class MyMethodInterceptor implements MethodInterceptor {


    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("in interceptor");

        //get the method and arguments we are intercepting
        Method method = invocation.getMethod();
        Object[] arguments = invocation.getArguments();
        //... do stuff before the method
        //call the underlying method
        return invocation.proceed();
    }

}

What this does is to create a around advice Advisor bean named "advisorBean" for all methods calls to a Spring bean MyAspectedService declared as

@Service
public class MyAspectedService {

    //various service methods
}

This approach focuses on only creating the necessary Advisors and interception implementation and delegates the weaving of the aspect to the Spring framework.

James W
  • 81
  • 1
  • 2
  • 1
  • Actually, I agree that this is more elegant than my original solution which I had started from a sample project found online, using a `AbstractMonitoringInterceptor` (because it was about logging/monitoring) instead of simply a `MethodInterceptor`. I have modified my own answer and also my sample repository in order to relfect that and hereby want to give you credit for it. This is what happens when an AOP guy who never uses Spring (I normally use native AspectJ) tries to find his bearings in the big Spring framework. – kriegaex Jan 11 '21 at 03:29
5

Maybe programmatic creation of @AspectJ Proxies according to the Spring AOP manual does what you want. Quoting from there because answers with external links only are frowned upon on SO:

// create a factory that can generate a proxy for the given target object
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);

// add an aspect, the class must be an @AspectJ aspect
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager.class);

// you can also add existing aspect instances, the type of the
// object supplied must be an @AspectJ aspect
factory.addAspect(usageTracker);

// now get the proxy object...
MyInterfaceType proxy = factory.getProxy();

Update:

So actually I played around a bit, not being a Spring user but rather an AspectJ expert. But anyway I found a way to dynamically register an advisor with a custom pointcut. The thing is, though, you need to know which beans you want to apply it to, and be careful to differentiate between beans which are already proxied and ones which are not.

Question: When in your application lifecycle and to which beans do you want to add the advisors? Have your other beans already been instantiated and wired (injected) into others? I am asking because it is quite easy to register advisors to beans you have direct references to, wrapping them into proxies or adding the advisors to existing proxies. But there is no obvious way to wrap a bean which has already been injected into other beans and not proxied yet. So how easy or difficult the solution is depends on your requirements.

P.S.: I am still wondering why your pointcuts are in a properties file instead of just in a Spring XML config file, which would be the standard way. That XML file is also loaded during application start-up. Where does the requirement to use another file come from? Both are basically editable (text) resource files.


Update 2: Tedious manual solution, adapted from another sample project

Okay, I have created a GitHub repo for you. Just build with Maven and run the class with the main(..) method. It looks like this:

package de.scrum_master.performancemonitor;

import org.aopalliance.aop.Advice;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class PerformanceApp {
  public static DefaultPointcutAdvisor createAdvisor(String pointcutExpression, Advice advice) {
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression(pointcutExpression);
    return new DefaultPointcutAdvisor(pointcut, advice);
  }

  public static Object adviseIfNecessary(Object bean, DefaultPointcutAdvisor advisor) {
    final String pointcutExpression = advisor.getPointcut().toString().replaceAll(".*\\(\\) ", "");
    if (!advisor.getPointcut().getClassFilter().matches(bean.getClass())) {
      System.out.println("Pointcut " + pointcutExpression + " does not match class " + bean.getClass());
      return bean;
    }
    System.out.println("Pointcut " + pointcutExpression + " matches class " + bean.getClass() + ", advising");
    Advised advisedBean = createProxy(bean);
    advisedBean.addAdvisor(advisor);
    return advisedBean;
  }

  public static Advised createProxy(Object bean) {
    if (bean instanceof Advised) {
      System.out.println("Bean " + bean + " is already an advised proxy, doing nothing");
      return (Advised) bean;
    }
    System.out.println("Creating proxy for bean " + bean);
    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.setTarget(bean);
    return (Advised) proxyFactory.getProxy();
  }

  public static void main(String[] args) {
    DefaultPointcutAdvisor advisor = createAdvisor(
      // Just load this from your YAML file as needed
      "execution(public int de.scrum_master.performancemonitor.PersonService.getAge(..))",
      new MyPerformanceMonitorInterceptor(true)
    );

    ApplicationContext context = new AnnotationConfigApplicationContext(AopConfiguration.class);
    Person person = (Person) adviseIfNecessary(context.getBean("person"), advisor);
    PersonService personService = (PersonService) adviseIfNecessary(context.getBean("personService"), advisor);

    System.out.println();
    System.out.println("Name: " + personService.getFullName(person));
    System.out.println("Age: " + personService.getAge(person));
    System.out.println();

    // BTW, you can also unadvise a bean like this.
    // Write your own utility method for it if you need it.
    ((Advised) personService).removeAdvisor(advisor);
    System.out.println("Name: " + personService.getFullName(person));
    System.out.println("Age: " + personService.getAge(person));
  }
}

The console log looks like this:

Pointcut execution(public int de.scrum_master.performancemonitor.PersonService.getAge(..)) does not match class class de.scrum_master.performancemonitor.Person
Pointcut execution(public int de.scrum_master.performancemonitor.PersonService.getAge(..)) matches class class de.scrum_master.performancemonitor.PersonService$$EnhancerBySpringCGLIB$$965d1d14, advising
Bean de.scrum_master.performancemonitor.PersonService@2fd1433e is already an advised proxy, doing nothing

web - 2018-03-10 09:14:29,229 [main] TRACE d.s.performancemonitor.PersonService - StopWatch 'de.scrum_master.performancemonitor.PersonService.getFullName': running time (millis) = 2
Name: Albert Einstein
web - 2018-03-10 09:14:29,235 [main] INFO  d.s.performancemonitor.PersonService - Method de.scrum_master.performancemonitor.PersonService.getAge execution started at: Sat Mar 10 09:14:29 ICT 2018
web - 2018-03-10 09:14:29,332 [main] INFO  d.s.performancemonitor.PersonService - Method de.scrum_master.performancemonitor.PersonService.getAge execution lasted: 100 ms
web - 2018-03-10 09:14:29,332 [main] INFO  d.s.performancemonitor.PersonService - Method de.scrum_master.performancemonitor.PersonService.getAge execution ended at: Sat Mar 10 09:14:29 ICT 2018
web - 2018-03-10 09:14:29,332 [main] WARN  d.s.performancemonitor.PersonService - Method execution longer than 10 ms!
Age: 146

web - 2018-03-10 09:14:29,334 [main] TRACE d.s.performancemonitor.PersonService - StopWatch 'de.scrum_master.performancemonitor.PersonService.getFullName': running time (millis) = 0
Name: Albert Einstein
Age: 146

You can nicely see how log output from the advisor gets printed. After detaching the advisor again, the log output goes away and only the log output from the advisor defined in class AopConfiguration remains. I.e. you can mix Spring configuration with your own dynamically attached advisors.

BTW, if you comment out the @Bean annotation in AopConfiguration like this

//@Bean
public Advisor performanceMonitorAdvisor() {

then class PersonService will not be proxied already by the time you attach your dynamic advisor and the console output changes to:

Pointcut execution(public int de.scrum_master.performancemonitor.PersonService.getAge(..)) does not match class class de.scrum_master.performancemonitor.Person
Pointcut execution(public int de.scrum_master.performancemonitor.PersonService.getAge(..)) matches class class de.scrum_master.performancemonitor.PersonService, advising
Creating proxy for bean de.scrum_master.performancemonitor.PersonService@6a03bcb1

Name: Albert Einstein
web - 2018-03-10 09:43:04,633 [main] INFO  d.s.performancemonitor.PersonService - Method de.scrum_master.performancemonitor.PersonService.getAge execution started at: Sat Mar 10 09:43:04 ICT 2018
web - 2018-03-10 09:43:04,764 [main] INFO  d.s.performancemonitor.PersonService - Method de.scrum_master.performancemonitor.PersonService.getAge execution lasted: 136 ms
web - 2018-03-10 09:43:04,769 [main] INFO  d.s.performancemonitor.PersonService - Method de.scrum_master.performancemonitor.PersonService.getAge execution ended at: Sat Mar 10 09:43:04 ICT 2018
web - 2018-03-10 09:43:04,769 [main] WARN  d.s.performancemonitor.PersonService - Method execution longer than 10 ms!
Age: 146

Name: Albert Einstein
Age: 146

Please note that not only the log lines produces by the Spring-configured advisor go away as expected but that also the line

Bean de.scrum_master.performancemonitor.PersonService@2fd1433e is already an advised proxy, doing nothing

changes to

Creating proxy for bean de.scrum_master.performancemonitor.PersonService@6a03bcb1

Update 3: More elegant solution according to James W's answer

According to James W's answer, I have modified my solution in order to let Spring automatically create proxy, advisor and let it add the advisor, see commit @ff53e57. The credit for that goes completely to James! Like I said before, I am not a Spring user and was unaware of the handy base class MethodInterceptor which is key to this solution, like James suggested.

For reference, I kept my trick to unadvise the service manually on demand, just had to modify the code to get a reference to the advisor because now it is created by Spring. The updated main program looks like this:

public class PerformanceApp {
  public static void main(String[] args) {
    ApplicationContext context = new AnnotationConfigApplicationContext(AopConfiguration.class);
    Person person = (Person) context.getBean("person");
    PersonService personService = (PersonService) context.getBean("personService");

    System.out.println("Name: " + personService.getFullName(person));
    System.out.println("Age: " + personService.getAge(person));
    System.out.println();

    System.out.println("Unadvising PersonService bean");
    Arrays.stream(((Advised) personService).getAdvisors())
      .filter(advisor -> advisor.getAdvice() instanceof MyPerformanceMonitorInterceptor)
      .findFirst()
      .ifPresent(((Advised) personService)::removeAdvisor);

    System.out.println("Name: " + personService.getFullName(person));
    System.out.println("Age: " + personService.getAge(person));
  }
}

It produces this output:

web - 2021-01-11 10:18:09,277 [main] INFO  d.s.p.MyPerformanceMonitorInterceptor - Method public java.lang.String de.scrum_master.performancemonitor.PersonService.getFullName(de.scrum_master.performancemonitor.Person) execution started at: Mon Jan 11 10:18:09 ICT 2021
web - 2021-01-11 10:18:09,293 [main] INFO  d.s.p.MyPerformanceMonitorInterceptor - Method public java.lang.String de.scrum_master.performancemonitor.PersonService.getFullName(de.scrum_master.performancemonitor.Person) execution lasted: 18 ms
web - 2021-01-11 10:18:09,293 [main] INFO  d.s.p.MyPerformanceMonitorInterceptor - Method public java.lang.String de.scrum_master.performancemonitor.PersonService.getFullName(de.scrum_master.performancemonitor.Person) execution ended at: Mon Jan 11 10:18:09 ICT 2021
web - 2021-01-11 10:18:09,293 [main] WARN  d.s.p.MyPerformanceMonitorInterceptor - Method execution longer than 10 ms!
Name: Albert Einstein
Age: 149

Unadvising PersonService bean
Name: Albert Einstein
Age: 149
kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • This won't work because I only know the targets via configuration file (at runtime). – Miguel Ping Mar 03 '18 at 21:55
  • 3
    This is also true for normal aspects configured via XML or load-time weaving. Please edit your question and describe your use case, i.e. what you want to achieve and why you think normal Spring AOP or AspectJ are inadequate. It would help me and others answer your question better without wasting time for answers you tell us afterwards "won't work". Thank you. But BTW, what Alex Savitsky said would also work, just create many potentially unneeded proxies (Spring AOP) or weave many classes without proxies (AspectJ). – kriegaex Mar 04 '18 at 03:56
  • I have updated my answer with more info and follow-up questions. I think we can work our way to the solution you need from there. – kriegaex Mar 07 '18 at 12:51
  • we are writing a lib and want to let consumers use our AOP advice according to a config file: `Label: PointcutpExpr` where we will use the `Label` to do additional tracing. We want to keep it all in a .yml config. In terms of application lifecycle, it would be on startup, before the beans are used. – Miguel Ping Mar 08 '18 at 14:56
  • Then what keeps you from generating a Spring XML config file from your YML during the build process or during start-up in a batch file? Or just tell the consumers to put their pointcuts - if they even understand how to write them - directly into the XML config. But for what it is worth, later today after work or during the weekend I can show you my code if you still insist in doing Spring's work (auto-proxy creation) manually. I don't like it, but it works for simple cases and at least you have a starting point. – kriegaex Mar 09 '18 at 01:49
  • Please check out my massive update #2 with sample code and link to GitHub project. – kriegaex Mar 10 '18 at 02:46
  • I marked your answer as correct, thanks for the effort. I ended up going a slightly different route, with a spring application event listener configured in `META-INF/spring.factories` – Miguel Ping Mar 12 '18 at 09:51
  • Can you share that?? Please? – Anand Varkey Philips Jan 10 '21 at 17:28
  • FYI, I have updated again, both this answer and the GitHub repo, according to James W's idea which is way less tedious to implement than mine. Now I think we have a nice and maintainable solution. Kudos to James! – kriegaex Jan 11 '21 at 03:30
0

Define a abstract Aop Class,like this:

public abstract class ControllerAop {
  public abstract void webaop();

 @Around(value = "webaop()")
 public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
   ...
 }
}

in your project,you can define a child class extends above aop class.

import com.chuxingyouhui.pdsc.knc.aop.ControllerAop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;


@Aspect
@Component
@Order(2)
public class ApiControllerAop extends ControllerAop {

   @Override
   @Pointcut(value = "execution(* com.chuxingyouhui.white..*.controller..*.*(..))")
   public void webaop() {
 }
}

if you have more pointcut path,you define more child class .