主要的思路:配置多个数据源加到动态数据源对象中,根据实际的情况动态的切换到相应的数据源。
架构流程图:
执行的步骤:建立数据源->数据源加到动态数据源对象->动态数据源的配置->动态切换
1、建立数据源
这一步比较简单,根据连接池(例如:HikariCP、c3p0)建立相关的数据源,本例中使用HikariCP。
@Configuration// 配置数据源
@RefreshScope
public class DataSourceConfigure implements Ordered, EnvironmentAware {
private final Logger logger = LoggerFactory.getLogger(DataSourceConfigure.class);
private static final String DATASOURCE_TYPE_DEFAULT = "com.zaxxer.hikari.HikariDataSource";
private Environment environment;
private Class dataSourceClass;
public DataSourceConfigure() {
Class dataSourceClass = null;
try {
dataSourceClass = Class.forName(DATASOURCE_TYPE_DEFAULT);
} catch (ClassNotFoundException e) {
logger.error("不存在类:" + DATASOURCE_TYPE_DEFAULT);
e.printStackTrace();
}
this.dataSourceClass = dataSourceClass;
}
@Bean
@RefreshScope
@Primary
public DataSource xxx() {
logger.info("注册数据源:xxx");
return (DataSource) Binder.get(environment).bind( "custom.datasource.xxx", this.dataSourceClass).orElse(null);
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
如上面的代码段所示,根据自己的实际内容建立相应的数据源。
2、数据源加到动态数据源对象
这一步把相关的数据源增加到动态数据源对象中,直接上代码。
@Configuration
@EnableAutoConfiguration
public class DatabaseConfiguration {
@Autowired
private ApplicationContext appContext;
private static final String DATASOURCE_PREFIX = "custom.datasource";
@Bean
public AbstractRoutingDataSource routingDataSource() {
RoutingDataSource proxy = new RoutingDataSource();
Map
3、动态数据源的配置(根据实际的使用,配置相关的配置,本例中使用的是Spring-data-jpa。)
下面的代码只是简单的配置一下JPA配置类,使得支持jpa的使用,需要根据实际情况修改。
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
entityManagerFactoryRef = "entityManagerFactoryDynamic",
transactionManagerRef = "transactionManagerDynamic",
basePackages = {"com.xxx.gbimclientdynamic.repository"},//设置Repository所在位置
repositoryFactoryBeanClass = BaseRepositoryFactoryBean.class
)
public class DynamicConfig {
@Autowired
@Qualifier("routingDataSource") //配置中定义的名字
private DataSource routingDataSource;
@Bean(name = "entityManagerFactoryDynamic")
@Primary
public EntityManagerFactory entityManagerFactoryDynamic() {
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
factory.setPackagesToScan("com.xxx.gbimclientdynamic.entity");
factory.setDataSource(routingDataSource);//数据源
factory.setPersistenceUnitName("dynamicPersistenceUnit");
Properties properties = new Properties();
properties.put("hibernate.show_sql", true);
properties.put("hibernate.dialect","org.hibernate.dialect.SQLServer2012Dialect");
factory.setJpaProperties(properties);
factory.afterPropertiesSet();//在完成了其它所有相关的配置加载以及属性设置后,才初始化
return factory.getObject();
}
@Bean(name = "transactionManagerDynamic")
@Primary
PlatformTransactionManager transactionManagerDynamic() {
return new JpaTransactionManager(entityManagerFactoryDynamic());
}
}
4、动态切换
这一步的这个例子的重点,这里会说得稍微详细一些。
首先要考虑的就是怎么可以实现切换呢?Spring提供了AbstractRoutingDataSource来实现这样的功能,AbstractRoutingDataSource的功能是在其中可以根据key值动态切换到具体的数据源。
AbstractRoutingDataSource的具体的实现,可以看一下具体的实现代码。
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
......
......
}
由代码中看到AbstractRoutingDataSource 是继承于 AbstractDataSource ,而AbstractDataSource是DataSource 的一个实现类,所以这里主要是要看一下获得连接的方法,即如下:
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
在getConnection()
方法中的determineTargetDataSource()
明显就是确定数据源的方法,所以我们继续看一下determineTargetDataSource()
这个方法。
/**
* 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");
Object lookupKey = determineCurrentLookupKey();
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;
}
在determineTargetDataSource()
中,determineCurrentLookupKey();
是获取数据源dataSource的key值的,在AbstractRoutingDataSource
类中,determineCurrentLookupKey()
是一个抽象方法,就该子类就必须重新该方法。接着根据determineCurrentLookupKey()
获得的key值,在this.resolvedDataSources
中获得dataSource
。如果不存在,就根据默认设置默认的数据源。
所以,根据源码,继承AbstractRoutingDataSource
类,并重写其中的determineCurrentLookupKey()
方法,就可以实现数据源的切换。
上述内容基本说明了如何切的问题,接下来需要考虑在何时切,并且应该如何实现的问题。
其实这里对简单的就是利用AOP了,AOP的具体内容可以自己去网上找一下,这里就不作详细的介绍了。就是利用AOP在调用需要调用数据库的方法前就设定好需要的数据源(利用上述的determineCurrentLookupKey()
)。
所以,动态切换的大体思路是这样的:
利用AOP,调用需要调用数据库的方法前利用determineCurrentLookupKey()
设定好需要的数据源。
完整代码如下:
//动态数据源存放对象,保存key值,用于动态切换使用
public class DbContextHolder {
//线程本地环境,ThreadLocal 是用于线性安全的
private static final ThreadLocal dataSources = new ThreadLocal();
//设置数据源
public static void setDataSource(String customerType) {
dataSources.set(customerType);
}
//获取数据源
public static String getDataSource() {
return dataSources.get();
}
//清除数据源
public static void clearDataSource() {
dataSources.remove();
}
}
//用于动态切换,就是上面说的源码的这部分
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder .getDataSource();
}
}
//定义一个注解
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DbName {
String value() default "";
}
//切面
@Aspect
@Component
public class DataSourceAspect {
private Logger logger = LoggerFactory.getLogger(getClass().getName());
//作用于service层 ||@args(com.xxx.connection.aop.RoutingDbName)
//这个匹配能匹配到类,方法,无法匹配到参数
@Pointcut("execution(public * com.xxx..*.service.impl..*.*(..))&&(@target(com.xxx.connection.aop.DbName)||@annotation(com.xxx.connection.aop.DbName))||execution(public * com.xxx..*.service.impl..*.*(.., @DbName(*), ..))")
public void routingDsPointCut() {
}
//方法切面,不指定注解的值时,第一个参数作为dbName,指定注解的值,可指定数据源,优先级:类->方法->参数
@Around("routingDsPointCut()")
public Object proceeRouting(ProceedingJoinPoint joinpoint) throws Throwable {
logger.info("set connection on service");
Object result = null;
MethodSignature signature = (MethodSignature) joinpoint.getSignature();
Object[] args = joinpoint.getArgs();
DbName dbName = null;
String dbStr = "";
//获取切面方法
Method method = signature.getMethod();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
Class[] parameterTypes = method.getParameterTypes();
//获取参数注解
for(int i =0;i0){
DbContextHolder.setDataSource((String)args[0]);
logger.info("使用第一个参数的值,当前使用"+(String)args[0]+"数据源!");
}else { //没有指定数据源
logger.info("注解的值不能为空!使用默认数据源!");
}
}else{
logger.info("service不切换数据源!");
}
result = joinpoint.proceed();//执行前后
logger.info("数据源切换完毕!");
return result;
}
}
5、拓展
上述的四步已经基本的说明了一个简单的动态数据源切换了,但是上面提交的数据源是已经确定了,如果数据源数据源还不确定的情况下,能不能动态的生成数据源动态的切换呢?答案是肯定的。这就是本部分要说的内容。
其实实现的思路也很简单:在切换前看看有没有需要切换的数据源,如果没有,就根据相关的配置生成。所以,内容基本和上述的四步很类似,唯一不一样的就是继承AbstractRoutingDataSource
类重新的determineCurrentLookupKey()
方法。直接上代码:
public class RoutingDataSource extends AbstractRoutingDataSource {
/**
* 默认值数据源类型,如需要别的数据源需要修改
*/
private final String DATASOURCE_TYPE_DEFAULT = "com.zaxxer.hikari.HikariDataSource";
/**
* 别名
*/
private final static ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases();
private Logger logger = LoggerFactory.getLogger(getClass().getName());
@Override
protected Object determineCurrentLookupKey() {
//需要切换的数据库
String dbName = DbContextHolder.getDataSource();
if (dbName == null) {
return null;
}
dbName=dbName.toLowerCase();
try {
/**
* 获取AbstractRoutingDataSource的targetDataSources属性,该属性存放数据源属性
*`
**/
Map
在这里determineCurrentLookupKey()
方法与第四步不相同的地方主要是这部分:
/**
* 获取AbstractRoutingDataSource的targetDataSources属性,该属性存放数据源属性
*`
**/
Map targetSourceMap = getTargetSource();
synchronized (this) {
//判断targetDataSources中是否已经存在要设置的数据源bean
// 存在的话,则直接返回beanName
// 不存在的话,则需要建立数据源
if (!targetSourceMap.keySet().contains(dbName)) {
logger.info("数据不存在,建立数据源。");
//建立数据源
Object dataSource = createDataSource(dbName,EnvironmentUtil.getEnvironment());
logger.info("建立数据源成功。");
/**
* 在创建后的bean,放入到targetDataSources Map中
* **/
targetSourceMap.put(dbName, dataSource);
logger.info("数据源放入到targetDataSources Map中。");
//通知spring有bean更新
super.afterPropertiesSet();
logger.info("通知spring有bean更新");
}else{
logger.info("数据已存在,切换到相应的数据源");
}
简单的看一下,这部分其实是和刚刚提到的思路完全一致的。首先判断一下有没有需要的数据源(if (!targetSourceMap.keySet().contains(dbName))
),如果没有的情况建立数据源(createDataSource(dbName,EnvironmentUtil.getEnvironment())
),然后加到AbstractRoutingDataSource
的targetDataSources
属性中(targetSourceMap.put(dbName, dataSource);
),最后通知spring有bean更新(super.afterPropertiesSet();
)。