Spring AOP 切点暂停与恢复:解决批量处理性能问题
2025-03-08 22:08:18
如何暂停和恢复 Spring AOP 切点
开发中, 给 JPA 仓库的 save
, saveAll
等方法加了些 AOP 切点, 用于在插入、更新实体时刷新物化视图。
可问题来了,批量处理时会频繁调用这些 save
方法。(而且,现有代码过于复杂,没法改成最后只调用一次 saveAll
的方式。)
有没有简单办法,让 Spring 在批量处理期间暂停特定的 AOP 切点,然后在需要的时候恢复它?
// 批量处理开始
// Spring,请暂停 AOP 切点
for(...) {
// 一些业务逻辑
myEntityRepository.save(myEntity);
}
// 好了,Spring,请恢复 AOP 切点
// 批量处理结束
问题根源
Spring AOP 的默认行为是,一旦定义了切点,它就会一直处于激活状态。 框架本身并没有提供直接从外部暂停或恢复特定切点的 API。 相关拦截器会对每次方法调用生效,没有提供暂停、恢复的开关。
解决方案
下面介绍几种处理这个问题的方法。
1. 使用自定义注解 + ThreadLocal
这种方法通过自定义注解标记需要忽略切点的方法,并在切点中使用 ThreadLocal
来存储和传递暂停状态。
原理:
- 创建一个自定义注解,例如
@IgnoreRefreshMaterializedView
。 - 在 AOP 切点中,检查当前线程的
ThreadLocal
变量中是否设置了“暂停”标志。 - 如果在
ThreadLocal
里找到了"暂停"标记,切点逻辑就不执行,直接跳过。 - 在批量处理开始前,设置
ThreadLocal
中的“暂停”标志。 - 在批量处理结束后,清除
ThreadLocal
中的“暂停”标志。 - 在切点检查方法上是否包含该注解,如果包含也直接跳过.
代码示例:
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 的应用。虽然可以达到目的,但不建议在生产环境中使用,因为过于复杂且容易出错。
原理:
- 获取原始的 Repository Bean。
- 使用
ProxyFactoryBean
手动创建一个代理,并在其中配置需要应用的切面。 - 在批量处理期间,使用原始的 Repository Bean,绕过代理。
- 在批量处理结束后,重新使用代理对象。
代码示例 (仅作演示,不建议生产使用):
// 获取原始 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 或条件化配置来启用或禁用整个切面。
原理:
- 将切面定义放在一个特定的配置类中。
- 使用
@Profile
或@Conditional
注解控制该配置类的加载。 - 在批量处理时, 使用不包含该切面配置的 profile。
- 其他时候, 使用包含该切面配置的 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.properties
或 application.yml
中配置。
这种方法的优点:
- 配置简单, 容易理解。
- 不需要修改业务代码。
缺点:
- 只能控制整个切面,无法针对单个方法或更细粒度的控制。
- 需要重启应用才能切换 profile。
4 使用自定义 BeanFactoryPostProcessor
(进阶,修改bean的定义)
可以更底层地控制 AOP 行为。通过实现 BeanFactoryPostProcessor
接口,可以在 Spring 容器实例化 bean 之前 修改 bean 的定义。 可以移除特定 bean 的 Advisor。
原理:
- 找到相关的 Advisor bean (例如,根据名称、类型等)。
- 在
postProcessBeanFactory
阶段将切面(Advisor)从相关的bean definition中移除。 - 在
ThreadLocal
里保存状态,或者处理其他标记来控制何时执行以上操作。 - 要恢复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 底层机制的例子就好。