Search code examples
javaspringspring-bootspring-aopspring5

Spring AOP pointcut is not triggered on an Object's method call when it is available inside list


I have a spring boot application with the couple of classes, configuration class and aspect as below. The below example is to illustrate the issue I face.

I have office class which has list of printers as dependency that is been created using external property file configuration. I would like to execute an aspect whenever Printer.getFilename method is been called. It is not triggering the aspect If I have list of Printers but It works when I have single Printer object without list.

package com.example

public class Office {
   private final List<Printer> printersList;

   public Office(Printer printersList){
     this.printersList = printersList;
   }

   public void printFiles(){
      for(Printer printer: printersList)
        printer.getFileName();
   }
}
package com.example

public class Printer {
  private deviceId;

  public String getFileName(){
     return "fileName";
  }
}
@Configuration
public class ApplicationConfiguration{
  @Bean
  public Office office(){
    List<Printer> printerList = new ArrayList<>();
    // Adding to the list based on printer id based on some external property file configuration
    printerList.add(new Printer());
    printerList.add(new Printer());
    return new Office(printerList);
  }
}
@Aspect
@Component
public class PrinterFileNameAspect {
    @Pointcut("execution(* com.example.Printer.getFileName())")
    private void getFileNameJp() {}

    @Around("getFileNameJp()")
    public String returnFileName(ProceedingJoinPoint pjp) {
        return "Modified File Name";
    }
} 

I find that the list of beans are not registered with the Spring container. Hence I modified the configuration class to register the bean

@Configuration
public class ApplicationConfiguration{
  @Autowired
  private GenericWebApplicationContext context;

  @Bean
  public Office office(){
    List<Printer> printerList = new ArrayList<>();
    // Adding to the list based on printer id
    Printer colorPrinter = new Printer();
    context.registerBean("colorPrinter", Printer.class, () -> colorPrinter);
    printerList.add(colorPrinter);
    Printer specialPrinter = new Printer();
    context.registerBean("specialPrinter", Printer.class, () -> specialPrinter);
    printerList.add(specialPrinter);
    return new Office(printerList);
  }
}

The above configuration changes doesn't help. I think I miss something in the fundamentals of spring aop. I want to implement the spring aop with the list of Printer since I can't change the list generation logic (The list generation logic is complex one and has to be dynamic).


Solution

  • I am adding an alternative answer because you seem to keen to learn how to use the new method GenericApplicationContext.registerBean(..) introduced in Spring 5. As I am not a Spring user, I also wanted to find out what that is about and came up with this solution.

    Again, I am providing full class definitions. They are similar, but slightly different from my first answer. Specifically, Printer is no longer a prototype-scoped @Component but a POJO. I still left Office to be a singleton component for convenience, though. If you also need multiple instances there, you can always adjust the code according to your needs.

    Now what is important and fixes your problem is this: After registering beans programmatically, you ought to acquire them from the application context via getBean() and not just add the manually created POJO instances to your printer list. Only if you get beans from the application context Spring takes care of also creating AOP proxies where necessary.

    package de.scrum_master.spring.q61661740;
    
    public class Printer {
      private String deviceId;
    
      public Printer(String deviceId) {
        this.deviceId = deviceId;
      }
    
      public String getFileName() {
        return deviceId + ".pdf";
      }
    }
    
    package de.scrum_master.spring.q61661740;
    
    import org.springframework.stereotype.Component;
    
    import java.util.ArrayList;
    import java.util.List;
    
    @Component
    public class Office {
      private final List<Printer> printersList = new ArrayList<>();
    
      public void addPrinter(Printer printer) {
        printersList.add(printer);
      }
    
      public void printFiles() {
        for (Printer printer : printersList)
          System.out.println(printer.getFileName());
      }
    }
    
    package de.scrum_master.spring.q61661740;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.springframework.stereotype.Component;
    
    @Aspect
    @Component
    public class PrinterFileNameAspect {
      // Package name is optional if aspect is in same name as Printer
      @Pointcut("execution(* de.scrum_master.spring.q61661740.Printer.getFileName())")
      private void getFileNameJp() {}
    
      @Around("getFileNameJp()")
      public String returnFileName(ProceedingJoinPoint pjp) throws Throwable {
        return "modified_" + pjp.proceed();
      }
    }
    
    package de.scrum_master.spring.q61661740;
    
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    import java.util.stream.Stream;
    
    @SpringBootApplication
    @Configuration
    @EnableAspectJAutoProxy
    public class Application {
      public static void main(String[] args) {
        try (AnnotationConfigApplicationContext appContext = new AnnotationConfigApplicationContext(Application.class)) {
          // If you want to get rid of the `@SpringBootApplication` annotation, add this:
          // appContext.scan(Application.class.getPackage().getName());
          Office office = appContext.getBean(Office.class);
          Stream
            .of("colorPrinter", "specialPrinter")
            .forEach(deviceID -> {
              appContext.registerBean(deviceID, Printer.class, () -> new Printer(deviceID));
              office.addPrinter(appContext.getBean(deviceID, Printer.class));
            });
          office.printFiles();
        }
      }
    }
    

    The console log looks like this (shortened):

    (...)
    18:20:54.169 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'office'
    18:20:54.177 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'colorPrinter'
    18:20:54.178 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'specialPrinter'
    colorPrinter.pdf
    specialPrinter.pdf
    (...)