Search code examples
javaspringspring-aop

Spring AOP aspect doesn't work in multi-module project


Let me simplify my question as below:

I have a Java Maven project called project-parent, in it I have multiple child module projects.

One of the project is called project-common, I put all the self-defined Spring AOP Aspects used across the whole project in there. I have written unit tests in project-common, and the aspect works properly as I expected in unit tests.

Then, I want to apply those Aspects in other modules. One of the submodules is called project-service. The aspect I apply to the method in the service should do auth management before and after the service method. However, I found those Aspects not working when the service is running. Also, project-service has a maven dependency on project-common.

The project structure is like below

project-parent
  -- project-common (in which define the aspect)
  -- project-service (where my aspect is used)
  ...
  -- other submodules omitted for simplicity

My Aspect defined is like:

    @Aspect
    @Component
    public class RequestServiceSupportAspect {
        @Pointcut(value = "@annotation(RequestServiceSupport)")
        public void matchMethod() {
            // pointcut
        }

        @Around("matchMethod()")
        public Object basicAuthSupport(ProceedingJoinPoint joinPoint) {
            ...
        }
    }

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface RequestServiceSupport {
    }

My Service using the Aspect is like:

  public class RequestServiceImpl implements RequestService {
      ...

      @RequestServiceSupport
      public Request addComment(Comment comment) {
          ...
      }
  }

Solution

  • I finally resolve the issue and got a chance to learn how Spring AOP works in the background and if it doesn't works how we can debug this kind of AOP Aspect issue.

    The root cause

    The root cause is that the aspect is not a Bean manged by Spring in project-service. Just add the below in project-service's Config class will solve the issue:

        @Configuration
        public class ServiceConfig {
            ...
    
            @Bean
            public RequestServiceSupportAspect requestServiceSupportAspect() {
                return new RequestServiceSupportAspect();
            }
    
        }
    

    The reason RequestServiceSupportAspect works in unit tests written in project-common is that we use @Component on the aspect definition and within project-common, there is a RequestServiceSupportAspect bean manged by the Spring, thus the aspect works.

    However in another submodule project-service, Aspect Class annotated with @Component will not create a Bean managed by Spring in default, as it is not in the path scanned by the SpringBoot application. You need to either mannually declare a Bean definition in the Config class or you need to define a Aspect bean in project-common and import the Config file or let project-commmon expose it by configuring resources/META-INF/spring.factories like below:

        org.springframework.boot.autoconfigure.EnableAutoConfiguration=xxxConfiguration
    

    How AOP works behind the scenes

    The above explanation should have solve the problem. But if you are interested how I get there, the below may provides some hints.

    1. I first start to check if my service bean has been proxied. The answer is no, I just see a raw bean, so I start to think how the aspect works at runtime and proxy the direct call to the real service so the aspect can add its action on it.
    2. After some digging, I found out that the BeanPostProcessor is a critical entry point to look into. First, we can dive into the below annotation chain:
        @EnableAspectJAutoProxy
        --> AspectJAutoProxyRegistrar
        --> AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary
        --> AnnotationAwareAspectJAutoProxyCreator.class
    

    If you see the hierarchy of AnnotationAwareAspectJAutoProxyCreator, it implements BeanPostProcessor interface, it is reasonable because then the Spring will be able to add proxy to the class where there is aspect bound to. It has two methods to implement:

    public interface BeanPostProcessor {
      
      @Nullable
      default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
      }
      
      @Nullable
      default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
      }
    }
    
    1. I start to read how AnnotationAwareAspectJAutoProxyCreator implements the method, and I find out that it is its base class AbstractAutoProxyCreator which implement it like below:
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName)
        throws BeansException {
          if (bean != null) {
             Object cacheKey = getCacheKey(bean.getClass(), beanName);
             if (!this.earlyProxyReferences.contains(cacheKey)) {
                return **wrapIfNecessary**(bean, beanName, cacheKey);
             }
          }
          return bean;
        }
    

    It is very obvious that wrapIfNecessary is the place where aspect proxy are added to Bean. I make a break point here and check what's going wrong.

    1. In the wrapIfNecessary method, I found that when my service bean is created, it go into the branch of DO_NOT_PROXY.
    protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
      if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
          return bean;
      }
      if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
          return bean;
      }
      if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
          this.advisedBeans.put(cacheKey, Boolean.FALSE);
          return bean;
      }
    
      // Create proxy if we have advice.
      Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
      if (specificInterceptors != DO_NOT_PROXY) {
          this.advisedBeans.put(cacheKey, Boolean.TRUE);
          Object proxy = createProxy(
              bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
          this.proxyTypes.put(cacheKey, proxy.getClass());
          return proxy;
      }
    
      this.advisedBeans.put(cacheKey, Boolean.FALSE);
      return bean;
    }
    

    The reason for this is getAdvicesAndAdvisorsForBean doesn't returned the my desired Aspect.

    I dig into getAdvicesAndAdvisorsForBean and find out that BeanFactoryAspectJAdvisorsBuilder::buildAspectJAdvisors is the place where all the candidates Bean are being imported.

    It initialized aspectNames exactly once using code often seen in singleton pattern which will be later used at BeanNameAutoProxyCreator::getAdvicesAndAdvisorsForBean to fetch the aspect you created.

    Then I find that it is the Aspect Bean not included in this project that makes my Aspect not working.

    1. If you look into the wrapIfNecessary method, you will also find the different proxy Spring AOP will create for its bean class
      public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
    
        @Override
        public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
            if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
                Class<?> targetClass = config.getTargetClass();
                if (targetClass == null) {
                    throw new AopConfigException("TargetSource cannot determine target class: " +
                            "Either an interface or a target is required for proxy creation.");
                }
                if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
                    return new JdkDynamicAopProxy(config);
                }
                return new ObjenesisCglibAopProxy(config);
            }
            else {
                return new JdkDynamicAopProxy(config);
            }
        }
    
        ...
      }
    

    If AOP Aspect is not working, how we can debug the issue

    If you find your Aspect is not working, make a break point at the below location:

      AbstractAutoProxyCreator::postProcessAfterInitialization() -> wrapIfNecessary
    

    Add conditional breakpoint filter for the service bean you want to add aspect for, step by step executing will leads you to the root cause.

    Summary

    Though the investigation process takes me some time, in the end, the root cause is quite simple and straight-foward. However, in our daily work, some of us may easily overlook it. That's the reason why I post my answer here so that in the future if anyone encounter with similar issue, the post may save some time for them.