利用切面自动切换数据源,出现多数据源情况
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方法可能有几个操作数据库的方法,连接池只是一个,只需处理一次切面切换,而不需要处理多次数据源切换【虽然一样】
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语句,不然就不生效了。