利用切面自动切换数据源,出现多数据源情况

1、需要切面首先切面注解(DataSourceTypeAnno)

2、数据源枚举类(表示数据源的bean)(DataSourceEnum)

3、Aspect切面对象 (DataSourceAspect)

4、线程上下文对象,以保证线程间安全,不同线程使用不同数据源(DataSourceContextHolder)

5、实现spring一个接口AbstractRoutingDataSource 的 dermineCurrentLookupKey()方法(也就是在spring创建连接对象时候会先调用这个方法来替换数据源)(DynamicDataSource)

6、具体配置多个bean的数据源config配置文件(MyBatisDefaultConfig)


文章目录

  • 1、DataSourceTypeAnno
  • 2、DataSourceEnum
  • 3、DataSourceAspect
  • 4、DataSourceContextHolder
  • 5、DynamicDataSource
  • 6、MyBatisDefaultConfig
  • 8、源码
  • 9、注意点
  • 10、参考


1、DataSourceTypeAnno

/**
 * @author liangchen
 * @date 2020/12/8
 */
// 在运行时可见
@Retention(RetentionPolicy.RUNTIME)
// 注解可以用在方法上
@Target({ElementType.METHOD})
public @interface DataSourceTypeAnno {
    DataSourceEnum value() default DataSourceEnum.DEFUALT;
}

2、DataSourceEnum

/**
 * @author liangchen
 * @date 2020/12/8
 */
public enum DataSourceEnum {
    /**
     * 默认数据
     */
    DEFUALT,
  	/** 账户数据源*/
    ACCOUNT;
}

3、DataSourceAspect

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 数据源连接器切面
 * @author liangchen
 * @date 2020/12/8
 */
@Component
@Aspect
@Log4j
public class DataSourceAspect {
    
//注解类路径  @Pointcut("@annotation(com.jack.base.annotations.DataSourceTypeAnno)")
    public void dataSourcePointcut() {
    }

    @Around("dataSourcePointcut()")
    public Object doAround(ProceedingJoinPoint pjp) {
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Method method = methodSignature.getMethod();
        DataSourceTypeAnno typeAnno = method.getAnnotation(DataSourceTypeAnno.class);
        DataSourceEnum sourceEnum = typeAnno.value();
				// 设置数据源标志,spring获取这些标志之后就会获取对象的数据源,其实就bean的实例名称
        if (sourceEnum == DataSourceEnum.DEFUALT) {
            DataSourceContextHolder.setDataSourceType(DataSourceEnum.DEFUALT);
        } else if (sourceEnum == DataSourceEnum.ACCOUNT) {
            DataSourceContextHolder.setDataSourceType(DataSourceEnum.ACCOUNT);
        }

        Object result = null;
        try {
            result = pjp.proceed();
        } catch (Throwable throwable) {
            log.error("切换数据源发生异常",throwable);
        } finally {
            DataSourceContextHolder.resetDataSourceType();
        }

        return result;
    }
}

4、DataSourceContextHolder

/**
 * @author liangchen
 * @date 2020/12/8
 */
public class DataSourceContextHolder {
    private static final ThreadLocal<DataSourceEnum> CONTEXT_HOLDER = new ThreadLocal<DataSourceEnum>() {

        @Override
        protected DataSourceEnum initialValue() {
           // 初始化线程的数据源是 DEFAULT
            return DataSourceEnum.DEFUALT;
        }
    };


    public static void setDataSourceType(DataSourceEnum type) {
        CONTEXT_HOLDER.set(type);
    }

    public static DataSourceEnum getDataSourceType() {
        return CONTEXT_HOLDER.get();
    }

    public static void resetDataSourceType() {
        CONTEXT_HOLDER.set(DataSourceEnum.DEFUALT);
    }
}

5、DynamicDataSource

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * @author liangchen
 * @date 2020/12/8
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
  // spring在使用connection时候需要取那个数据源
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceType();
    }
}

6、MyBatisDefaultConfig

//配置数据源
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @author liangchen
 * @date 2020/12/8
 */
@Configuration
@MapperScan(basePackages = "com.jack.bo.*.dao")
public class MyBatisConfig {
    static final String CONFIG_LOCATION = "classpath:mybatis-config.xml";
    static final String MAPPER_LOCATION = "classpath:mybatis/default/*.xml";
    static final String SQL_SESSION_FACTORY = "defaultSqlSessionFactory";
    static final String TRANSACTION_MANAGER = "defaultTransactionManager";
    /**
     * @return
     * @throws Exception
     * @Primary 该注解表示在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@autowire注解报错
     */

    @Primary
    @Bean("defautDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.default")
    public DataSource defautDataSource() throws Exception {
        return DataSourceBuilder.create().build();
    }

    @Bean("accountDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.account")
    public DataSource accountDataSource() throws Exception {
        return DataSourceBuilder.create().build();
    }

    /**
     * @Qualifier 根据名称进行注入,通常是在具有相同的多个类型的实例的一个注入(例如有多个DataSource类型的实例)
     */
    @Bean("dynamicDataSource")
    public DynamicDataSource dynamicDataSource(@Qualifier("defaultDataSource") DataSource defautDataSource,
                                               @Qualifier("accountDataSource") DataSource accountDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
        targetDataSources.put(DataSourceEnum.ACCOUNT, accountDataSource);
        targetDataSources.put(DataSourceEnum.DEFUALT, defautDataSource);

        DynamicDataSource dataSource = new DynamicDataSource();
        // 该方法是AbstractRoutingDataSource的方法
        dataSource.setTargetDataSources(targetDataSources);
        // 默认的datasource设置为myTestDbDataSource
        dataSource.setDefaultTargetDataSource(defautDataSource);

        return dataSource;
    }
   
    //配置一个jdbcTemplate
    @Bean
    public JdbcTemplate jdbcTemplate(@Qualifier("dynamicDataSource") DataSource dataSource){
        return new JdbcTemplate(dataSource);
    }
    /**
     * 根据数据源创建SqlSessionFactory
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DynamicDataSource dynamicDataSource,
                                               @Value("mybatis.typeAliasesPackage") String typeAliasesPackage,
                                               @Value("mybatis.mapperLocations") String mapperLocations) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dynamicDataSource);// 指定数据源(这个必须有,否则报错)
        // 下边两句仅仅用于*.xml文件,如果整个持久层操作不需要使用到xml文件的话(只用注解就可以搞定),则不加
//        factoryBean.setTypeAliasesPackage(typeAliasesPackage);// 指定基包
//        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));//
        factoryBean.setConfigLocation(new PathMatchingResourcePatternResolver()
                .getResource(CONFIG_LOCATION));
        factoryBean.setVfs(SpringBootVFS.class);
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources(MAPPER_LOCATION));
        return factoryBean.getObject();
    }

    /**
     * 配置事务管理器
     */
    @Bean
    public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception {
        return new DataSourceTransactionManager(dataSource);
    }
}
  • @ConfigurationProperties(prefix = “spring.datasource.default”) 需要在yml文件或配置中心配置一下数据源,表示加载以spring.datasource.default开头的属性进行对象的属性字段填充

###7、切点位置

  • 可以controller,service,dao,最好还是放在service层的方法上,因为如果放到controller,如果其他类直接调用service不会切换数据源, 放在dao,控制有点细了,一个service方法可能有几个操作数据库的方法,连接池只是一个,只需处理一次切面切换,而不需要处理多次数据源切换【虽然一样】

springboot 定义切面扫描 springboot切面获取注解属性_springboot 定义切面扫描

8、源码

  • 切换源代码图示
/** 这一段就数据源切换代码
	 * Retrieve the current target DataSource. Determines the
	 * {@link #determineCurrentLookupKey() current lookup key}, performs
	 * a lookup in the {@link #setTargetDataSources targetDataSources} map,
	 * falls back to the specified
	 * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
	 * @see #determineCurrentLookupKey()
	 */
protected DataSource determineTargetDataSource() {
		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    // 获取自己定义key(枚举类)
		Object lookupKey = determineCurrentLookupKey();
    // resolvedDataSources 是加载所有数据源(当前是两个)
		DataSource dataSource = this.resolvedDataSources.get(lookupKey);
   // 如果没有就用默认的数据源
		if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
			dataSource = this.resolvedDefaultDataSource;
		}
		if (dataSource == null) {
			throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
		}
		return dataSource;
	}
  • 那么 resolvedDataSources 其实是一个map, 它是怎么生成呢
    -
  • 核心代码
@Override
	public void afterPropertiesSet() {
    // 在@Bean初始化进去的两个
		if (this.targetDataSources == null) {
			throw new IllegalArgumentException("Property 'targetDataSources' is required");
		}
		this.resolvedDataSources = new HashMap<Object, DataSource>(this.targetDataSources.size());
    // 设置数据源
		for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) {
			Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());
			DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());
			this.resolvedDataSources.put(lookupKey, dataSource);
		}
		if (this.defaultTargetDataSource != null) {
			this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
		}
	}

9、注意点

1、 jdbctemplate必须调用有注解的服务层或者dao,不能单独写sql语句,不然就不生效了。