Spring AOP 方面在多模块项目中不起作用

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

让我将我的问题简化如下:

我有一个名为 project-parent 的 Java Maven 项目,其中有多个子模块项目。

其中一个项目叫做project-common,我把整个项目中用到的所有自定义Spring AOP Aspects都放在那里。我已经在 project-common 中编写了单元测试,并且该方面按照我在单元测试中的预期正常工作。

然后,我想在其他模块中应用这些方面。其中一个子模块称为项目服务。我在服务中申请方法的方面应该在服务方法前后进行授权管理。但是,我发现当服务 运行 时,那些方面不工作。此外,project-service 对 project-common 有 maven 依赖。

项目结构如下

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

我定义的方面是这样的:

    @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 {
    }

我使用方面的服务是这样的:

  public class RequestServiceImpl implements RequestService {
      ...

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

我终于解决了这个问题,并有机会了解 Spring AOP 如何在后台工作,如果它不起作用,我们如何调试这种 AOP 方面的问题。

根本原因

根本原因是该方面不是项目服务中 Spring 管理的 Bean。只需在项目服务的配置中添加以下内容 class 即可解决问题:

    @Configuration
    public class ServiceConfig {
        ...

        @Bean
        public RequestServiceSupportAspect requestServiceSupportAspect() {
            return new RequestServiceSupportAspect();
        }

    }

RequestServiceSupportAspect 在用 project-common 编写的单元测试中工作的原因是我们在 aspect 定义上使用 @Component 并且在 project-common 中,有一个由 Spring 管理的 RequestServiceSupportAspect bean,因此方面有效。

但是在另一个子模块项目服务中,使用@Component注解的Aspect Class默认不会创建由Spring管理的Bean,因为它不在[=扫描的路径中92=]启动应用程序。您需要在 Config class 中手动声明一个 Bean 定义,或者您需要在 project-common 中定义一个 Aspect bean 并导入 Config 文件,或者让 project-commmon 通过配置 resources/META-INF/spring.factories 公开它如下所示:

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=xxxConfiguration

AOP背后是如何工作的

以上解释应该已经解决问题了。但如果你对我如何到达那里感兴趣,下面可能会提供一些提示。

  1. 我首先开始检查我的服务bean 是否已被代理。 答案是否定的,我只是看到一个原始 bean,所以我开始思考方面如何在运行时工作并将直接调用代理到真实服务,以便方面可以在其上添加它的操作。
  2. 经过一些挖掘,我发现 BeanPostProcessor 是一个需要研究的关键入口点。 首先,我们可以深入了解以下注释链:
    @EnableAspectJAutoProxy
    --> AspectJAutoProxyRegistrar
    --> AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary
    --> AnnotationAwareAspectJAutoProxyCreator.class

如果你看到 AnnotationAwareAspectJAutoProxyCreator 的层次结构,它实现了 BeanPostProcessor 接口,这是合理的,因为这样 Spring 就可以将代理添加到 class 有方面绑定的地方。 它有两种实现方法:

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. 我开始阅读 AnnotationAwareAspectJAutoProxyCreator 是如何实现该方法的,我发现它是它的基础 class AbstractAutoProxyCreator 实现它如下所示:
    @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;
    }

很明显wrapIfNecessary是Bean添加aspect proxy的地方。 我这里打个断点,看看哪里出了问题。

  1. 在 wrapIfNecessary 方法中,我发现当我的服务 bean 创建时,它进入了 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;
}

原因是 getAdvicesAndAdvisorsForBean 没有返回我想要的方面。

我深入研究了 getAdvicesAndAdvisorsForBean,发现 BeanFactoryAspectJAdvisorsBuilder::buildAspectJAdvisors 是导入所有候选 Bean 的地方。

它使用单例模式中常见的代码仅初始化了一次 aspectNames,稍后将在 BeanNameAutoProxyCreator::getAdvicesAndAdvisorsForBean 中使用该代码来获取您创建的方面。

然后我发现是这个项目中没有包含的Aspect Bean导致我的Aspect无法工作。

  1. 如果查看 wrapIfNecessary 方法,您还会发现不同的代理 Spring AOP 将为其 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);
        }
    }

    ...
  }

如果 AOP Aspect 不工作,我们如何调试问题

如果您发现您的 Aspect 不工作,请在以下位置打断点:

  AbstractAutoProxyCreator::postProcessAfterInitialization() -> wrapIfNecessary

为要为其添加方面的服务 bean 添加条件断点过滤器,逐步执行将引导您找到根本原因。

总结

虽然调查过程花了我一些时间,但归根结底,根本原因非常简单明了。然而,在我们的日常工作中,我们中的一些人可能很容易忽视它。这就是为什么我 post 在这里回答的原因,以便将来如果有人遇到类似问题,post 可能会为他们节省一些时间。

您可以添加(或扩展)@ComponentScan 以使方面组件可访问,而不是像 @Bean 那样显式提供方面。

@ComponentScan("my.aspect.package") // << use relevant package from 'project-common'
@Configuration
public class ServiceConfig {
   ...
}