在项目中遇到需要使用多数据源的情况,解决办法是,使用注解,切面拦截来注入不同的dataSource。实现代码在底部。
基本思路:在dao的方法前加上@TargetDataSource(ORDER_DATA_SOURCE)注解来表明使用的哪个数据源。
问题:事务开启一般是在service中开启,事务开启后会导致数据源切换失败,数据源切换需要在事务开启前执行。
解决:
1、@EnableAspectJAutoProxy(exposeProxy = true)
2、数据源切入点@Pointcut增加service切入点,其次service中需要开启事务的方法写两个,
3、方法1加@TargetDataSource(ORDER_DATA_SOURCE),方法2加@Transactional,
4、控制器调用方法1。方法1(使用代理方式调用,而不是直接调用)调用方法2。
总结:控制器->方法1(切换数据源,使用代理方式调用方法2)->方法2(开启事务,执行多个dao操作)
代理调用示例:
public class TestService{ @TargetDataSource(ORDER_DATA_SOURCE) public void method1() { ((TestService)AopContext.currentProxy()).method2(); } @Transactional public void method2() { //dao操作 } }
动态切换数据源示例:
/** * 目标数据源注解,注解在方法上指定数据源的名称 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface TargetDataSource { String value();//此处接收的是数据源的名称 }
/** * 动态数据源持有者,负责利用ThreadLocal存取数据源名称 */ public class DynamicDataSourceHolder { /** * 本地线程共享对象 */ private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>(); public static void putDataSource(String name) { THREAD_LOCAL.set(name); } public static String getDataSource() { return THREAD_LOCAL.get(); } public static void removeDataSource() { THREAD_LOCAL.remove(); } }
/** * 目标数据源注解,注解在方法上指定数据源的名称 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface TargetDataSource { String value();//此处接收的是数据源的名称 }
@Component @Data @ConfigurationProperties(prefix = "spring") public class DBProperties { private DataSource datasource; private DataSource orderDatasource ; }
@Configuration @EnableScheduling @Slf4j public class DataSourceConfig { @Autowired private DBProperties properties; @Bean(name = "dataSource") public DataSource dataSource() { //按照目标数据源名称和目标数据源对象的映射存放在Map中 Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("datasource", properties.getDatasource()); targetDataSources.put("orderDatasource", properties.getOrderDatasource()); //采用是想AbstractRoutingDataSource的对象包装多数据源 DynamicDataSource dataSource = new DynamicDataSource(); dataSource.setTargetDataSources(targetDataSources); //设置默认的数据源,当拿不到数据源时,使用此配置 dataSource.setDefaultTargetDataSource(properties.getDatasource()); return dataSource; } @Bean public PlatformTransactionManager txManager() { return new DataSourceTransactionManager(dataSource()); } public static final String ORDER_DATA_SOURCE = "orderDatasource"; }
@Component @Aspect @Slf4j public class DataSourceAspect { private static final Logger LOG = LoggerFactory.getLogger(DataSourceAspect.class); //切换放在mapper接口的方法上,所以这里要配置AOP切面的切入点 //增加api 控制器切入点,是因为动态数据源切换需要在事务开启前执行,故需要在service前切换 @Pointcut("execution( * com.lifeccp.besra.repository..*.*(..)) || execution( * com.lifeccp.besra.service..*.*(..))") public void dataSourcePointCut() { } @Before("dataSourcePointCut()") public void before(JoinPoint joinPoint) { Object target = joinPoint.getTarget(); String method = joinPoint.getSignature().getName(); Class claz = target.getClass() ; Class<?>[] clazz = target.getClass().getInterfaces(); Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes(); try { Method m = null ; if(clazz.length <= 0){ m = claz.getMethod(method,parameterTypes) ; }else{ m = clazz[0].getMethod(method, parameterTypes); } //如果方法上存在切换数据源的注解,则根据注解内容进行数据源切换 if (m != null && m.isAnnotationPresent(TargetDataSource.class)) { TargetDataSource data = m.getAnnotation(TargetDataSource.class); String dataSourceName = data.value(); DynamicDataSourceHolder.putDataSource(dataSourceName); LOG.info("current thread " + Thread.currentThread().getName() + " add " + dataSourceName + " to ThreadLocal"); } else { LOG.info("switch datasource fail,use default"); } } catch (Exception e) { LOG.error("current thread " + Thread.currentThread().getName() + " add data to ThreadLocal error", e); } } //执行完切面后,将线程共享中的数据源名称清空 @After("dataSourcePointCut()") public void after(JoinPoint joinPoint){ DynamicDataSourceHolder.removeDataSource(); } }