返回

Spring AOP 切点暂停与恢复:解决批量处理性能问题

java

如何暂停和恢复 Spring AOP 切点

开发中, 给 JPA 仓库的 save, saveAll 等方法加了些 AOP 切点, 用于在插入、更新实体时刷新物化视图。

可问题来了,批量处理时会频繁调用这些 save 方法。(而且,现有代码过于复杂,没法改成最后只调用一次 saveAll 的方式。)

有没有简单办法,让 Spring 在批量处理期间暂停特定的 AOP 切点,然后在需要的时候恢复它?

// 批量处理开始
// Spring,请暂停 AOP 切点

for(...) {
  // 一些业务逻辑
  myEntityRepository.save(myEntity);
}

// 好了,Spring,请恢复 AOP 切点
// 批量处理结束

问题根源

Spring AOP 的默认行为是,一旦定义了切点,它就会一直处于激活状态。 框架本身并没有提供直接从外部暂停或恢复特定切点的 API。 相关拦截器会对每次方法调用生效,没有提供暂停、恢复的开关。

解决方案

下面介绍几种处理这个问题的方法。

1. 使用自定义注解 + ThreadLocal

这种方法通过自定义注解标记需要忽略切点的方法,并在切点中使用 ThreadLocal 来存储和传递暂停状态。

原理:

  1. 创建一个自定义注解,例如 @IgnoreRefreshMaterializedView
  2. 在 AOP 切点中,检查当前线程的 ThreadLocal 变量中是否设置了“暂停”标志。
  3. 如果在 ThreadLocal 里找到了"暂停"标记,切点逻辑就不执行,直接跳过。
  4. 在批量处理开始前,设置 ThreadLocal 中的“暂停”标志。
  5. 在批量处理结束后,清除 ThreadLocal 中的“暂停”标志。
  6. 在切点检查方法上是否包含该注解,如果包含也直接跳过.

代码示例:

1.1 自定义注解:

package com.example.demo.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

}

1.2 ThreadLocal 工具类:

package com.example.demo.aop;

public class AopContext {

    private static final ThreadLocal<Boolean> ignoreRefreshMaterializedView = ThreadLocal.withInitial(() -> Boolean.FALSE);

    public static void setIgnoreRefreshMaterializedView(Boolean ignore) {
        ignoreRefreshMaterializedView.set(ignore);
    }

    public static Boolean getIgnoreRefreshMaterializedView() {
        return ignoreRefreshMaterializedView.get();
    }

    public static void clear() {
        ignoreRefreshMaterializedView.remove();
    }
}

1.3 切点定义:

package com.example.demo.aop;

import com.example.demo.annotation.IgnoreRefreshMaterializedView;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Component
public class RefreshMaterializedViewAspect {

    @Around("execution(* com.example.demo.repository.*.save*(..))")
    public Object aroundSaveMethods(ProceedingJoinPoint joinPoint) throws Throwable {

         // 如果设置了忽略标志,直接跳过
        if (Boolean.TRUE.equals(AopContext.getIgnoreRefreshMaterializedView())) {
          return joinPoint.proceed();
        }

         //check if the method is annotated with @IgnoreRefreshMaterializedView
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        if (method.isAnnotationPresent(IgnoreRefreshMaterializedView.class)) {
           return joinPoint.proceed();
        }
        
        // 原有的切面逻辑,例如刷新物化视图
        Object result = joinPoint.proceed();
        System.out.println("Refreshing materialized view...");
        return result;
    }
}

1.4 使用示例:

package com.example.demo.service;

import com.example.demo.aop.AopContext;
import com.example.demo.entity.MyEntity;
import com.example.demo.repository.MyEntityRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    @Autowired
    private MyEntityRepository myEntityRepository;

    public void batchProcess() {
        try {
            // 设置忽略标记
            AopContext.setIgnoreRefreshMaterializedView(true);

            for (int i = 0; i < 100; i++) {
                MyEntity entity = new MyEntity();
                entity.setName("Entity " + i);
                myEntityRepository.save(entity);
            }

        } finally {
            // 清除忽略标记,确保后续操作正常执行 AOP 逻辑
            AopContext.clear();
        }

       // AOP will work
        myEntityRepository.save(new MyEntity());
    }
}

安全建议:

  • 务必在使用完 ThreadLocal 后清除其值, 防止内存泄漏。finally 块是做这件事的好地方。

进阶用法
可以将自定义注解改为类和方法级别通用。
可以利用ThreadLocal传递更多的控制上下文,提供更精细的控制.

2. 使用 Spring 提供的 ProxyFactoryBean (不推荐用于生产)

这种方式通过手动创建代理对象来控制 AOP 的应用。虽然可以达到目的,但不建议在生产环境中使用,因为过于复杂且容易出错。

原理:

  1. 获取原始的 Repository Bean。
  2. 使用 ProxyFactoryBean 手动创建一个代理,并在其中配置需要应用的切面。
  3. 在批量处理期间,使用原始的 Repository Bean,绕过代理。
  4. 在批量处理结束后,重新使用代理对象。

代码示例 (仅作演示,不建议生产使用):

//  获取原始 Bean 和 代理 Bean (大致逻辑, 非完整代码)

@Autowired
private MyEntityRepository myEntityRepository;

@Autowired
private ApplicationContext applicationContext;

private MyEntityRepository originalRepository;
private MyEntityRepository proxiedRepository;


@PostConstruct
public void init() {
    proxiedRepository = myEntityRepository; // 一开始拿到的是代理对象

     // 通过 Bean 名称获取原始对象
    originalRepository = (MyEntityRepository) applicationContext.getBean("myEntityRepository"); // 这里假设 Bean 名称是 "myEntityRepository"

   // 如果上述方法获取失败,可以尝试从代理中反向解析,代码会更复杂一些
}

public void batchProcess() {

        //使用原始对象,绕过 AOP
        for (int i = 0; i < 100; i++) {
            MyEntity entity = new MyEntity();
            entity.setName("Entity " + i);
            originalRepository.save(entity);
        }
       //在其他需要使用aop的方法里使用: proxiedRepository

        // 恢复, 但比较麻烦, 通常需要将repository注入到不同的字段里, 然后手动控制bean的替换.
}

强烈不推荐此方法原因:

  • 代码复杂,容易出错。
  • 需要手动处理 Bean 的获取和替换, 破坏了 Spring 的 IoC/DI 原则。
  • 维护困难, 可读性差。

3. 条件化配置切面 (Configuration-based Aspect Enabling)

利用 Spring 的 profile 或条件化配置来启用或禁用整个切面。

原理:

  1. 将切面定义放在一个特定的配置类中。
  2. 使用 @Profile@Conditional 注解控制该配置类的加载。
  3. 在批量处理时, 使用不包含该切面配置的 profile。
  4. 其他时候, 使用包含该切面配置的 profile。

代码示例:

3.1 切面配置类:

package com.example.demo.config;

import com.example.demo.aop.RefreshMaterializedViewAspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
@Profile("!batch-processing") // 除了 batch-processing 这个 profile,其他情况都启用
public class AopConfig {

    @Bean
    public RefreshMaterializedViewAspect refreshMaterializedViewAspect() {
        return new RefreshMaterializedViewAspect();
    }
}

3.2 启动参数/配置文件:

在启动应用时,根据是否进行批量处理来指定不同的 profile。 例如:

  • 批量处理时: -Dspring.profiles.active=batch-processing
  • 非批量处理时: -Dspring.profiles.active=default (或任何其他 profile)

或者,可以在 application.propertiesapplication.yml 中配置。

这种方法的优点:

  • 配置简单, 容易理解。
  • 不需要修改业务代码。

缺点:

  • 只能控制整个切面,无法针对单个方法或更细粒度的控制。
  • 需要重启应用才能切换 profile。

4 使用自定义 BeanFactoryPostProcessor (进阶,修改bean的定义)

可以更底层地控制 AOP 行为。通过实现 BeanFactoryPostProcessor 接口,可以在 Spring 容器实例化 bean 之前 修改 bean 的定义。 可以移除特定 bean 的 Advisor。

原理:

  1. 找到相关的 Advisor bean (例如,根据名称、类型等)。
  2. postProcessBeanFactory阶段将切面(Advisor)从相关的bean definition中移除。
  3. ThreadLocal里保存状态,或者处理其他标记来控制何时执行以上操作。
  4. 要恢复AOP,要么重启应用(简单但不方便),或者保存原始的 Bean 定义,在需要时重新添加 Advisor(复杂).

代码示例 (恢复部分略,仅做思路演示):


import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;
import java.util.Arrays;

@Component
public class AopDisablingBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

   private static final String TARGET_ASPECT_BEAN_NAME = "refreshMaterializedViewAspect"; // 替换成你的切面 Bean 名称
  // private static final String TARGET_REPOSITORY_BEAN_NAME = ".*Repository"; // 用正则表达式匹配多个 repository

   @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

        if (Boolean.TRUE.equals(AopContext.getIgnoreRefreshMaterializedView())) {

               // Find the Advisor (aspect) bean
               String[] advisorBeanNames = beanFactory.getBeanNamesForType(Advisor.class);
              //  System.out.println("all advisor beans: " + Arrays.toString(advisorBeanNames));

                String advisorBeanNameToRemove = null;
                 for(String name : advisorBeanNames){
                       if(name.contains(TARGET_ASPECT_BEAN_NAME)){ //注意这里用了contains, 可能匹配多个
                            advisorBeanNameToRemove= name;
                             break;
                       }
                  }

               if(advisorBeanNameToRemove == null){
                    return; // No advisor to disable.
                }

              // 遍历所有 bean 定义,找出应用了目标切面的bean,从bean定义上删除该切面.
                for (String beanName : beanFactory.getBeanDefinitionNames()) {
                   // System.out.println("Checking bean: " + beanName);
                    BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);

                    // 根据beanDefinition查找是否被AOP代理. 此处代码根据Spring版本和AOP实现方式可能有很大差别,需要调试分析.
                   //以下只是一种可能的示例, 实际应用中需要根据调试情况调整
                     if(beanDefinition.hasPropertyValues()){
                         if (beanDefinition.getPropertyValues().contains("advisors")) {
                              //这里是极其粗略的演示,假设advisors是一个数组,并且能通过名称移除。
                             // 真实情况可能需要类型判断,反射, 获取内部数据结构等等...
                              //remove(beanDefinition.getPropertyValues().get("advisors"), advisorBeanNameToRemove);
                              // 只是演示移除思路
                             System.out.println("==>Found AOP on bean: " + beanName+", will remove aspect:"+advisorBeanNameToRemove);

                        }
                   }
                   // 或 通过attribute
                   /*
                    if (beanDefinition.hasAttribute("org.springframework.aop.framework.autoproxy.AutoProxyUtils.originalTargetClass"))
                      if (Arrays.stream(beanFactory.getBeanDefinition(beanName).getRole()).anyMatch(role -> role == BeanDefinition.ROLE_INFRASTRUCTURE)){
                     //....
                   }
                  */

               }
          }
    }

     // private void removeAdvisor(List<?> advisors, String advisorBeanNameToRemove){ ... }

}

注意事项:

  • 这种方法直接操作了 Spring 容器的底层, 比较复杂, 容易引入问题。
  • 需要对 Spring AOP 的内部机制有一定了解。
  • 代码需要调试才能正确执行,并且不同的Spring版本行为可能有差异。
  • 恢复 AOP 的代码更复杂, 这里没有提供, 因为这通常不是一个好主意(维护原始 bean 定义,重新注册等...非常繁琐且容易出错). 更好的方式是配合方案 3,用profile控制.

建议:
通常情况下, 优先选择方案 1 (自定义注解+ThreadLocal),因为它最简单、最安全,也易于理解和维护.方案 3 (条件化配置)在不需要频繁切换 AOP 状态的情况下也很好用。 方案 4 作为了解 Spring 底层机制的例子就好。