You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring非Boot项目AspectJ配置引发的事务与EntityManager异常问题

问题描述

我有一个非Spring Boot的Spring项目,启用<aop:aspectj-autoproxy />后,@PersistenceContext和@Transactional注解出现异常行为:

1. 配置<aop:aspectj-autoproxy proxy-target-class="true" />时的问题

外部调用delete方法,其内部调用find方法时EntityManager为null,但外部直接调用find方法正常。通用类代码如下:

@Transactional(readOnly = false, propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public class GenericServiceImpl<T extends IEntity<PK>, PK extends Serializable> implements IGenericService<T, PK> {
    private final Class<T> type;
    
    public GenericServiceImpl(final Class<T> type) {
        this.type = type;
    }

    private EntityManager em;
    
    @PersistenceContext
    public final void setEntityManager(final EntityManager em) {
        this.em = em;
    }

    public final EntityManager getEntityManager() {
        return em;
    }
 
    public final void delete(final PK pk) { // 外部调用此方法
        T o = find(pk); // 内部调用失败,em为null
        if (o != null) {
            o.setDeleted(true);
            save(o);
        }
    }

    @Transactional(readOnly = true)  // 外部直接调用此方法正常
    public final T find(final PK pk) {
        if (pk == null) {
            return null;
        }

        T o = em.find(type, pk); // 从delete内部调用时em为null
        if (o != null && o.isDeleted()) {
            return null;
        } else {
            return o;
        }
    }
}

2. 仅使用<aop:aspectj-autoproxy />时的问题

  • 切面中无法获取参数名(已通过自动生成参数名临时解决);
  • 解决EntityManager为空问题后,调用delete方法删除实体后,调用find方法仍返回旧状态,疑似实体被持久化上下文缓存。

环境信息

  • Java版本:8
  • 核心依赖:
<spring.version>4.3.30.RELEASE</spring.version>
....
    <!-- Spring -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-expression</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-support</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-orm</artifactId>
        <version>${spring.version}</version>
    </dependency>
    
    
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.19</version>
    </dependency>

解决方案

针对问题1:CGLIB代理下内部调用EntityManager为null的问题

原因

设置proxy-target-class="true"时Spring使用CGLIB生成子类代理,但你的setEntityManager、delete、find均为final方法——CGLIB无法代理final方法,导致Spring的依赖注入和事务切面无法生效到这些方法上。内部调用时this指向目标对象而非代理对象,目标对象的em未被正确注入,因此出现null。

解决方法

方法1:移除方法的final修饰符

让CGLIB可以代理这些方法,内部调用时会通过代理对象访问,确保em正确注入:

@Transactional(readOnly = false, propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public class GenericServiceImpl<T extends IEntity<PK>, PK extends Serializable> implements IGenericService<T, PK> {
    private final Class<T> type;
    
    public GenericServiceImpl(final Class<T> type) {
        this.type = type;
    }

    private EntityManager em;
    
    @PersistenceContext
    public void setEntityManager(final EntityManager em) { // 移除final
        this.em = em;
    }

    public EntityManager getEntityManager() { // 移除final
        return em;
    }
 
    public void delete(final PK pk) { // 移除final
        T o = find(pk);
        if (o != null) {
            o.setDeleted(true);
            save(o);
        }
    }

    @Transactional(readOnly = true)
    public T find(final PK pk) { // 移除final
        if (pk == null) {
            return null;
        }

        T o = em.find(type, pk);
        if (o != null && o.isDeleted()) {
            return null;
        } else {
            return o;
        }
    }
}

方法2:通过Spring上下文获取代理对象(耦合性较高)

如果无法修改方法修饰符,可在内部调用时获取代理对象:

@Autowired
private ApplicationContext applicationContext;

public final void delete(final PK pk) {
    IGenericService<T, PK> proxy = applicationContext.getBean(getClass());
    T o = proxy.find(pk);
    if (o != null) {
        o.setDeleted(true);
        proxy.save(o);
    }
}

针对问题2:JDK动态代理下的两个问题

问题2.1:切面无法获取参数名

原因

JDK动态代理依赖接口,Spring默认依赖Java 8编译时保留的参数名信息。如果编译时未添加-parameters参数,Spring无法直接获取参数名。

解决方法

在Maven编译插件中添加-parameters参数,确保编译时保留参数名:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.1</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <compilerArgs>
            <arg>-parameters</arg>
        </compilerArgs>
    </configuration>
</plugin>

问题2.2:delete后find返回旧状态(持久化上下文缓存)

原因

逻辑删除后,EntityManager的一级缓存仍保留实体旧状态,find方法优先从缓存读取,而非查询数据库。

解决方法

方法1:手动管理缓存

在save后同步数据库并清除该实体的缓存:

public void delete(final PK pk) {
    T o = find(pk);
    if (o != null) {
        o.setDeleted(true);
        save(o);
        em.flush(); // 同步变更到数据库
        em.detach(o); // 从缓存移除该实体
    }
}

方法2:使用JPQL查询绕过缓存

修改find方法为JPQL查询,直接从数据库获取最新状态:

@Transactional(readOnly = true)
public T find(final PK pk) {
    if (pk == null) {
        return null;
    }
    TypedQuery<T> query = em.createQuery(
        "SELECT t FROM " + type.getSimpleName() + " t WHERE t.id = :id AND t.deleted = false", 
        type
    );
    query.setParameter("id", pk);
    List<T> result = query.getResultList();
    return result.isEmpty() ? null : result.get(0);
}

方法3:调整事务传播行为

将find方法设置为开启新事务,新事务的EntityManager缓存是干净的:

@Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
public T find(final PK pk) {
    // 原有逻辑
}

额外建议

  • 优先使用JDK动态代理(不设置proxy-target-class=true),Spring对接口代理的支持更成熟,避免CGLIB代理的final方法限制;
  • 所有需要AOP增强(事务、自定义切面)的方法,不要设置为final。

内容的提问来源于stack exchange,提问作者Volodymyr Bilovus

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 03:25:09