Spring事务只读切面未触发及数据源路由失效问题咨询
问题:Spring读写分离切面未触发,数据源路由失效
核心现象
- 标记
@Transactional(readOnly = true)的方法执行时,TransactionReadonlyAspect切面逻辑完全不触发(控制台无Aspect executed输出) - 所有数据库操作始终默认使用主数据源,从未切换至从库
DatabaseContextHolder的ThreadLocal变量始终返回null,导致MasterSlaveRoutingDataSource无法获取正确路由key
相关代码片段
切面类
@Aspect @Component @Order(0) public class TransactionReadonlyAspect { @Around("@annotation(transactional)") public Object proceed(ProceedingJoinPoint proceedingJoinPoint, org.springframework.transaction.annotation.Transactional transactional) throws Throwable { System.out.println("Aspect executed"); try { if (transactional.readOnly()) { DatabaseContextHolder.set(DatabaseEnvironment.READONLY); } return proceedingJoinPoint.proceed(); } finally { DatabaseContextHolder.reset(); } } }
数据源配置类
package com.programmingsharing.demoreadwriterouting.conf; import com.programmingsharing.demoreadwriterouting.context.DatabaseEnvironment; import com.programmingsharing.demoreadwriterouting.datasource.MasterSlaveRoutingDataSource; import com.zaxxer.hikari.HikariDataSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; @Configuration public class DataSourceConfiguration { @Value("${jdbc.master.url}") private String mstUrl; @Value("${jdbc.master.username}") private String mstUsername; @Value("${jdbc.master.password}") private String mstPassword; @Value("${jdbc.slave.url}") private String slaveUrl; @Value("${jdbc.slave.username}") private String slaveUsername; @Value("${jdbc.slave.password}") private String slavePassword; @Bean public DataSource dataSource(){ MasterSlaveRoutingDataSource masterSlaveRoutingDataSource = new MasterSlaveRoutingDataSource(); Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DatabaseEnvironment.UPDATABLE, masterDataSource()); targetDataSources.put(DatabaseEnvironment.READONLY, slaveDataSource()); masterSlaveRoutingDataSource.setTargetDataSources(targetDataSources); // 默认指向主库 masterSlaveRoutingDataSource.setDefaultTargetDataSource(masterDataSource()); return masterSlaveRoutingDataSource; } public DataSource slaveDataSource() { HikariDataSource hikariDataSource = new HikariDataSource(); hikariDataSource.setJdbcUrl(slaveUrl); hikariDataSource.setUsername(slaveUsername); hikariDataSource.setPassword(slavePassword); return hikariDataSource; } public DataSource masterDataSource() { HikariDataSource hikariDataSource = new HikariDataSource(); hikariDataSource.setJdbcUrl(mstUrl); hikariDataSource.setUsername(mstUsername); hikariDataSource.setPassword(mstPassword); return hikariDataSource; } }
上下文持有类
public class DatabaseContextHolder { private static final ThreadLocal<DatabaseEnvironment> CONTEXT = new ThreadLocal<>(); public static void set(DatabaseEnvironment databaseEnvironment) { CONTEXT.set(databaseEnvironment); } public static DatabaseEnvironment getEnvironment() { System.out.println("CONTEXT.get() : " + CONTEXT.get()); return CONTEXT.get(); } public static void reset() { CONTEXT.set(DatabaseEnvironment.UPDATABLE); } }
数据源路由类
public class MasterSlaveRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { System.out.println("DatabaseContextHolder.getEnvironment() : " + DatabaseContextHolder.getEnvironment()); return DatabaseContextHolder.getEnvironment(); } }
解决方案
1. 启用Spring AOP自动代理
在配置类或主启动类上添加@EnableAspectJAutoProxy(proxyTargetClass = true),确保Spring能识别并创建切面代理。同时检查@ComponentScan是否包含切面类所在包路径,避免切面未被扫描加载。
2. 调整切面执行顺序
Spring事务切面默认优先级为Ordered.LOWEST_PRECEDENCE,需让你的切面先于事务切面执行(否则事务绑定数据源后切换ThreadLocal无效)。修改切面的@Order为最高优先级:
@Aspect @Component @Order(Ordered.HIGHEST_PRECEDENCE) public class TransactionReadonlyAspect { // 原有代码不变 }
3. 修复切点匹配逻辑
如果@Transactional注解标注在接口方法上,默认JDK动态代理无法拦截接口注解。此时要么启用CGLIB代理(已通过proxyTargetClass = true实现),要么直接在实现类方法上标注@Transactional注解。
4. 完善DatabaseContextHolder逻辑
- 为
getEnvironment()添加默认值,避免未设置时返回null:
public static DatabaseEnvironment getEnvironment() { DatabaseEnvironment env = CONTEXT.get(); System.out.println("CONTEXT.get() : " + env); return env != null ? env : DatabaseEnvironment.UPDATABLE; }
- 修改
reset()方法移除ThreadLocal值,避免内存泄漏:
public static void reset() { CONTEXT.remove(); }
5. 确保事务管理启用
在配置类或主启动类上添加@EnableTransactionManagement,否则@Transactional注解不会生效,切面自然无法触发。
内容的提问来源于stack exchange,提问作者user19751568
相关产品推荐
相关产品推荐

