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

Jso*_*son 3 java spring spring-aop

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

我有一个名为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
Run Code Online (Sandbox Code Playgroud)

我的方面定义如下:

    @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 {
    }
Run Code Online (Sandbox Code Playgroud)

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

  public class RequestServiceImpl implements RequestService {
      ...

      @RequestServiceSupport
      public Request addComment(Comment comment) {
          ...
      }
  }
Run Code Online (Sandbox Code Playgroud)

Jso*_*son 8

我终于解决了这个问题,并有机会了解 Spring AOP 在后台如何工作,以及如果它不起作用我们如何调试此类 AOP Aspect 问题。

根本原因

根本原因是aspect不是project-service中Spring管理的Bean。只需在项目服务的 Config 类中添加以下内容即可解决该问题:

    @Configuration
    public class ServiceConfig {
        ...

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

    }
Run Code Online (Sandbox Code Playgroud)

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

然而,在另一个子模块project-service中,使用@Component注解的Aspect Class默认不会创建Spring管理的Bean,因为它不在SpringBoot应用程序扫描的路径中。您需要在 Config 类中手动声明 Bean 定义,或者需要在 project-common 中定义 Aspect bean 并导入 Config 文件,或者让 project-common 通过配置 resources/META-INF/spring.factories 来公开它,如下所示:

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=xxxConfiguration
Run Code Online (Sandbox Code Playgroud)

AOP 在幕后如何工作

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

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

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

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;
  }
}
Run Code Online (Sandbox Code Playgroud)
  1. 我开始阅读AnnotationAwareAspectJAutoProxyCreator如何实现该方法,我发现它是它的基类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;
    }
Run Code Online (Sandbox Code Playgroud)

很明显,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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

    ...
  }
Run Code Online (Sandbox Code Playgroud)

如果 AOP Aspect 不起作用,我们如何调试该问题

如果您发现 Aspect 不起作用,请在以下位置下一个断点:

  AbstractAutoProxyCreator::postProcessAfterInitialization() -> wrapIfNecessary
Run Code Online (Sandbox Code Playgroud)

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

概括

虽然调查过程花了我一些时间,但最终,根本原因非常简单明了。然而,在我们的日常工作中,有些人可能很容易忽视这一点。这就是我在这里发布我的答案的原因,以便将来如果有人遇到类似问题,该帖子可能会为他们节省一些时间。